mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
feat(tui): read Session families in one request
With the data layer's family read carrying every member's status, permissions, and forms, the TUI's own per-member reads are redundant. Started alongside the family read, they still went out as separate requests. The session route now awaits only the family read and no longer syncs pending requests for each descendant. Tab prefetch drops its permission and form reads. The open dialog reads one family per distinct root of running Sessions and of Sessions with pending requests, instead of two pending-request reads per running Session and a single read per root. Opening a root with subagents now costs /session/active once per connection and one family read.
This commit is contained in:
@@ -176,22 +176,14 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
|
||||
|
||||
createEffect(() => {
|
||||
const knownRoots = new Set(props.sessions.map((session) => session.id))
|
||||
const active = data.session.active()
|
||||
active.forEach((id) => {
|
||||
void data.session.permission.sync(id).catch(() => undefined)
|
||||
void data.session.form.sync(id).catch(() => undefined)
|
||||
})
|
||||
new Set(
|
||||
[
|
||||
...data.session.permission.sessions(),
|
||||
...data.session.form.sessions(),
|
||||
...(recent() === true ? active : []),
|
||||
].map((id) => data.session.root(id)),
|
||||
).forEach((rootID) => {
|
||||
if (!knownRoots.has(rootID) && !data.session.get(rootID)) {
|
||||
void data.session.sync(rootID).catch(() => undefined)
|
||||
}
|
||||
})
|
||||
// A family read loads the root and every member's pending requests. Any member's ID reads the same family, so an
|
||||
// unloaded Session resolves to itself.
|
||||
new Set([
|
||||
...data.session.active().map((id) => data.session.root(id)),
|
||||
...[...data.session.permission.sessions(), ...data.session.form.sessions()]
|
||||
.map((id) => data.session.root(id))
|
||||
.filter((rootID) => !knownRoots.has(rootID) && !data.session.get(rootID)),
|
||||
]).forEach((rootID) => void data.session.sync(rootID, { children: true }).catch(() => undefined))
|
||||
})
|
||||
|
||||
const options = createMemo(() => {
|
||||
|
||||
@@ -310,12 +310,7 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
||||
.filter((sessionID) => sessionID !== current())
|
||||
for (const sessionID of sessions) {
|
||||
if (stale) return
|
||||
await Promise.allSettled([
|
||||
data.session.message.sync(sessionID),
|
||||
data.session.pending.sync(sessionID),
|
||||
data.session.permission.sync(sessionID),
|
||||
data.session.form.sync(sessionID),
|
||||
])
|
||||
await Promise.allSettled([data.session.message.sync(sessionID), data.session.pending.sync(sessionID)])
|
||||
}
|
||||
}, TAB_PREFETCH_DELAY)
|
||||
onCleanup(() => {
|
||||
|
||||
@@ -325,15 +325,6 @@ export function Session(props: {
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on([descendantSessionIDs, () => client.connection.status()], ([sessionIDs, status]) => {
|
||||
if (status !== "connected") return
|
||||
void Promise.allSettled(
|
||||
sessionIDs.flatMap((sessionID) => [data.session.permission.sync(sessionID), data.session.form.sync(sessionID)]),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
if (client.connection.status() !== "connected") return
|
||||
setSynced(false)
|
||||
@@ -342,11 +333,8 @@ export function Session(props: {
|
||||
let superseded = false
|
||||
onCleanup(() => (superseded = true))
|
||||
void (async () => {
|
||||
await Promise.all([
|
||||
data.session.sync(sessionID, { children: true }),
|
||||
data.session.permission.sync(sessionID).catch(() => undefined),
|
||||
data.session.form.sync(sessionID).catch(() => undefined),
|
||||
])
|
||||
// The family read also loads every member's status, permissions and forms.
|
||||
await data.session.sync(sessionID, { children: true })
|
||||
if (superseded) return
|
||||
const info = data.session.get(sessionID)
|
||||
if (!info) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CliRenderEvents, TextAttributes } from "@opentui/core"
|
||||
import path from "path"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { createSignal, onMount } from "solid-js"
|
||||
import type { SessionInfo } from "@opencode/client"
|
||||
import type { OpenCodeEvent, SessionInfo } from "@opencode/client"
|
||||
import { DialogOpen, DialogOpenKey } from "../../../src/component/dialog-open"
|
||||
import { ConfigProvider } from "../../../src/config"
|
||||
import { ClientProvider } from "../../../src/context/client"
|
||||
@@ -854,28 +854,56 @@ test("option arrows stay in the only visible section", async () => {
|
||||
|
||||
test("surfaces sessions awaiting permissions or questions at the top with attention icons", async () => {
|
||||
const remote = { directory: "/tmp/opencode/remote" }
|
||||
const roots = [
|
||||
...Array.from({ length: 9 }, (_, index) => ({
|
||||
...recentSession,
|
||||
id: `ses_recent_${index + 1}`,
|
||||
title: `Recent session ${index + 1}`,
|
||||
time: { created: 1, updated: 100 - index },
|
||||
})),
|
||||
{
|
||||
...recentSession,
|
||||
id: "ses_parent_question",
|
||||
title: "Parent awaiting question",
|
||||
location: remote,
|
||||
time: { created: 1, updated: 5 },
|
||||
},
|
||||
{
|
||||
...recentSession,
|
||||
id: "ses_blocked_permission",
|
||||
title: "Old session awaiting permission",
|
||||
time: { created: 1, updated: 1 },
|
||||
},
|
||||
].map((session) =>
|
||||
["ses_recent_1", "ses_blocked_permission"].includes(session.id)
|
||||
? { ...session, status: "running" as const }
|
||||
: session,
|
||||
)
|
||||
const child = {
|
||||
...recentSession,
|
||||
id: "ses_child_question",
|
||||
parentID: "ses_parent_question",
|
||||
title: "Child subagent",
|
||||
location: remote,
|
||||
time: { created: 1, updated: 2 },
|
||||
}
|
||||
const question: Extract<OpenCodeEvent, { type: "form.created" }>["data"]["form"] = {
|
||||
id: "frm_question",
|
||||
sessionID: child.id,
|
||||
title: "Questions",
|
||||
fields: [{ key: "q0", type: "string", title: "Target" }],
|
||||
}
|
||||
const fixture = await renderOpen((url) => {
|
||||
if (url.pathname === "/api/session/active") {
|
||||
return json({
|
||||
data: {
|
||||
ses_recent_1: { type: "running" },
|
||||
ses_blocked_permission: { type: "running" },
|
||||
ses_child_question: { type: "running" },
|
||||
},
|
||||
})
|
||||
}
|
||||
if (url.pathname === "/api/session/ses_child_question") {
|
||||
return json({
|
||||
data: {
|
||||
...recentSession,
|
||||
id: "ses_child_question",
|
||||
parentID: "ses_parent_question",
|
||||
title: "Child subagent",
|
||||
location: remote,
|
||||
time: { created: 1, updated: 2 },
|
||||
},
|
||||
})
|
||||
}
|
||||
const info = [...roots, child].find((session) => url.pathname === `/api/session/${session.id}`)
|
||||
if (info) return json({ data: info })
|
||||
if (url.pathname === "/api/session/ses_blocked_permission/permission") {
|
||||
return json({
|
||||
data: [
|
||||
@@ -888,45 +916,16 @@ test("surfaces sessions awaiting permissions or questions at the top with attent
|
||||
],
|
||||
})
|
||||
}
|
||||
if (url.pathname === "/api/session/ses_child_question/form") {
|
||||
return json({
|
||||
data: [
|
||||
{
|
||||
id: "frm_question",
|
||||
sessionID: "ses_child_question",
|
||||
title: "Questions",
|
||||
fields: [{ key: "q0", type: "string", title: "Target" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
if (url.pathname === "/api/session/ses_child_question/form") return json({ data: [question] })
|
||||
if (url.pathname !== "/api/session") return undefined
|
||||
return json({
|
||||
data: [
|
||||
...Array.from({ length: 9 }, (_, index) => ({
|
||||
...recentSession,
|
||||
id: `ses_recent_${index + 1}`,
|
||||
title: `Recent session ${index + 1}`,
|
||||
time: { created: 1, updated: 100 - index },
|
||||
})),
|
||||
{
|
||||
...recentSession,
|
||||
id: "ses_parent_question",
|
||||
title: "Parent awaiting question",
|
||||
location: remote,
|
||||
time: { created: 1, updated: 5 },
|
||||
},
|
||||
{
|
||||
...recentSession,
|
||||
id: "ses_blocked_permission",
|
||||
title: "Old session awaiting permission",
|
||||
time: { created: 1, updated: 1 },
|
||||
},
|
||||
],
|
||||
cursor: {},
|
||||
})
|
||||
const parentID = url.searchParams.get("parentID")
|
||||
if (parentID && parentID !== "null") return json({ data: parentID === child.parentID ? [child] : [], cursor: {} })
|
||||
return json({ data: roots, cursor: {} })
|
||||
})
|
||||
|
||||
// The idle child is unloaded and outside reconnect hydration, so only the dialog can read its family.
|
||||
fixture.emit({ id: "evt_question", created: 2, type: "form.created", location: remote, data: { form: question } })
|
||||
|
||||
try {
|
||||
const frame = await fixture.app.waitForFrame(
|
||||
(value) =>
|
||||
|
||||
@@ -165,7 +165,16 @@ test.each([40, 120])("subagent completion notices navigate to the child session
|
||||
if (url.pathname.endsWith("/inbox") || url.pathname.endsWith("/permission")) return json({ data: [] })
|
||||
return undefined
|
||||
}, createEventStream())
|
||||
const server = Bun.serve({ port: 0, idleTimeout: 0, fetch: (request) => calls.fetch(request) })
|
||||
// The fixture synthesizes the family from in-process reads, so only requests through the server reach this log.
|
||||
const requests: string[] = []
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
idleTimeout: 0,
|
||||
fetch: (request) => {
|
||||
requests.push(new URL(request.url).pathname)
|
||||
return calls.fetch(request)
|
||||
},
|
||||
})
|
||||
const { run } = await import("../src/app")
|
||||
const task = Effect.runPromise(
|
||||
run({
|
||||
@@ -189,6 +198,12 @@ test.each([40, 120])("subagent completion notices navigate to the child session
|
||||
const x = lines[y].indexOf("General finished")
|
||||
await setup.mockMouse.click(x + 1, y)
|
||||
await setup.waitForFrame((frame) => frame.includes("Investigate authentication"))
|
||||
// One family read loads both members, their pending requests, and serves the child's route.
|
||||
expect(
|
||||
requests
|
||||
.filter((pathname) => /^\/api\/session\/(active|[^/]+\/(family|permission|form))$/.test(pathname))
|
||||
.toSorted(),
|
||||
).toEqual(["/api/session/active", `/api/session/${parent.id}/family`])
|
||||
} finally {
|
||||
setup.renderer.destroy()
|
||||
await task
|
||||
|
||||
Reference in New Issue
Block a user