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) {...@@ -1843,7 +1843,11 @@ export default function ChatView(props: ChatViewProps) {
1843 // `codex_personal`) surfaces its own status/message in the banner rather1843 // `codex_personal`) surfaces its own status/message in the banner rather
1844 // than the default Codex's. Falls back to first-match-by-kind when no1844 // than the default Codex's. Falls back to first-match-by-kind when no
1845 // saved instance id is available or the instance no longer exists.1845 // saved instance id is available or the instance no longer exists.
1846 const selectedProviderInstanceId =
1847 providerStatuses.find((status) => status.instanceId === selectedProviderByThreadId)
1848 ?.instanceId ?? null;
1846 const activeProviderInstanceId =1849 const activeProviderInstanceId =
1850 selectedProviderInstanceId ??
1847 activeThread?.session?.providerInstanceId ??1851 activeThread?.session?.providerInstanceId ??
1848 activeThread?.modelSelection.instanceId ??1852 activeThread?.modelSelection.instanceId ??
1849 activeProject?.defaultModelSelection?.instanceId ??1853 activeProject?.defaultModelSelection?.instanceId ??
apps/web/src/components/chat/ProviderStatusBanner.tsx+31-16
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { type ServerProvider } from "@t3tools/contracts";1import { type ServerProvider } from "@t3tools/contracts";
2import { memo } from "react";2import { memo } from "react";
3import { Alert, AlertDescription, AlertTitle } from "../ui/alert";3import { InfoIcon } from "lucide-react";
4import { CircleAlertIcon } from "lucide-react";4import { cn } from "~/lib/utils";
5import { formatProviderDriverKindLabel } from "../../providerModels";5import { formatProviderDriverKindLabel } from "../../providerModels";
66
7export const ProviderStatusBanner = memo(function ProviderStatusBanner({7export const ProviderStatusBanner = memo(function ProviderStatusBanner({
...@@ -13,22 +13,37 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({...@@ -13,22 +13,37 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({
13 return null;13 return null;
14 }14 }
1515
16 const providerLabel = status.displayName?.trim() || formatProviderDriverKindLabel(status.driver);16 const providerName = status.displayName?.trim() || formatProviderDriverKindLabel(status.driver);
17 const defaultMessage =17 const isUnauthenticated = status.status === "error" && status.auth.status === "unauthenticated";
18 status.status === "error"18 const title = isUnauthenticated
19 ? `${providerLabel} provider is unavailable.`19 ? `${providerName} is unauthenticated`
20 : `${providerLabel} provider has limited availability.`;20 : `${providerName} provider status`;
21 const title = `${providerLabel} 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
23 return (28 return (
24 <div className="pt-3 mx-auto max-w-3xl">29 <div className="mx-auto w-fit max-w-[calc(100%-2rem)] pt-3">
25 <Alert variant={status.status === "error" ? "error" : "warning"}>30 <div
26 <CircleAlertIcon />31 className={cn(
27 <AlertTitle>{title}</AlertTitle>32 "inline-flex items-center gap-3 rounded-xl border px-3.5 py-3 text-card-foreground text-sm",
28 <AlertDescription className="line-clamp-3" title={status.message ?? defaultMessage}>33 status.status === "warning"
29 {status.message ?? defaultMessage}34 ? "border-warning/32 bg-warning/4 [&_svg]:text-warning"
30 </AlertDescription>35 : "border-destructive/32 bg-destructive/4 text-destructive-foreground [&_svg]:text-destructive",
31 </Alert>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>
32 </div>47 </div>
33 );48 );
34});49});
apps/web/src/components/chat/ThreadErrorBanner.tsx+4-8
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { memo } from "react";1import { memo } from "react";
2import { Alert, AlertAction, AlertDescription } from "../ui/alert";2import { Alert, AlertAction, AlertDescription } from "../ui/alert";
3import { Button } from "../ui/button";
3import { CircleAlertIcon, XIcon } from "lucide-react";4import { CircleAlertIcon, XIcon } from "lucide-react";
45
5export const ThreadErrorBanner = memo(function ThreadErrorBanner({6export const ThreadErrorBanner = memo(function ThreadErrorBanner({
...@@ -19,14 +20,9 @@ export const ThreadErrorBanner = memo(function ThreadErrorBanner({...@@ -19,14 +20,9 @@ export const ThreadErrorBanner = memo(function ThreadErrorBanner({
19 </AlertDescription>20 </AlertDescription>
20 {onDismiss && (21 {onDismiss && (
21 <AlertAction>22 <AlertAction>
22 <button23 <Button variant="ghost" size="icon-xs" aria-label="Dismiss error" onClick={onDismiss}>
23 type="button"24 <XIcon className="text-destructive" />
24 aria-label="Dismiss error"25 </Button>
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>
30 </AlertAction>26 </AlertAction>
31 )}27 )}
32 </Alert>28 </Alert>
apps/web/src/components/ui/alert-dialog.tsx+3-2
...@@ -17,8 +17,9 @@ function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) {...@@ -17,8 +17,9 @@ function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) {
17function AlertDialogBackdrop({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {17function AlertDialogBackdrop({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
18 return (18 return (
19 <AlertDialogPrimitive.Backdrop19 <AlertDialogPrimitive.Backdrop
20 forceRender
20 className={cn(21 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",
22 className,23 className,
23 )}24 )}
24 data-slot="alert-dialog-backdrop"25 data-slot="alert-dialog-backdrop"
...@@ -31,7 +32,7 @@ function AlertDialogViewport({ className, ...props }: AlertDialogPrimitive.Viewp...@@ -31,7 +32,7 @@ function AlertDialogViewport({ className, ...props }: AlertDialogPrimitive.Viewp
31 return (32 return (
32 <AlertDialogPrimitive.Viewport33 <AlertDialogPrimitive.Viewport
33 className={cn(34 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",
35 className,36 className,
36 )}37 )}
37 data-slot="alert-dialog-viewport"38 data-slot="alert-dialog-viewport"
apps/web/src/components/ui/alert.tsx+75-34
...@@ -1,55 +1,101 @@...@@ -1,55 +1,101 @@
1import { cva, type VariantProps } from "class-variance-authority";1import { cva, type VariantProps } from "class-variance-authority";
2import { Children, isValidElement } from "react";
2import type * as React from "react";3import type * as React from "react";
34
4import { cn } from "~/lib/utils";5import { cn } from "~/lib/utils";
56
6const alertVariants = cva(7const alertVariants = cva("relative rounded-xl border px-3.5 py-3 text-card-foreground text-sm", {
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 defaultVariants: {
8 {9 variant: "default",
9 defaultVariants: {10 },
10 variant: "default",11 variants: {
11 },12 variant: {
12 variants: {13 default: "bg-transparent dark:bg-input/32 [&_svg]:text-muted-foreground",
13 variant: {14 error:
14 default: "bg-transparent dark:bg-input/32 [&>svg]:text-muted-foreground",15 "border-destructive/32 bg-destructive/4 text-destructive-foreground [&_[data-slot=alert-description]]:text-destructive-foreground/80 [&_svg]:text-destructive",
15 error: "border-destructive/32 bg-destructive/4 [&>svg]:text-destructive",16 info: "border-info/32 bg-info/4 [&_svg]:text-info",
16 info: "border-info/32 bg-info/4 [&>svg]:text-info",17 success: "border-success/32 bg-success/4 [&_svg]:text-success",
17 success: "border-success/32 bg-success/4 [&>svg]:text-success",18 warning: "border-warning/32 bg-warning/4 [&_svg]:text-warning",
18 warning: "border-warning/32 bg-warning/4 [&>svg]:text-warning",
19 },
20 },19 },
21 },20 },
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
24function Alert({42function Alert({
25 className,43 className,
26 variant,44 variant,
45 children,
27 ...props46 ...props
28}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {47}: 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
29 return (67 return (
30 <div68 <div
31 className={cn(alertVariants({ variant }), className)}69 className={cn(alertVariants({ variant }), className)}
32 data-slot="alert"70 data-slot="alert"
33 role="alert"71 role="alert"
34 {...props}72 {...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>
36 );88 );
37}89}
3890
39function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {91function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
40 return (92 return <div className={cn("font-medium", className)} data-slot="alert-title" {...props} />;
41 <div
42 className={cn("font-medium [svg~&]:col-start-2", className)}
43 data-slot="alert-title"
44 {...props}
45 />
46 );
47}93}
4894
49function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {95function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
50 return (96 return (
51 <div97 <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)}
53 data-slot="alert-description"99 data-slot="alert-description"
54 {...props}100 {...props}
55 />101 />
...@@ -57,16 +103,11 @@ function AlertDescription({ className, ...props }: React.ComponentProps<"div">)...@@ -57,16 +103,11 @@ function AlertDescription({ className, ...props }: React.ComponentProps<"div">)
57}103}
58104
59function AlertAction({ className, ...props }: React.ComponentProps<"div">) {105function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
60 return (106 return <div className={cn("flex gap-1", className)} data-slot="alert-action" {...props} />;
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 );
70}107}
71108
109AlertTitle.displayName = "AlertTitle";
110AlertDescription.displayName = "AlertDescription";
111AlertAction.displayName = "AlertAction";
112
72export { Alert, AlertTitle, AlertDescription, AlertAction };113export { Alert, AlertTitle, AlertDescription, AlertAction };
apps/web/src/components/ui/command.tsx+1-1
...@@ -30,7 +30,7 @@ function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.B...@@ -30,7 +30,7 @@ function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.B
30 return (30 return (
31 <CommandDialogPrimitive.Backdrop31 <CommandDialogPrimitive.Backdrop
32 className={cn(32 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",
34 className,34 className,
35 )}35 )}
36 data-slot="command-dialog-backdrop"36 data-slot="command-dialog-backdrop"
apps/web/src/components/ui/dialog.tsx+3-2
...@@ -23,8 +23,9 @@ function DialogClose(props: DialogPrimitive.Close.Props) {...@@ -23,8 +23,9 @@ function DialogClose(props: DialogPrimitive.Close.Props) {
23function DialogBackdrop({ className, ...props }: DialogPrimitive.Backdrop.Props) {23function DialogBackdrop({ className, ...props }: DialogPrimitive.Backdrop.Props) {
24 return (24 return (
25 <DialogPrimitive.Backdrop25 <DialogPrimitive.Backdrop
26 forceRender
26 className={cn(27 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",
28 className,29 className,
29 )}30 )}
30 data-slot="dialog-backdrop"31 data-slot="dialog-backdrop"
...@@ -37,7 +38,7 @@ function DialogViewport({ className, ...props }: DialogPrimitive.Viewport.Props)...@@ -37,7 +38,7 @@ function DialogViewport({ className, ...props }: DialogPrimitive.Viewport.Props)
37 return (38 return (
38 <DialogPrimitive.Viewport39 <DialogPrimitive.Viewport
39 className={cn(40 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",
41 className,42 className,
42 )}43 )}
43 data-slot="dialog-viewport"44 data-slot="dialog-viewport"
apps/web/src/components/ui/sheet.tsx+1-1
...@@ -22,7 +22,7 @@ function SheetBackdrop({ className, ...props }: SheetPrimitive.Backdrop.Props) {...@@ -22,7 +22,7 @@ function SheetBackdrop({ className, ...props }: SheetPrimitive.Backdrop.Props) {
22 return (22 return (
23 <SheetPrimitive.Backdrop23 <SheetPrimitive.Backdrop
24 className={cn(24 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",
26 className,26 className,
27 )}27 )}
28 data-slot="sheet-backdrop"28 data-slot="sheet-backdrop"