From b44ed835c63caafae619e303ca019a251c2698b2 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Thu, 23 Jul 2026 01:02:48 -0400 Subject: [PATCH] fix(web): sync provider banner dismissal --- apps/web/src/components/ChatView.tsx | 32 ++++++++++++---- .../chat/ProviderStatusBanner.test.tsx | 28 +++++++++++++- .../components/chat/ProviderStatusBanner.tsx | 37 ++++++++++--------- 3 files changed, 70 insertions(+), 27 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 87520c6f84faee7a57971f59fb789df21cf36874..957a14d6e8d08e4315139ff527969a1e211ff897 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -213,7 +213,11 @@ import { PanelLayoutControls, RightPanelMaximizeControl } from "./chat/PanelLayo import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview"; import { NoActiveThreadState } from "./NoActiveThreadState"; import { resolveEffectiveEnvMode, resolveLocalCheckoutBranchMismatch } from "./BranchToolbar.logic"; -import { ProviderStatusBanner } from "./chat/ProviderStatusBanner"; +import { + getProviderStatusBannerKey, + ProviderStatusBanner, + shouldShowProviderStatusBanner, +} from "./chat/ProviderStatusBanner"; import { ThreadErrorBanner } from "./chat/ThreadErrorBanner"; import { ComposerBannerStack, type ComposerBannerStackItem } from "./chat/ComposerBannerStack"; import { @@ -2320,11 +2324,22 @@ function ChatViewContent(props: ChatViewProps) { const defaultInstanceId = defaultInstanceIdForDriver(selectedProvider); return providerStatuses.find((status) => status.instanceId === defaultInstanceId) ?? null; }, [activeProviderInstanceId, providerStatuses, selectedProvider]); - const hasTimelineTopBanner = - Boolean(threadError) || - (activeProviderStatus !== null && - activeProviderStatus.status !== "ready" && - activeProviderStatus.status !== "disabled"); + const providerStatusBannerKey = getProviderStatusBannerKey(activeProviderStatus); + const [dismissedProviderStatusBannerKey, setDismissedProviderStatusBannerKey] = useState< + string | null + >(null); + useEffect(() => { + if (providerStatusBannerKey === null && dismissedProviderStatusBannerKey !== null) { + setDismissedProviderStatusBannerKey(null); + } + }, [dismissedProviderStatusBannerKey, providerStatusBannerKey]); + const visibleProviderStatus = shouldShowProviderStatusBanner( + activeProviderStatus, + dismissedProviderStatusBannerKey, + ) + ? activeProviderStatus + : null; + const hasTimelineTopBanner = Boolean(threadError) || visibleProviderStatus !== null; const activeProjectCwd = activeProject?.workspaceRoot ?? null; const activeThreadWorktreePath = activeThread?.worktreePath ?? null; const activeWorkspaceRoot = activeThreadWorktreePath ?? activeProjectCwd ?? undefined; @@ -5463,7 +5478,10 @@ function ChatViewContent(props: ChatViewProps) {
{/* Provider status overlays the timeline without changing its content height. */}
- + setDismissedProviderStatusBannerKey(providerStatusBannerKey)} + />
{/* Messages Wrapper */}
diff --git a/apps/web/src/components/chat/ProviderStatusBanner.test.tsx b/apps/web/src/components/chat/ProviderStatusBanner.test.tsx index 2bd217bfe42e19a942a894293418a62341b30d6f..2a5bfe9ed3a893d4f180e55f290ceda9d06a401e 100644 --- a/apps/web/src/components/chat/ProviderStatusBanner.test.tsx +++ b/apps/web/src/components/chat/ProviderStatusBanner.test.tsx @@ -2,7 +2,11 @@ import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3 import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vite-plus/test"; -import { ProviderStatusBanner } from "./ProviderStatusBanner"; +import { + getProviderStatusBannerKey, + ProviderStatusBanner, + shouldShowProviderStatusBanner, +} from "./ProviderStatusBanner"; function warningProvider(): ServerProvider { return { @@ -23,11 +27,31 @@ function warningProvider(): ServerProvider { } describe("ProviderStatusBanner", () => { + it("stays hidden after its current warning is dismissed", () => { + const status = warningProvider(); + + expect(shouldShowProviderStatusBanner(status, null)).toBe(true); + expect(shouldShowProviderStatusBanner(status, getProviderStatusBannerKey(status))).toBe(false); + }); + it("renders an accessible dismiss control for provider warnings", () => { - const markup = renderToStaticMarkup(); + const markup = renderToStaticMarkup( + {}} />, + ); expect(markup).toContain('role="alert"'); expect(markup).toContain('aria-label="Dismiss Codex provider warning"'); expect(markup).toContain("absolute top-2 right-2"); }); + + it("labels error dismiss controls with the correct severity", () => { + const markup = renderToStaticMarkup( + {}} + />, + ); + + expect(markup).toContain('aria-label="Dismiss Codex provider error"'); + }); }); diff --git a/apps/web/src/components/chat/ProviderStatusBanner.tsx b/apps/web/src/components/chat/ProviderStatusBanner.tsx index a900129aace7c6061a092debe4ad7886d22cca61..a64a94bc1c38bba4def7d3665fd9b1032f31a36c 100644 --- a/apps/web/src/components/chat/ProviderStatusBanner.tsx +++ b/apps/web/src/components/chat/ProviderStatusBanner.tsx @@ -1,33 +1,34 @@ import { type ServerProvider } from "@t3tools/contracts"; -import { memo, useEffect, useState } from "react"; +import { memo } from "react"; import { InfoIcon, XIcon } from "lucide-react"; import { cn } from "~/lib/utils"; import { formatProviderDriverKindLabel } from "../../providerModels"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +export function getProviderStatusBannerKey(status: ServerProvider | null): string | null { + return !status || status.status === "ready" || status.status === "disabled" + ? null + : [status.instanceId, status.status, status.auth.status, status.message ?? ""].join("\u0000"); +} + +export function shouldShowProviderStatusBanner( + status: ServerProvider | null, + dismissedBannerKey: string | null, +): boolean { + const bannerKey = getProviderStatusBannerKey(status); + return bannerKey !== null && bannerKey !== dismissedBannerKey; +} + export const ProviderStatusBanner = memo(function ProviderStatusBanner({ + onDismiss, status, }: { + onDismiss: () => void; status: ServerProvider | null; }) { - const bannerKey = - !status || status.status === "ready" || status.status === "disabled" - ? null - : [status.instanceId, status.status, status.auth.status, status.message ?? ""].join("\u0000"); - const [dismissedBannerKey, setDismissedBannerKey] = useState(null); - - useEffect(() => { - if (bannerKey === null && dismissedBannerKey !== null) { - setDismissedBannerKey(null); - } - }, [bannerKey, dismissedBannerKey]); - if (!status || status.status === "ready" || status.status === "disabled") { return null; } - if (bannerKey === dismissedBannerKey) { - return null; - } const providerName = status.displayName?.trim() || formatProviderDriverKindLabel(status.driver); const isUnauthenticated = status.status === "error" && status.auth.status === "unauthenticated"; @@ -66,9 +67,9 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({
-- 2.54.0