diff --git a/apps/web/src/components/chat/ErrorNotificationBar.test.tsx b/apps/web/src/components/chat/ErrorNotificationBar.test.tsx index b23bc183..77338234 100644 --- a/apps/web/src/components/chat/ErrorNotificationBar.test.tsx +++ b/apps/web/src/components/chat/ErrorNotificationBar.test.tsx @@ -85,4 +85,73 @@ describe("ErrorNotificationBar", () => { expect(markup).toContain("Worktree thread could not start"); expect(markup).toContain("Base branch 'main' does not resolve to a commit yet."); }); + + it("re-shows thread errors when the message changes after dismissal", async () => { + const onDismissThreadError = vi.fn(); + let renderer: ReactTestRenderer | null = null; + + await act(async () => { + renderer = create( + , + ); + }); + + const dismissAll = renderer!.root.findByProps({ "aria-label": "Dismiss notifications" }); + await act(async () => { + dismissAll.props.onClick(); + }); + + expect(renderer!.toJSON()).toBeNull(); + + await act(async () => { + renderer!.update( + , + ); + }); + + expect(renderer!.toJSON()).not.toBeNull(); + expect(renderer!.root.findByProps({ "aria-label": "Show 1 notification" })).toBeTruthy(); + }); + + it("does not hide non-dismissible provider notifications via dismiss all", async () => { + let renderer: ReactTestRenderer | null = null; + + await act(async () => { + renderer = create( + , + ); + }); + + const dismissAll = renderer!.root.findByProps({ "aria-label": "Dismiss notifications" }); + await act(async () => { + dismissAll.props.onClick(); + }); + + expect(renderer!.toJSON()).not.toBeNull(); + expect(renderer!.root.findByProps({ "aria-label": "Show 1 notification" })).toBeTruthy(); + expect(JSON.stringify(renderer!.toJSON())).toContain("OpenAI (Codex CLI) needs verification"); + }); }); diff --git a/apps/web/src/components/chat/ErrorNotificationBar.tsx b/apps/web/src/components/chat/ErrorNotificationBar.tsx index f8b9298d..b3bab3ad 100644 --- a/apps/web/src/components/chat/ErrorNotificationBar.tsx +++ b/apps/web/src/components/chat/ErrorNotificationBar.tsx @@ -54,6 +54,10 @@ interface NotificationItem { onDismiss?: () => void; } +function buildThreadErrorNotificationId(error: string): string { + return `thread-error:${error}`; +} + export const ErrorNotificationBar = memo(function ErrorNotificationBar({ threadError, showAuthFailuresAsErrors = true, @@ -130,7 +134,7 @@ export const ErrorNotificationBar = memo(function ErrorNotificationBar({ if (showAuthFailuresAsErrors || !isAuthenticationThreadError(threadError)) { const presentation = humanizeThreadError(threadError); items.push({ - id: "thread-error", + id: buildThreadErrorNotificationId(threadError), kind: "thread-error", icon: CircleAlertIcon, title: presentation.title ?? "Error", @@ -210,8 +214,8 @@ export const ErrorNotificationBar = memo(function ErrorNotificationBar({ notif.onDismiss(); } } - setDismissedIds(new Set(notifications.map((n) => n.id))); - }, [visibleNotifications, notifications]); + setDismissedIds(new Set(visibleNotifications.filter((n) => n.dismissible).map((n) => n.id))); + }, [visibleNotifications]); // Nothing to show if (visibleNotifications.length === 0) return null;