authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-09 17:52:16-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-09 17:52:16-07:00
log238715fd6fb1e5106db1e310eed7c389304b4840
tree00ce625f75cd3108319fd4bd440d2e8cef89886a
parentb03bc4b52d184c5f2367d702e043586a0e9fcdaa
signature Signed by PGP key B5690EEEBB952194

Polish dialog/alert surfaces and unauthenticated provider banner (#3016)

Co-authored-by: ss <ss@barekey.dev> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

8 files changed, 122 insertions(+), 64 deletions(-)

apps/web/src/components/ChatView.tsx+4
......@@ -1843,7 +1843,11 @@ export default function ChatView(props: ChatViewProps) {
18431843 // `codex_personal`) surfaces its own status/message in the banner rather
18441844 // than the default Codex's. Falls back to first-match-by-kind when no
18451845 // saved instance id is available or the instance no longer exists.
1846 const selectedProviderInstanceId =
1847 providerStatuses.find((status) => status.instanceId === selectedProviderByThreadId)
1848 ?.instanceId ?? null;
18461849 const activeProviderInstanceId =
1850 selectedProviderInstanceId ??
18471851 activeThread?.session?.providerInstanceId ??
18481852 activeThread?.modelSelection.instanceId ??
18491853 activeProject?.defaultModelSelection?.instanceId ??
apps/web/src/components/chat/ProviderStatusBanner.tsx+31-16
......@@ -1,7 +1,7 @@
11import { type ServerProvider } from "@t3tools/contracts";
22import { memo } from "react";
3import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
4import { CircleAlertIcon } from "lucide-react";
3import { InfoIcon } from "lucide-react";
4import { cn } from "~/lib/utils";
55import { formatProviderDriverKindLabel } from "../../providerModels";
66
77export const ProviderStatusBanner = memo(function ProviderStatusBanner({
......@@ -13,22 +13,37 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({
1313 return null;
1414 }
1515
16 const providerLabel = status.displayName?.trim() || formatProviderDriverKindLabel(status.driver);
17 const defaultMessage =
18 status.status === "error"
19 ? `${providerLabel} provider is unavailable.`
20 : `${providerLabel} provider has limited availability.`;
21 const title = `${providerLabel} provider status`;
16 const providerName = status.displayName?.trim() || formatProviderDriverKindLabel(status.driver);
17 const isUnauthenticated = status.status === "error" && status.auth.status === "unauthenticated";
18 const title = isUnauthenticated
19 ? `${providerName} is unauthenticated`
20 : `${providerName} provider status`;
21 const message = isUnauthenticated
22 ? "Sign in via the CLI to authenticate again."
23 : (status.message ??
24 (status.status === "error"
25 ? `${providerName} provider is unavailable.`
26 : `${providerName} provider has limited availability.`));
2227
2328 return (
24 <div className="pt-3 mx-auto max-w-3xl">
25 <Alert variant={status.status === "error" ? "error" : "warning"}>
26 <CircleAlertIcon />
27 <AlertTitle>{title}</AlertTitle>
28 <AlertDescription className="line-clamp-3" title={status.message ?? defaultMessage}>
29 {status.message ?? defaultMessage}
30 </AlertDescription>
31 </Alert>
29 <div className="mx-auto w-fit max-w-[calc(100%-2rem)] pt-3">
30 <div
31 className={cn(
32 "inline-flex items-center gap-3 rounded-xl border px-3.5 py-3 text-card-foreground text-sm",
33 status.status === "warning"
34 ? "border-warning/32 bg-warning/4 [&_svg]:text-warning"
35 : "border-destructive/32 bg-destructive/4 text-destructive-foreground [&_svg]:text-destructive",
36 )}
37 role="alert"
38 >
39 <InfoIcon className="size-4 shrink-0" aria-hidden />
40 <div className="flex min-w-0 flex-col gap-1">
41 <div className="font-medium">{title}</div>
42 <div className="line-clamp-3 text-muted-foreground" title={message}>
43 {message}
44 </div>
45 </div>
46 </div>
3247 </div>
3348 );
3449});
apps/web/src/components/chat/ThreadErrorBanner.tsx+4-8
......@@ -1,5 +1,6 @@
11import { memo } from "react";
22import { Alert, AlertAction, AlertDescription } from "../ui/alert";
3import { Button } from "../ui/button";
34import { CircleAlertIcon, XIcon } from "lucide-react";
45
56export const ThreadErrorBanner = memo(function ThreadErrorBanner({
......@@ -19,14 +20,9 @@ export const ThreadErrorBanner = memo(function ThreadErrorBanner({
1920 </AlertDescription>
2021 {onDismiss && (
2122 <AlertAction>
22 <button
23 type="button"
24 aria-label="Dismiss error"
25 className="inline-flex size-6 items-center justify-center rounded-md text-destructive/60 transition-colors hover:text-destructive"
26 onClick={onDismiss}
27 >
28 <XIcon className="size-3.5" />
29 </button>
23 <Button variant="ghost" size="icon-xs" aria-label="Dismiss error" onClick={onDismiss}>
24 <XIcon className="text-destructive" />
25 </Button>
3026 </AlertAction>
3127 )}
3228 </Alert>
apps/web/src/components/ui/alert-dialog.tsx+3-2
......@@ -17,8 +17,9 @@ function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) {
1717function AlertDialogBackdrop({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
1818 return (
1919 <AlertDialogPrimitive.Backdrop
20 forceRender
2021 className={cn(
21 "fixed inset-0 z-50 transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
22 "fixed inset-0 z-50 bg-background/60 backdrop-blur-xs transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
2223 className,
2324 )}
2425 data-slot="alert-dialog-backdrop"
......@@ -31,7 +32,7 @@ function AlertDialogViewport({ className, ...props }: AlertDialogPrimitive.Viewp
3132 return (
3233 <AlertDialogPrimitive.Viewport
3334 className={cn(
34 "fixed inset-0 z-50 grid grid-rows-[1fr_auto_3fr] justify-items-center bg-white/48 p-4 backdrop-blur-[2px] dark:bg-black/28",
35 "fixed inset-0 z-50 grid grid-rows-[1fr_auto_1fr] justify-items-center p-4",
3536 className,
3637 )}
3738 data-slot="alert-dialog-viewport"
apps/web/src/components/ui/alert.tsx+75-34
......@@ -1,55 +1,101 @@
11import { cva, type VariantProps } from "class-variance-authority";
2import { Children, isValidElement } from "react";
23import type * as React from "react";
34
45import { cn } from "~/lib/utils";
56
6const alertVariants = cva(
7 "relative grid w-full items-start gap-x-2 gap-y-0.5 rounded-xl border px-3.5 py-3 text-card-foreground text-sm has-[>svg]:has-data-[slot=alert-action]:grid-cols-[calc(var(--spacing)*4)_1fr_auto] has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-data-[slot=alert-action]:grid-cols-[1fr_auto] has-[>svg]:gap-x-2 [&>svg]:h-lh [&>svg]:w-4",
8 {
9 defaultVariants: {
10 variant: "default",
11 },
12 variants: {
13 variant: {
14 default: "bg-transparent dark:bg-input/32 [&>svg]:text-muted-foreground",
15 error: "border-destructive/32 bg-destructive/4 [&>svg]:text-destructive",
16 info: "border-info/32 bg-info/4 [&>svg]:text-info",
17 success: "border-success/32 bg-success/4 [&>svg]:text-success",
18 warning: "border-warning/32 bg-warning/4 [&>svg]:text-warning",
19 },
7const alertVariants = cva("relative rounded-xl border px-3.5 py-3 text-card-foreground text-sm", {
8 defaultVariants: {
9 variant: "default",
10 },
11 variants: {
12 variant: {
13 default: "bg-transparent dark:bg-input/32 [&_svg]:text-muted-foreground",
14 error:
15 "border-destructive/32 bg-destructive/4 text-destructive-foreground [&_[data-slot=alert-description]]:text-destructive-foreground/80 [&_svg]:text-destructive",
16 info: "border-info/32 bg-info/4 [&_svg]:text-info",
17 success: "border-success/32 bg-success/4 [&_svg]:text-success",
18 warning: "border-warning/32 bg-warning/4 [&_svg]:text-warning",
2019 },
2120 },
22);
21});
22
23function alertChildSlot(child: React.ReactElement): string | undefined {
24 const propsSlot = (child.props as Record<string, string | undefined>)["data-slot"];
25 if (propsSlot) {
26 return propsSlot;
27 }
28
29 const type = child.type as { displayName?: string; name?: string };
30 switch (type.displayName ?? type.name) {
31 case "AlertAction":
32 return "alert-action";
33 case "AlertTitle":
34 return "alert-title";
35 case "AlertDescription":
36 return "alert-description";
37 default:
38 return undefined;
39 }
40}
2341
2442function Alert({
2543 className,
2644 variant,
45 children,
2746 ...props
2847}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
48 const icon: React.ReactNode[] = [];
49 const content: React.ReactNode[] = [];
50 const action: React.ReactNode[] = [];
51
52 Children.forEach(children, (child) => {
53 if (!isValidElement(child)) {
54 content.push(child);
55 return;
56 }
57 const slot = alertChildSlot(child);
58 if (slot === "alert-action") {
59 action.push(child);
60 } else if (slot === "alert-title" || slot === "alert-description") {
61 content.push(child);
62 } else {
63 icon.push(child);
64 }
65 });
66
2967 return (
3068 <div
3169 className={cn(alertVariants({ variant }), className)}
3270 data-slot="alert"
3371 role="alert"
3472 {...props}
35 />
73 >
74 <div className="flex items-center gap-2">
75 {icon.length > 0 && (
76 <div className="flex size-4 shrink-0 items-center justify-center [&>svg]:size-full">
77 {icon}
78 </div>
79 )}
80 {content.length > 0 && (
81 <div className="flex min-w-0 flex-1 flex-col gap-0.5">{content}</div>
82 )}
83 {action.length > 0 && (
84 <div className="flex shrink-0 items-center self-center">{action}</div>
85 )}
86 </div>
87 </div>
3688 );
3789}
3890
3991function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
40 return (
41 <div
42 className={cn("font-medium [svg~&]:col-start-2", className)}
43 data-slot="alert-title"
44 {...props}
45 />
46 );
92 return <div className={cn("font-medium", className)} data-slot="alert-title" {...props} />;
4793}
4894
4995function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
5096 return (
5197 <div
52 className={cn("flex flex-col gap-2.5 text-muted-foreground [svg~&]:col-start-2", className)}
98 className={cn("flex flex-col gap-2.5 text-muted-foreground", className)}
5399 data-slot="alert-description"
54100 {...props}
55101 />
......@@ -57,16 +103,11 @@ function AlertDescription({ className, ...props }: React.ComponentProps<"div">)
57103}
58104
59105function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
60 return (
61 <div
62 className={cn(
63 "flex gap-1 max-sm:col-start-2 max-sm:mt-2 sm:row-start-1 sm:row-end-3 sm:self-center sm:[[data-slot=alert-description]~&]:col-start-2 sm:[[data-slot=alert-title]~&]:col-start-2 sm:[svg~&]:col-start-2 sm:[svg~[data-slot=alert-description]~&]:col-start-3 sm:[svg~[data-slot=alert-title]~&]:col-start-3",
64 className,
65 )}
66 data-slot="alert-action"
67 {...props}
68 />
69 );
106 return <div className={cn("flex gap-1", className)} data-slot="alert-action" {...props} />;
70107}
71108
109AlertTitle.displayName = "AlertTitle";
110AlertDescription.displayName = "AlertDescription";
111AlertAction.displayName = "AlertAction";
112
72113export { Alert, AlertTitle, AlertDescription, AlertAction };
apps/web/src/components/ui/command.tsx+1-1
......@@ -30,7 +30,7 @@ function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.B
3030 return (
3131 <CommandDialogPrimitive.Backdrop
3232 className={cn(
33 "fixed inset-0 z-50 bg-background/60 transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
33 "fixed inset-0 z-50 bg-background/60 backdrop-blur-xs transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
3434 className,
3535 )}
3636 data-slot="command-dialog-backdrop"
apps/web/src/components/ui/dialog.tsx+3-2
......@@ -23,8 +23,9 @@ function DialogClose(props: DialogPrimitive.Close.Props) {
2323function DialogBackdrop({ className, ...props }: DialogPrimitive.Backdrop.Props) {
2424 return (
2525 <DialogPrimitive.Backdrop
26 forceRender
2627 className={cn(
27 "fixed inset-0 z-50 transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
28 "fixed inset-0 z-50 bg-background/60 backdrop-blur-xs transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
2829 className,
2930 )}
3031 data-slot="dialog-backdrop"
......@@ -37,7 +38,7 @@ function DialogViewport({ className, ...props }: DialogPrimitive.Viewport.Props)
3738 return (
3839 <DialogPrimitive.Viewport
3940 className={cn(
40 "fixed inset-0 z-50 grid grid-rows-[1fr_auto_3fr] justify-items-center bg-white/48 p-4 backdrop-blur-[2px] dark:bg-black/28",
41 "fixed inset-0 z-50 grid grid-rows-[1fr_auto_1fr] justify-items-center p-4",
4142 className,
4243 )}
4344 data-slot="dialog-viewport"
apps/web/src/components/ui/sheet.tsx+1-1
......@@ -22,7 +22,7 @@ function SheetBackdrop({ className, ...props }: SheetPrimitive.Backdrop.Props) {
2222 return (
2323 <SheetPrimitive.Backdrop
2424 className={cn(
25 "fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
25 "fixed inset-0 z-50 bg-background/60 backdrop-blur-xs transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
2626 className,
2727 )}
2828 data-slot="sheet-backdrop"