| author | |
| committer | |
| log | 238715fd6fb1e5106db1e310eed7c389304b4840 |
| tree | 00ce625f75cd3108319fd4bd440d2e8cef89886a |
| parent | b03bc4b52d184c5f2367d702e043586a0e9fcdaa |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 1843 | // `codex_personal`) surfaces its own status/message in the banner rather |
| 1844 | 1844 | // than the default Codex's. Falls back to first-match-by-kind when no |
| 1845 | 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 | 1849 | const activeProviderInstanceId = |
| 1850 | selectedProviderInstanceId ?? | |
| 1847 | 1851 | activeThread?.session?.providerInstanceId ?? |
| 1848 | 1852 | activeThread?.modelSelection.instanceId ?? |
| 1849 | 1853 | activeProject?.defaultModelSelection?.instanceId ?? |
apps/web/src/components/chat/ProviderStatusBanner.tsx+31-16| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | import { type ServerProvider } from "@t3tools/contracts"; |
| 2 | 2 | import { memo } from "react"; |
| 3 | import { Alert, AlertDescription, AlertTitle } from "../ui/alert"; | |
| 4 | import { CircleAlertIcon } from "lucide-react"; | |
| 3 | import { InfoIcon } from "lucide-react"; | |
| 4 | import { cn } from "~/lib/utils"; | |
| 5 | 5 | import { formatProviderDriverKindLabel } from "../../providerModels"; |
| 6 | 6 | |
| 7 | 7 | export const ProviderStatusBanner = memo(function ProviderStatusBanner({ |
| ... | ... | @@ -13,22 +13,37 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({ |
| 13 | 13 | return null; |
| 14 | 14 | } |
| 15 | 15 | |
| 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.`)); | |
| 22 | 27 | |
| 23 | 28 | 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> | |
| 32 | 47 | </div> |
| 33 | 48 | ); |
| 34 | 49 | }); |
apps/web/src/components/chat/ThreadErrorBanner.tsx+4-8| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 | import { memo } from "react"; |
| 2 | 2 | import { Alert, AlertAction, AlertDescription } from "../ui/alert"; |
| 3 | import { Button } from "../ui/button"; | |
| 3 | 4 | import { CircleAlertIcon, XIcon } from "lucide-react"; |
| 4 | 5 | |
| 5 | 6 | export const ThreadErrorBanner = memo(function ThreadErrorBanner({ |
| ... | ... | @@ -19,14 +20,9 @@ export const ThreadErrorBanner = memo(function ThreadErrorBanner({ |
| 19 | 20 | </AlertDescription> |
| 20 | 21 | {onDismiss && ( |
| 21 | 22 | <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> | |
| 30 | 26 | </AlertAction> |
| 31 | 27 | )} |
| 32 | 28 | </Alert> |
apps/web/src/components/ui/alert-dialog.tsx+3-2| ... | ... | @@ -17,8 +17,9 @@ function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) { |
| 17 | 17 | function AlertDialogBackdrop({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) { |
| 18 | 18 | return ( |
| 19 | 19 | <AlertDialogPrimitive.Backdrop |
| 20 | forceRender | |
| 20 | 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 | 23 | className, |
| 23 | 24 | )} |
| 24 | 25 | data-slot="alert-dialog-backdrop" |
| ... | ... | @@ -31,7 +32,7 @@ function AlertDialogViewport({ className, ...props }: AlertDialogPrimitive.Viewp |
| 31 | 32 | return ( |
| 32 | 33 | <AlertDialogPrimitive.Viewport |
| 33 | 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 | 36 | className, |
| 36 | 37 | )} |
| 37 | 38 | data-slot="alert-dialog-viewport" |
apps/web/src/components/ui/alert.tsx+75-34| ... | ... | @@ -1,55 +1,101 @@ |
| 1 | 1 | import { cva, type VariantProps } from "class-variance-authority"; |
| 2 | import { Children, isValidElement } from "react"; | |
| 2 | 3 | import type * as React from "react"; |
| 3 | 4 | |
| 4 | 5 | import { cn } from "~/lib/utils"; |
| 5 | 6 | |
| 6 | const 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 | }, | |
| 7 | const 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", | |
| 20 | 19 | }, |
| 21 | 20 | }, |
| 22 | ); | |
| 21 | }); | |
| 22 | ||
| 23 | function 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 | } | |
| 23 | 41 | |
| 24 | 42 | function Alert({ |
| 25 | 43 | className, |
| 26 | 44 | variant, |
| 45 | children, | |
| 27 | 46 | ...props |
| 28 | 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 | 67 | return ( |
| 30 | 68 | <div |
| 31 | 69 | className={cn(alertVariants({ variant }), className)} |
| 32 | 70 | data-slot="alert" |
| 33 | 71 | role="alert" |
| 34 | 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 | } |
| 38 | 90 | |
| 39 | 91 | function 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} />; | |
| 47 | 93 | } |
| 48 | 94 | |
| 49 | 95 | function AlertDescription({ className, ...props }: React.ComponentProps<"div">) { |
| 50 | 96 | return ( |
| 51 | 97 | <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 | 99 | data-slot="alert-description" |
| 54 | 100 | {...props} |
| 55 | 101 | /> |
| ... | ... | @@ -57,16 +103,11 @@ function AlertDescription({ className, ...props }: React.ComponentProps<"div">) |
| 57 | 103 | } |
| 58 | 104 | |
| 59 | 105 | function 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} />; | |
| 70 | 107 | } |
| 71 | 108 | |
| 109 | AlertTitle.displayName = "AlertTitle"; | |
| 110 | AlertDescription.displayName = "AlertDescription"; | |
| 111 | AlertAction.displayName = "AlertAction"; | |
| 112 | ||
| 72 | 113 | export { Alert, AlertTitle, AlertDescription, AlertAction }; |
apps/web/src/components/ui/command.tsx+1-1| ... | ... | @@ -30,7 +30,7 @@ function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.B |
| 30 | 30 | return ( |
| 31 | 31 | <CommandDialogPrimitive.Backdrop |
| 32 | 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 | 34 | className, |
| 35 | 35 | )} |
| 36 | 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 | 23 | function DialogBackdrop({ className, ...props }: DialogPrimitive.Backdrop.Props) { |
| 24 | 24 | return ( |
| 25 | 25 | <DialogPrimitive.Backdrop |
| 26 | forceRender | |
| 26 | 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 | 29 | className, |
| 29 | 30 | )} |
| 30 | 31 | data-slot="dialog-backdrop" |
| ... | ... | @@ -37,7 +38,7 @@ function DialogViewport({ className, ...props }: DialogPrimitive.Viewport.Props) |
| 37 | 38 | return ( |
| 38 | 39 | <DialogPrimitive.Viewport |
| 39 | 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 | 42 | className, |
| 42 | 43 | )} |
| 43 | 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 | 22 | return ( |
| 23 | 23 | <SheetPrimitive.Backdrop |
| 24 | 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 | 26 | className, |
| 27 | 27 | )} |
| 28 | 28 | data-slot="sheet-backdrop" |