From 4d9603bd8a86e60ccb40f5ce6f96839340ed559b Mon Sep 17 00:00:00 2001 From: Hona <10430890+Hona@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:27:54 +0000 Subject: [PATCH] fix(app): keep root tab active for subagents --- .../subagent-child-navigation.spec.ts | 67 ++++++++++++++++++- packages/app/src/components/titlebar.tsx | 65 +++++++++++------- 2 files changed, 107 insertions(+), 25 deletions(-) diff --git a/packages/app/e2e/regression/subagent-child-navigation.spec.ts b/packages/app/e2e/regression/subagent-child-navigation.spec.ts index 9a2ba091dc5..00c4500dfaa 100644 --- a/packages/app/e2e/regression/subagent-child-navigation.spec.ts +++ b/packages/app/e2e/regression/subagent-child-navigation.spec.ts @@ -8,10 +8,13 @@ const directory = "C:/OpenCode/SubagentNavigation" const projectID = "proj_subagent_navigation" const parentID = "ses_subagent_parent" const childID = "ses_subagent_child" +const grandchildID = "ses_subagent_grandchild" const parentTitle = "Parent session" const childTitle = "Subagent child session" +const grandchildTitle = "Nested subagent session" // Child session pages derive their heading from the task part that spawned them. const taskDescription = "Inspect child navigation" +const nestedTaskDescription = "Inspect nested navigation" test.use({ viewport: { width: 1440, height: 900 } }) @@ -26,6 +29,23 @@ test("navigates to a subagent child session missing from the session list", asyn await expect(titlebarRight.getByRole("button", { name: "Toggle review" })).toHaveCount(1) }) +test("keeps the root tab active for a nested subagent", async ({ page }) => { + await setup(page) + await openChildFromParent(page) + await expectSessionTitle(page, taskDescription) + + const card = page.locator(`a[href="${sessionHref(grandchildID)}"]`) + await expect(card).toBeVisible() + await card.click() + + await expect(page).toHaveURL(new RegExp(`/server/.+/session/${grandchildID}$`), { timeout: 15_000 }) + await expectSessionTitle(page, nestedTaskDescription) + + const rootTab = page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(parentID)}"])`) + await expect(rootTab).toHaveAttribute("data-active", "true") + await expect(page.locator("[data-titlebar-tab-slot]:visible")).toHaveCount(1) +}) + test("keeps the parent visible while the child session resolves", async ({ page }) => { await setup(page) const requested = Promise.withResolvers() @@ -94,8 +114,10 @@ async function setup(page: Page, events?: () => OpenCodeEvent[]) { connected: ["opencode"], default: { providerID: "opencode", modelID: "claude-opus-4-6" }, }, - sessions: [session(parentID, parentTitle, 1700000000000), childSession()], - pageMessages: (sessionID) => ({ items: sessionID === parentID ? parentMessages() : [] }), + sessions: [session(parentID, parentTitle, 1700000000000), childSession(), grandchildSession()], + pageMessages: (sessionID) => ({ + items: sessionID === parentID ? parentMessages() : sessionID === childID ? childMessages() : [], + }), events, eventRetry: events ? 16 : undefined, }) @@ -145,6 +167,10 @@ function childSession() { return session(childID, childTitle, 1700000001000, { parentID }) } +function grandchildSession() { + return session(grandchildID, grandchildTitle, 1700000002000, { parentID: childID }) +} + function parentMessages(): SessionMessageInfo[] { const userID = "msg_user_0001" const assistantID = "msg_assistant_0001" @@ -182,6 +208,43 @@ function parentMessages(): SessionMessageInfo[] { ] } +function childMessages(): SessionMessageInfo[] { + const userID = "msg_user_0002" + const assistantID = "msg_assistant_0002" + return [ + { + id: userID, + type: "user", + time: { created: 1700000002000 }, + text: "Delegate nested work to a subagent", + }, + { + id: assistantID, + type: "assistant", + time: { created: 1700000003000, completed: 1700000004000 }, + model: { id: "claude-opus-4-6", providerID: "opencode" }, + agent: "build", + cost: 0.01, + tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + finish: "stop", + content: [ + { + type: "tool", + id: "call_subagent_0002", + name: "subagent", + time: { created: 1700000003000, ran: 1700000003000, completed: 1700000004000 }, + state: { + status: "completed", + input: { description: nestedTaskDescription, agent: "explore", prompt: "Inspect the nested work." }, + content: [{ type: "text", text: "Nested subagent finished" }], + metadata: { sessionID: grandchildID }, + }, + }, + ], + }, + ] +} + async function configurePage(page: Page) { const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` await page.addInitScript( diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 39daac2f43f..3d9f2b6ff78 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -34,6 +34,7 @@ import { tabKey, useTabs } from "@/context/tabs" import type { PromptSession } from "@/context/prompt" import "./titlebar.css" import { newTabTooltipKeybind } from "./command-tooltip-keybind" +import { rootSession } from "@/utils/session-route" const v2TitlebarHeight = 36 const minTitlebarZoom = 0.25 @@ -170,15 +171,43 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl const tabs = useTabs() const tabsStore = tabs.store const tabsStoreActions = tabs - const [session] = createResource( - () => { - const route = layout.route() - if (route.type !== "session") return undefined - const conn = global.servers.list().find((item) => ServerConnection.key(item) === route.server) - return conn ? { route, sdk: global.ensureServerCtx(conn).sdk } : undefined - }, - ({ route, sdk }) => sdk.api.session.get({ sessionID: route.sessionId }).catch(() => {}), + const routeContext = createMemo(() => { + const route = layout.route() + if (route.type !== "session") return + const conn = global.servers.list().find((item) => ServerConnection.key(item) === route.server) + return conn ? { route, ctx: global.ensureServerCtx(conn) } : undefined + }) + const [resolvedSession] = createResource(routeContext, ({ route, ctx }) => + (async () => { + const session = + ctx.data.session.get(route.sessionId) ?? + (await ctx.sdk.api.session.get({ sessionID: route.sessionId })) + const root = await rootSession( + session, + async (sessionID) => + ctx.data.session.get(sessionID) ?? (await ctx.sdk.api.session.get({ sessionID })), + ) + return { session, rootID: root.id } + })().catch(() => undefined), ) + const session = () => { + const input = routeContext() + if (!input) return + const loaded = input.ctx.data.session.get(input.route.sessionId) + if (loaded) return loaded + const resolved = resolvedSession() + return resolved?.session.id === input.route.sessionId ? resolved.session : undefined + } + const rootID = () => { + const input = routeContext() + if (!input) return + const current = input.ctx.data.session.get(input.route.sessionId) + const resolved = resolvedSession() + if (!current) return resolved?.session.id === input.route.sessionId ? resolved.rootID : undefined + const root = input.ctx.data.session.root(current.id) + if (!current.parentID || input.ctx.data.session.get(root)) return root + return resolved?.session.id === input.route.sessionId ? resolved.rootID : undefined + } const matchRoute = (route: LayoutRoute) => { if (route.type === "home") return @@ -186,19 +215,10 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl return tabsStore.find((item) => item.type === "draft" && item.draftID === route.draftID) } if (route.type === "session") { - const main = tabsStore.find( - (item) => - item.type === "session" && item.server === route.server && item.sessionId === route.sessionId, + const sessionId = rootID() ?? route.sessionId + return tabsStore.find( + (item) => item.type === "session" && item.server === route.server && item.sessionId === sessionId, ) - if (main) return main - const s = session() - if (s?.parentID) { - const parentID = s.parentID - const parent = tabsStore.find( - (item) => item.type === "session" && item.server === route.server && item.sessionId === parentID, - ) - if (parent) return parent - } } } @@ -214,9 +234,8 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl } if (route.type === "session") { - const s = session() - if (!s) return - const sessionId = s.parentID ?? s.id + const sessionId = rootID() + if (!sessionId) return const next = { server: route.server, sessionId } tabsStoreActions.addSessionTab(next) }