Compare commits

..

1 Commits

Author SHA1 Message Date
Filip Hejmowski 011831ccbd adjust sessionid param desc 2026-08-20 13:08:24 +02:00
3 changed files with 28 additions and 109 deletions
@@ -8,13 +8,10 @@ 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 } })
@@ -29,23 +26,6 @@ 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>()
@@ -114,10 +94,8 @@ async function setup(page: Page, events?: () => OpenCodeEvent[]) {
connected: ["opencode"],
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
},
sessions: [session(parentID, parentTitle, 1700000000000), childSession(), grandchildSession()],
pageMessages: (sessionID) => ({
items: sessionID === parentID ? parentMessages() : sessionID === childID ? childMessages() : [],
}),
sessions: [session(parentID, parentTitle, 1700000000000), childSession()],
pageMessages: (sessionID) => ({ items: sessionID === parentID ? parentMessages() : [] }),
events,
eventRetry: events ? 16 : undefined,
})
@@ -167,10 +145,6 @@ 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"
@@ -208,43 +182,6 @@ 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(
+23 -42
View File
@@ -34,7 +34,6 @@ 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
@@ -171,43 +170,15 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
const tabs = useTabs()
const tabsStore = tabs.store
const tabsStoreActions = tabs
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] = 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 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
@@ -215,10 +186,19 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
return tabsStore.find((item) => item.type === "draft" && item.draftID === route.draftID)
}
if (route.type === "session") {
const sessionId = rootID() ?? route.sessionId
return tabsStore.find(
(item) => item.type === "session" && item.server === route.server && item.sessionId === sessionId,
const main = tabsStore.find(
(item) =>
item.type === "session" && item.server === route.server && item.sessionId === route.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
}
}
}
@@ -234,8 +214,9 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
}
if (route.type === "session") {
const sessionId = rootID()
if (!sessionId) return
const s = session()
if (!s) return
const sessionId = s.parentID ?? s.id
const next = { server: route.server, sessionId }
tabsStoreActions.addSessionTab(next)
}
+3 -2
View File
@@ -25,7 +25,7 @@ export const Input = Schema.Struct({
prompt: Schema.String.annotate({ description: "The task for the subagent to perform" }),
sessionID: Schema.optionalKey(SessionSchema.ID).annotate({
description:
"Continue a specific previous subagent conversation by passing its sessionID. Calls without a sessionID start a new conversation.",
"Optional. To continue a previous child, pass the sessionID returned by its earlier subagent call. To start a new child, omit this field. Never pass the current session ID or invent an ID.",
}),
background: Schema.optionalKey(Schema.Boolean).annotate({
description:
@@ -40,7 +40,8 @@ export const Output = Schema.Struct({
})
export const description = [
"Spawns an agent in a child session to work on the specified task.",
"The output includes a sessionID you can pass back later to continue that specific conversation with the subagent.",
"To start a new child, omit sessionID. To continue a previous child, pass the sessionID returned by its earlier subagent call.",
"Never pass the current session ID or invent an ID.",
"New child sessions start with fresh context, so include all relevant context and instructions when you don't pass a sessionID.",
"Foreground (default) runs the subagent to completion and returns its final response.",
"Background mode (background=true) launches it asynchronously and returns immediately; you are notified when it finishes.",