feat(core): global form support (#35959)

This commit is contained in:
Aiden Cline 2026-07-08 17:25:34 -05:00 committed by GitHub
commit b44a981eef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 493 additions and 93 deletions

View file

@ -77,6 +77,7 @@ function form(id: string, sessionID = "session"): Extract<V2Event, { type: "form
return {
id,
sessionID,
title: "Input requested",
mode: "form",
fields: [],
}
@ -138,12 +139,22 @@ const questionNotification: TuiAttentionNotifyInput = {
}
const formNotification: TuiAttentionNotifyInput = {
title: "Demo session",
title: "Input requested",
message: "Input needs response",
notification: { when: "blurred" },
sound: { name: "question", when: "always" },
}
const titledFormNotification: TuiAttentionNotifyInput = {
...formNotification,
title: "Confirm deployment",
}
const globalFormNotification: TuiAttentionNotifyInput = {
...formNotification,
title: "demo-mcp is requesting input",
}
const permissionNotification: TuiAttentionNotifyInput = {
title: "Demo session",
message: "Permission needs input",
@ -155,19 +166,29 @@ describe("internal notifications TUI plugin", () => {
test("notifies for form, question, and permission requests with blurred notifications and always-on sounds", async () => {
const harness = await setup()
harness.emit({ id: "event-1", created: 0, type: "form.created", data: { form: form("form-1") } })
harness.emit({
id: "event-1",
created: 0,
type: "form.created",
data: { form: { ...form("form-1"), title: "Confirm deployment" } },
})
harness.emit({ id: "event-2", created: 0, type: "question.asked", data: question("question-1") })
harness.emit({ id: "event-3", created: 0, type: "permission.asked", data: permission("permission-1") })
expect(harness.notifications).toEqual([formNotification, questionNotification, permissionNotification])
expect(harness.notifications).toEqual([titledFormNotification, questionNotification, permissionNotification])
})
test("ignores global forms until the TUI can render them", async () => {
test("notifies for global forms once the TUI can render them", async () => {
const harness = await setup()
harness.emit({ id: "event-1", created: 0, type: "form.created", data: { form: form("form-1", "global") } })
harness.emit({
id: "event-1",
created: 0,
type: "form.created",
data: { form: { ...form("form-1", "global"), title: "demo-mcp is requesting input" } },
})
expect(harness.notifications).toEqual([])
expect(harness.notifications).toEqual([globalFormNotification])
})
test("dedupes pending forms, questions, and permissions until they are resolved", async () => {
@ -243,14 +264,14 @@ describe("internal notifications TUI plugin", () => {
id: "event-1",
created: 0,
type: "form.created",
data: { form: form("form-1", "subagent") },
data: { form: { ...form("form-1", "subagent"), title: "Questions" } },
})
harness.emit(executionStarted("event-2", "subagent"))
harness.emit(executionSucceeded("event-3", "subagent"))
expect(harness.notifications).toEqual([
{
title: "Subagent session",
title: "Questions",
message: "Input needs response",
notification: false,
sound: { name: "question", when: "always" },

View file

@ -1485,7 +1485,9 @@ test("adds, dismisses, and refreshes form requests", async () => {
const events = createEventStream()
const calls = createFetch((url) => {
if (url.pathname !== "/api/session/ses_1/form") return
return json({ data: [{ id: "frm_remote", sessionID: "ses_1", mode: "form", fields: [] }] })
return json({
data: [{ id: "frm_remote", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] }],
})
}, events)
let data!: ReturnType<typeof useData>
let sdk!: ReturnType<typeof useSDK>
@ -1514,13 +1516,13 @@ test("adds, dismisses, and refreshes form requests", async () => {
id: "evt_form_created_1",
created: 0,
type: "form.created",
data: { form: { id: "frm_1", sessionID: "ses_1", mode: "form", fields: [] } },
data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] } },
})
emitEvent(events, {
id: "evt_form_created_duplicate",
created: 1,
type: "form.created",
data: { form: { id: "frm_1", sessionID: "ses_1", mode: "form", fields: [] } },
data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] } },
})
await wait(() => data.session.form.list("ses_1")?.length === 1)
@ -1536,7 +1538,7 @@ test("adds, dismisses, and refreshes form requests", async () => {
id: "evt_form_created_2",
created: 3,
type: "form.created",
data: { form: { id: "frm_2", sessionID: "ses_1", mode: "form", fields: [] } },
data: { form: { id: "frm_2", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] } },
})
emitEvent(events, {
id: "evt_form_cancelled_2",
@ -1553,11 +1555,238 @@ test("adds, dismisses, and refreshes form requests", 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>
let sdk!: ReturnType<typeof useSDK>
function Probe() {
data = useData()
sdk = useSDK()
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(() => sdk.connection.status() === "connected")
events.emit({
id: "evt_form_created_global_other",
created: 0,
location: other,
type: "form.created",
data: {
form: { id: "frm_other", sessionID: "global", title: "Input requested", 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_created_global_default",
created: 1,
location: { directory },
type: "form.created",
data: {
form: { id: "frm_default", sessionID: "global", title: "Input requested", mode: "form", fields: [] },
},
})
await wait(() => data.session.form.list("global", { directory })?.length === 1)
events.emit({
id: "evt_form_replied_global_other",
created: 2,
location: other,
type: "form.replied",
data: { id: "frm_other", sessionID: "global", answer: {} },
})
await wait(() => data.session.form.list("global", other)?.length === 0)
expect(data.session.form.list("global", { directory })?.map((form) => form.id)).toEqual(["frm_default"])
} 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: [
{
id: requestedDirectory === other.directory ? "frm_other" : "frm_default",
sessionID: "global",
title: "Input requested",
mode: "form",
fields: [],
},
],
})
}, events)
let data!: ReturnType<typeof useData>
let sdk!: ReturnType<typeof useSDK>
function Probe() {
data = useData()
sdk = useSDK()
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(() => sdk.connection.status() === "connected" && requests.length > 0)
requests.length = 0
await data.session.form.refresh("global", { directory })
await data.session.form.refresh("global", other)
expect(requests).toHaveLength(2)
expect(requests[1]?.searchParams.get("location[directory]")).toBe(other.directory)
expect(requests[1]?.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 })?.map((form) => form.id)).toEqual(["frm_default"])
} finally {
app.renderer.destroy()
}
})
test("refreshes global forms once per loaded location after reconnect", async () => {
const events = createEventStream()
const requests: URL[] = []
const counts = new Map<string, number>()
const home = { directory: process.cwd() }
const other = { directory: "/tmp/opencode-other", workspaceID: "wrk_other" }
const calls = createFetch((url) => {
if (url.pathname === "/api/location")
return json({ ...home, project: { id: "proj_test", directory: home.directory } })
if (url.pathname === "/api/session")
return json({
data: [
{ id: "ses_default", title: "Default", location: home, time: { created: 0, updated: 0 } },
{ id: "ses_other_1", title: "Other one", location: other, time: { created: 0, updated: 0 } },
{ id: "ses_other_2", title: "Other two", location: other, time: { created: 0, updated: 0 } },
],
cursor: {},
})
if (url.pathname !== "/api/form/request") return
requests.push(url)
const requestedDirectory = url.searchParams.get("location[directory]") ?? home.directory
const requestedWorkspace = url.searchParams.get("location[workspace]") ?? undefined
const count = (counts.get(requestedDirectory) ?? 0) + 1
counts.set(requestedDirectory, count)
return json({
location: {
directory: requestedDirectory,
workspaceID: requestedWorkspace,
project: { id: "proj_test", directory: requestedDirectory },
},
data: [
{
id: `frm_${requestedDirectory === other.directory ? "other" : "default"}_${count}`,
sessionID: "global",
title: "Input requested",
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(
() =>
data.session.form.list("global", home)?.[0]?.id === "frm_default_1" &&
data.session.form.list("global", other)?.[0]?.id === "frm_other_1",
)
expect(requests).toHaveLength(2)
requests.length = 0
events.disconnect()
await wait(
() =>
data.session.form.list("global", home)?.[0]?.id === "frm_default_2" &&
data.session.form.list("global", other)?.[0]?.id === "frm_other_2",
4000,
)
expect(requests).toHaveLength(2)
expect(
requests.map((url) => [
url.searchParams.get("location[directory]") ?? directory,
url.searchParams.get("location[workspace]") ?? undefined,
]),
).toEqual([
[home.directory, undefined],
[other.directory, other.workspaceID],
])
} finally {
app.renderer.destroy()
}
})
test("reconciles all pending form requests when the event stream reconnects", async () => {
const events = createEventStream()
let requests = [
{ id: "frm_old", sessionID: "ses_old", mode: "form" as const, fields: [] },
{ id: "frm_keep", sessionID: "ses_keep", mode: "url" as const, url: "https://example.com" },
{ id: "frm_old", sessionID: "ses_old", title: "Input requested", mode: "form" as const, fields: [] },
{
id: "frm_keep",
sessionID: "ses_keep",
title: "Input requested",
mode: "url" as const,
url: "https://example.com",
},
]
let calls = 0
const fetch = createFetch((url) => {
@ -1588,7 +1817,9 @@ test("reconciles all pending form requests when the event stream reconnects", as
await wait(() => data.session.form.list("ses_old")?.[0]?.id === "frm_old")
expect(data.session.form.list("ses_keep")?.[0]?.id).toBe("frm_keep")
requests = [{ id: "frm_new", sessionID: "ses_new", mode: "form" as const, fields: [] }]
requests = [
{ id: "frm_new", sessionID: "ses_new", title: "Input requested", mode: "form" as const, fields: [] },
]
events.disconnect()
await wait(() => calls === 2 && data.session.form.list("ses_new")?.[0]?.id === "frm_new")