feat(app): add child agent session picker

This commit is contained in:
𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 2026-06-27 00:44:00 +00:00
commit 9a146b0d34
11 changed files with 444 additions and 3 deletions

View file

@ -0,0 +1,125 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/ChildSessionPicker"
const projectID = "proj_child_session_picker"
const rootID = "ses_root"
const newestRunningID = "ses_new_running"
const olderRunningID = "ses_old_running"
const newestIdleID = "ses_new_idle"
test.use({ viewport: { width: 1280, height: 800 }, reducedMotion: "reduce" })
test("opens from the hotkey and navigates running child agents first", async ({ page }) => {
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
vcs: "git",
name: "child-session-picker",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: {
all: [
{
id: "opencode",
name: "OpenCode",
models: {
"claude-opus-4-6": {
id: "claude-opus-4-6",
name: "Claude Opus 4.6",
limit: { context: 200_000 },
},
},
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
},
sessions: [
session(rootID, "Coordinate the release", 1),
session(olderRunningID, "Review accessibility (@review subagent)", 2, "review"),
session(newestRunningID, "Trace the session state (@explore subagent)", 4, "explore"),
session(newestIdleID, "Summarize the API (@docs subagent)", 5, "docs"),
],
statuses: {
[olderRunningID]: { type: "busy" },
[newestRunningID]: { type: "busy" },
[newestIdleID]: { type: "idle" },
},
pageMessages: () => ({ items: [] }),
})
await configurePage(page)
await page.goto(sessionHref(rootID))
await expectSessionTitle(page, "Coordinate the release")
const input = page.getByRole("textbox", { name: /Ask anything/ })
await expect(input).toBeVisible()
await page.keyboard.press("Control+Shift+ArrowDown")
await expect(page.getByText("Child agents", { exact: true })).toBeVisible()
await expect(input).toBeHidden()
const options = page.getByRole("option")
await expect(options).toHaveCount(3)
await expect(options.nth(0)).toContainText("@explore")
await expect(options.nth(1)).toContainText("@review")
await expect(options.nth(2)).toContainText("@docs")
await expect(options.nth(0)).toBeFocused()
await page.keyboard.press("Escape")
await expect(page.getByText("Child agents", { exact: true })).toBeHidden()
await expect(input).toBeFocused()
await page.keyboard.press("Control+Shift+ArrowDown")
await expect(options.nth(0)).toBeFocused()
await page.keyboard.press("ArrowDown")
await expect(options.nth(1)).toBeFocused()
await page.keyboard.press("Enter")
await expect(page).toHaveURL(sessionHref(olderRunningID))
await expectSessionTitle(page, "Review accessibility")
})
function session(id: string, title: string, updated: number, agent?: string) {
return {
id,
slug: id,
projectID,
directory,
parentID: id === rootID ? undefined : rootID,
title,
agent,
version: "dev",
time: { created: updated, updated },
}
}
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(
({ directory, dirBase64, server, sessionID }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: [{ worktree: directory, expanded: true }] },
lastProject: { local: directory },
}),
)
localStorage.setItem(
"opencode.global.dat:tabs",
JSON.stringify([{ type: "session", server, dirBase64, sessionId: sessionID }]),
)
},
{ directory, dirBase64: base64Encode(directory), server, sessionID: rootID },
)
}
function sessionHref(sessionID: string) {
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
return `/server/${base64Encode(server)}/session/${sessionID}`
}

View file

@ -1,7 +1,7 @@
import type { Page, Route } from "@playwright/test"
const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/vcs/status", "/vcs/diff"])
const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"])
const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp"])
export interface MockServerConfig {
provider: unknown
@ -17,6 +17,7 @@ export interface MockServerConfig {
todos?: (sessionID: string) => unknown[]
permissions?: unknown[] | (() => unknown[])
questions?: unknown[] | (() => unknown[])
statuses?: Record<string, unknown>
}
export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
@ -36,6 +37,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
"/agent": [{ name: "build", mode: "primary" }],
"/vcs": { branch: "main", default_branch: "main" },
"/session": config.sessions,
"/session/status": config.statuses ?? {},
}
await page.route("**/*", async (route) => {