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;