diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 45c0b2625f..a8b91548c5 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -69,6 +69,12 @@ function locationKey(location: LocationRef) { return JSON.stringify([location.directory, location.workspaceID]) } +function matchesGlobalForm(form: FormInfo, id: string, location?: LocationRef) { + if (form.id !== id) return false + if (!form.location || !location) return true + return locationKey(form.location) === locationKey(location) +} + function locationQuery(ref?: LocationRef) { return ref ? { directory: ref.directory, workspace: ref.workspaceID } : undefined } @@ -589,7 +595,14 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ ) break case "form.created": - if (store.session.form[event.data.form.sessionID]?.some((form) => form.id === event.data.form.id)) break + if ( + store.session.form[event.data.form.sessionID]?.some((form) => + event.data.form.sessionID === "global" + ? matchesGlobalForm(form, event.data.form.id, event.location) + : form.id === event.data.form.id, + ) + ) + break setStore("session", "form", event.data.form.sessionID, [ ...(store.session.form[event.data.form.sessionID] ?? []), mutable({ ...event.data.form, location: event.location }), @@ -601,7 +614,11 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ "session", "form", event.data.sessionID, - (store.session.form[event.data.sessionID] ?? []).filter((form) => form.id !== event.data.id), + (store.session.form[event.data.sessionID] ?? []).filter((form) => + event.data.sessionID === "global" + ? !matchesGlobalForm(form, event.data.id, event.location) + : form.id !== event.data.id, + ), ) break case "shell.created": diff --git a/packages/tui/src/feature-plugins/system/notifications.ts b/packages/tui/src/feature-plugins/system/notifications.ts index a5033b62e0..ce39c916ee 100644 --- a/packages/tui/src/feature-plugins/system/notifications.ts +++ b/packages/tui/src/feature-plugins/system/notifications.ts @@ -26,6 +26,13 @@ function sessionErrorMessage(error: SessionError) { return "Session error" } +function formKey( + event: Extract, + id: string, +) { + return JSON.stringify([event.location?.directory, event.location?.workspaceID, id]) +} + const tui: TuiPlugin = async (api) => { const active = new Set() const errored = new Set() @@ -34,18 +41,18 @@ const tui: TuiPlugin = async (api) => { const permissions = new Set() api.event.on("form.created", (event) => { - if (event.data.form.sessionID === "global") return - if (forms.has(event.data.form.id)) return - forms.add(event.data.form.id) + const key = formKey(event, event.data.form.id) + if (forms.has(key)) return + forms.add(key) notify(api, event.data.form.sessionID, "Input needs response", "question") }) api.event.on("form.replied", (event) => { - forms.delete(event.data.id) + forms.delete(formKey(event, event.data.id)) }) api.event.on("form.cancelled", (event) => { - forms.delete(event.data.id) + forms.delete(formKey(event, event.data.id)) }) api.event.on("question.asked", (event) => { diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 683e7633d8..6a5919f5e1 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -187,6 +187,11 @@ export function Session() { ...(data.session.form.list("global", location()) ?? []), ] }) + const formKey = createMemo(() => { + const form = forms()[0] + if (!form) return + return JSON.stringify([form.location?.directory, form.location?.workspaceID, form.id]) + }) const [composer, setComposer] = createStore({ open: false, tab: undefined as string | undefined, @@ -261,6 +266,7 @@ export function Session() { return } await data.session.form.refresh("global", info.location) + if (route.sessionID !== sessionID) return project.workspace.set(info.location.workspaceID) editor.reconnect(info.location.directory) if (route.sessionID === sessionID && scroll) scroll.scrollBy(100_000) @@ -932,17 +938,17 @@ export function Session() { setComposer("open", false)} /> - {null} + {null} 0}> 0}> - + {(_) => { const form = forms()[0] return form ? : null diff --git a/packages/tui/test/cli/cmd/tui/notifications.test.ts b/packages/tui/test/cli/cmd/tui/notifications.test.ts index 0014292d23..c8e8668da6 100644 --- a/packages/tui/test/cli/cmd/tui/notifications.test.ts +++ b/packages/tui/test/cli/cmd/tui/notifications.test.ts @@ -155,6 +155,11 @@ const formNotification: TuiAttentionNotifyInput = { sound: { name: "question", when: "always" }, } +const globalFormNotification: TuiAttentionNotifyInput = { + ...formNotification, + title: undefined, +} + const permissionNotification: TuiAttentionNotifyInput = { title: "Demo session", message: "Permission needs input", @@ -173,12 +178,12 @@ describe("internal notifications TUI plugin", () => { expect(harness.notifications).toEqual([formNotification, 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") } }) - expect(harness.notifications).toEqual([]) + expect(harness.notifications).toEqual([globalFormNotification]) }) test("dedupes pending forms, questions, and permissions until they are resolved", async () => { diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 5024bcbedf..f79fcf9f9e 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -914,13 +914,23 @@ test("tracks global forms by location", async () => { expect(data.session.form.list("global", { directory }) ?? []).toEqual([]) events.emit({ - id: "evt_form_replied_global", + id: "evt_form_created_global_default", created: 1, + location: { directory }, + type: "form.created", + data: { form: { id: "frm_global", sessionID: "global", mode: "form", fields: [] } }, + }) + await wait(() => data.session.form.list("global", { directory })?.length === 1) + + events.emit({ + id: "evt_form_replied_global", + created: 2, location: other, type: "form.replied", data: { id: "frm_global", 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_global"]) } finally { app.renderer.destroy() }