fix(tui): scope global forms by location

This commit is contained in:
Aiden Cline 2026-07-03 17:37:10 -05:00
commit 3056187950
4 changed files with 125 additions and 9 deletions

View file

@ -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",

View file

@ -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() {
<PermissionPrompt request={permissions()[0]} directory={session()?.location.directory} />
</Match>
<Match when={forms().length > 0}>
<Show when={forms()[0]} keyed>
{(form) => <FormPrompt form={form} />}
<Show when={forms()[0]?.id} keyed>
{() => <Show when={forms()[0]}>{(form) => <FormPrompt form={form()} />}</Show>}
</Show>
</Match>
<Match when={composer.open || !!session()?.parentID}>{null}</Match>

View file

@ -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<typeof useData>
function Probe() {
data = useData()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
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<typeof useData>
function Probe() {
data = useData()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
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)

View file

@ -100,7 +100,8 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
if (url.pathname === "/api/form/request") return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
if (url.pathname === "/api/session") return json({ data: [], cursor: {} })
if (url.pathname === "/api/session/active") return json({ data: {}, watermarks: {} })
if (/^\/api\/session\/[^/]+\/(permission|form)$/.test(url.pathname)) return json([])
if (/^\/api\/session\/[^/]+\/permission$/.test(url.pathname)) return json([])
if (/^\/api\/session\/[^/]+\/form$/.test(url.pathname)) return json({ data: [] })
if (
["/api/agent", "/api/model", "/api/provider", "/api/integration", "/api/command", "/api/skill"].includes(
url.pathname,