Compare commits

...

1 Commits

Author SHA1 Message Date
Hona 4d9603bd8a fix(app): keep root tab active for subagents 2026-08-20 10:27:54 +00:00
2 changed files with 107 additions and 25 deletions
@@ -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<void>()
@@ -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(
+42 -23
View File
@@ -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)
}