feat(tui): render session forms (#35421)

This commit is contained in:
Aiden Cline 2026-07-05 19:35:30 -05:00 committed by GitHub
commit 5bcf8d5a0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1202 additions and 18 deletions

View file

@ -72,6 +72,15 @@ function question(id: string, sessionID = "session"): QuestionRequest {
}
}
function form(id: string, sessionID = "session"): Extract<V2Event, { type: "form.created" }>["data"]["form"] {
return {
id,
sessionID,
mode: "form",
fields: [],
}
}
function permission(id: string, sessionID = "session"): PermissionRequest {
return {
id,
@ -139,6 +148,13 @@ const questionNotification: TuiAttentionNotifyInput = {
sound: { name: "question", when: "always" },
}
const formNotification: TuiAttentionNotifyInput = {
title: "Demo session",
message: "Input needs response",
notification: { when: "blurred" },
sound: { name: "question", when: "always" },
}
const permissionNotification: TuiAttentionNotifyInput = {
title: "Demo session",
message: "Permission needs input",
@ -147,39 +163,60 @@ const permissionNotification: TuiAttentionNotifyInput = {
}
describe("internal notifications TUI plugin", () => {
test("notifies for question and permission requests with blurred notifications and always-on sounds", async () => {
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: "question.asked", data: question("question-1") })
harness.emit({ id: "event-2", created: 0, type: "permission.asked", data: permission("permission-1") })
harness.emit({ id: "event-1", created: 0, type: "form.created", data: { form: form("form-1") } })
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([questionNotification, permissionNotification])
expect(harness.notifications).toEqual([formNotification, questionNotification, permissionNotification])
})
test("dedupes pending questions and permissions until they are resolved", async () => {
test("ignores global forms until the TUI can render them", async () => {
const harness = await setup()
harness.emit({ id: "event-1", created: 0, type: "question.asked", data: question("question-1") })
harness.emit({ id: "event-2", created: 0, type: "question.asked", data: question("question-1") })
harness.emit({ id: "event-1", created: 0, type: "form.created", data: { form: form("form-1", "global") } })
expect(harness.notifications).toEqual([])
})
test("dedupes pending forms, questions, and permissions until they are resolved", async () => {
const harness = await setup()
harness.emit({ id: "event-1", created: 0, type: "form.created", data: { form: form("form-1") } })
harness.emit({ id: "event-2", created: 0, type: "form.created", data: { form: form("form-1") } })
harness.emit({
id: "event-3",
created: 0,
type: "form.cancelled",
data: { sessionID: "session", id: "form-1" },
})
harness.emit({ id: "event-4", created: 0, type: "form.created", data: { form: form("form-1") } })
harness.emit({ id: "event-5", created: 0, type: "question.asked", data: question("question-1") })
harness.emit({ id: "event-6", created: 0, type: "question.asked", data: question("question-1") })
harness.emit({
id: "event-7",
created: 0,
type: "question.replied",
data: { sessionID: "session", requestID: "question-1", answers: [] },
})
harness.emit({ id: "event-4", created: 0, type: "question.asked", data: question("question-1") })
harness.emit({ id: "event-8", created: 0, type: "question.asked", data: question("question-1") })
harness.emit({ id: "event-5", created: 0, type: "permission.asked", data: permission("permission-1") })
harness.emit({ id: "event-6", created: 0, type: "permission.asked", data: permission("permission-1") })
harness.emit({ id: "event-9", created: 0, type: "permission.asked", data: permission("permission-1") })
harness.emit({ id: "event-10", created: 0, type: "permission.asked", data: permission("permission-1") })
harness.emit({
id: "event-7",
id: "event-11",
created: 0,
type: "permission.replied",
data: { sessionID: "session", requestID: "permission-1", reply: "once" },
})
harness.emit({ id: "event-8", created: 0, type: "permission.asked", data: permission("permission-1") })
harness.emit({ id: "event-12", created: 0, type: "permission.asked", data: permission("permission-1") })
expect(harness.notifications).toEqual([
formNotification,
formNotification,
questionNotification,
questionNotification,
permissionNotification,
@ -207,14 +244,19 @@ describe("internal notifications TUI plugin", () => {
test("uses sound-only notifications and subagent_done sound for subagent sessions", async () => {
const harness = await setup()
harness.emit({ id: "event-1", created: 0, type: "question.asked", data: question("question-1", "subagent") })
harness.emit({
id: "event-1",
created: 0,
type: "form.created",
data: { form: form("form-1", "subagent") },
})
harness.emit(stepStarted("event-2", "subagent"))
harness.emit(stepEnded("event-3", "subagent"))
expect(harness.notifications).toEqual([
{
title: "Subagent session",
message: "Question needs input",
message: "Input needs response",
notification: false,
sound: { name: "question", when: "always" },
},

View file

@ -807,6 +807,76 @@ test("adds and dismisses permission requests from live events", async () => {
}
})
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: [] }] })
}, 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.connection.status() === "connected")
emitEvent(events, {
id: "evt_form_created_1",
created: 0,
type: "form.created",
data: { form: { id: "frm_1", sessionID: "ses_1", 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: [] } },
})
await wait(() => data.session.form.list("ses_1")?.length === 1)
emitEvent(events, {
id: "evt_form_replied_1",
created: 2,
type: "form.replied",
data: { sessionID: "ses_1", id: "frm_1", answer: {} },
})
await wait(() => data.session.form.list("ses_1")?.length === 0)
emitEvent(events, {
id: "evt_form_created_2",
created: 3,
type: "form.created",
data: { form: { id: "frm_2", sessionID: "ses_1", mode: "form", fields: [] } },
})
emitEvent(events, {
id: "evt_form_cancelled_2",
created: 4,
type: "form.cancelled",
data: { sessionID: "ses_1", id: "frm_2" },
})
await wait(() => data.session.form.list("ses_1")?.length === 0)
await data.session.form.refresh("ses_1")
expect(data.session.form.list("ses_1")?.map((form) => form.id)).toEqual(["frm_remote"])
} finally {
app.renderer.destroy()
}
})
test("adds and dismisses question requests from live events", async () => {
const events = createEventStream()
const calls = createFetch(undefined, events)