From 30561879508327be767a84e9d7de9c27a896599c Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 3 Jul 2026 17:37:10 -0500 Subject: [PATCH] fix(tui): scope global forms by location --- packages/tui/src/context/data.tsx | 11 ++- packages/tui/src/routes/session/index.tsx | 8 +- packages/tui/test/cli/tui/data.test.tsx | 112 ++++++++++++++++++++++ packages/tui/test/fixture/tui-sdk.ts | 3 +- 4 files changed, 125 insertions(+), 9 deletions(-) diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 55d1503794..fab50ba169 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -714,12 +714,15 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ }, }, form: { - list(sessionID: string) { - return store.session.form[sessionID] + list(sessionID: string, ref?: LocationRef) { + const forms = store.session.form[sessionID] + if (sessionID !== "global" || !ref) return forms + const key = locationKey(ref) + return forms?.filter((form) => form.location && locationKey(form.location) === key) }, - async refresh(sessionID: string) { + async refresh(sessionID: string, ref?: LocationRef) { if (sessionID === "global") { - const result = await sdk.api.form.listRequests({ location: locationQuery(defaultLocation()) }) + const result = await sdk.api.form.listRequests({ location: locationQuery(ref ?? defaultLocation()) }) const location = { directory: result.location.directory, workspaceID: result.location.workspaceID } setStore( "session", diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 4286ce8f92..67109dd9fb 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -185,7 +185,7 @@ export function Session() { const sessionIDs = session()?.parentID ? [route.sessionID] : [route.sessionID, ...descendantSessionIDs()] return [ ...sessionIDs.flatMap((sessionID) => data.session.form.list(sessionID) ?? []), - ...(data.session.form.list("global") ?? []), + ...(data.session.form.list("global", location()) ?? []), ] }) const [composer, setComposer] = createStore({ @@ -266,7 +266,7 @@ export function Session() { await Promise.all([ data.session.permission.refresh(sessionID), data.session.form.refresh(sessionID), - data.session.form.refresh("global"), + data.session.form.refresh("global", info.location), ]) project.workspace.set(info.location.workspaceID) @@ -949,8 +949,8 @@ export function Session() { 0}> - - {(form) => } + + {() => {(form) => }} {null} diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 3d2a2a1d88..99e5dbce76 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -698,6 +698,118 @@ test("adds and dismisses permission requests from live events", async () => { } }) +test("tracks global forms by location", async () => { + const events = createEventStream() + const calls = createFetch(undefined, events) + const other = { directory: "/tmp/opencode-other", workspaceID: "wrk_other" } + let data!: ReturnType + + function Probe() { + data = useData() + return + } + + const app = await testRender(() => ( + + + + + + + + + + )) + + try { + await wait(() => data.connection.status() === "connected") + events.emit({ + id: "evt_form_created_1", + created: 0, + location: other, + type: "form.created", + data: { + form: { + id: "frm_1", + sessionID: "global", + mode: "form", + fields: [], + }, + }, + }) + + await wait(() => data.session.form.list("global", other)?.length === 1) + expect(data.session.form.list("global", { directory }) ?? []).toEqual([]) + + events.emit({ + id: "evt_form_replied_1", + created: 1, + location: other, + type: "form.replied", + data: { id: "frm_1", sessionID: "global", answer: {} }, + }) + await wait(() => data.session.form.list("global", other)?.length === 0) + } finally { + app.renderer.destroy() + } +}) + +test("refreshes global forms for the requested location", async () => { + const events = createEventStream() + const requests: URL[] = [] + const other = { directory: "/tmp/opencode-other", workspaceID: "wrk_other" } + const calls = createFetch((url) => { + if (url.pathname !== "/api/form/request") return + requests.push(url) + const requestedDirectory = url.searchParams.get("location[directory]") ?? directory + const requestedWorkspace = url.searchParams.get("location[workspace]") ?? undefined + return json({ + location: { + directory: requestedDirectory, + workspaceID: requestedWorkspace, + project: { id: "proj_test", directory: requestedDirectory }, + }, + data: + requestedDirectory === other.directory + ? [{ id: "frm_other", sessionID: "global", mode: "form", fields: [] }] + : [], + }) + }, events) + let data!: ReturnType + + function Probe() { + data = useData() + return + } + + const app = await testRender(() => ( + + + + + + + + + + )) + + try { + await wait(() => requests.length > 0) + requests.length = 0 + + await data.session.form.refresh("global", other) + + expect(requests).toHaveLength(1) + expect(requests[0]?.searchParams.get("location[directory]")).toBe(other.directory) + expect(requests[0]?.searchParams.get("location[workspace]")).toBe(other.workspaceID) + expect(data.session.form.list("global", other)?.map((form) => form.id)).toEqual(["frm_other"]) + expect(data.session.form.list("global", { directory }) ?? []).toEqual([]) + } finally { + app.renderer.destroy() + } +}) + test("settles pending tools when a live failure arrives", async () => { const events = createEventStream() const calls = createFetch(undefined, events) diff --git a/packages/tui/test/fixture/tui-sdk.ts b/packages/tui/test/fixture/tui-sdk.ts index b8fc93c428..d4613ae358 100644 --- a/packages/tui/test/fixture/tui-sdk.ts +++ b/packages/tui/test/fixture/tui-sdk.ts @@ -100,7 +100,8 @@ export function createFetch(override?: FetchHandler, events?: ReturnType