fix(cliproxy): refresh non-default gemini quota tokens

This commit is contained in:
Tam Nhu Tran committed 2026-04-08 23:55:55 -04:00
1 parent 04878f0864
commit b6fb443f57
11 files changed
+207 -71

No files matched your search

@@ -249,7 +249,7 @@ export function AccountCard({
)}
</div>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
<TooltipContent side="top" className="max-w-sm">
<QuotaTooltipContent quota={quota} resetTime={resetTime} />
</TooltipContent>
</Tooltip>
@@ -250,7 +250,7 @@ export function AccountQuotaPanel({
</div>
)}
</TooltipTrigger>
<TooltipContent side={mode === 'compact' ? 'top' : 'bottom'} className="max-w-xs">
<TooltipContent side={mode === 'compact' ? 'top' : 'bottom'} className="max-w-sm">
<QuotaTooltipContent quota={quota} resetTime={resetTime} />
</TooltipContent>
</Tooltip>
@@ -306,23 +306,8 @@ export function AccountQuotaPanel({
</div>
)}
</TooltipTrigger>
<TooltipContent side={mode === 'compact' ? 'top' : 'bottom'} className="max-w-[260px]">
<div className="space-y-1 text-xs">
<p>{failureInfo.summary}</p>
{failureInfo.actionHint && (
<p className="text-muted-foreground">{failureInfo.actionHint}</p>
)}
{failureInfo.technicalDetail && (
<p className="font-mono text-[11px] text-muted-foreground">
{failureInfo.technicalDetail}
</p>
)}
{failureInfo.rawDetail && (
<pre className="whitespace-pre-wrap break-all rounded bg-muted/40 px-2 py-1 font-mono text-[10px] text-muted-foreground">
{failureInfo.rawDetail}
</pre>
)}
</div>
<TooltipContent side={mode === 'compact' ? 'top' : 'bottom'} className="max-w-sm">
<QuotaTooltipContent quota={quota} resetTime={resetTime} />
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -86,22 +86,35 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
if (!quota.success) {
const failureInfo = getQuotaFailureInfo(quota);
const failureToneClass =
failureInfo?.tone === 'destructive'
? 'text-destructive'
: failureInfo?.tone === 'warning'
? 'text-amber-700 dark:text-amber-300'
: 'text-foreground';
return (
<div className="text-xs space-y-1">
<p className="font-medium text-destructive">
{failureInfo?.label || quota.error || 'Failed to load quota'}
</p>
<p className="text-destructive/90">{failureInfo?.summary || quota.error}</p>
<div className="min-w-[16rem] space-y-2 text-xs">
<div className="space-y-1">
<p className={cn('font-semibold tracking-tight', failureToneClass)}>
{failureInfo?.label || quota.error || 'Failed to load quota'}
</p>
<p className="leading-relaxed text-foreground/90">
{failureInfo?.summary || quota.error}
</p>
</div>
{failureInfo?.actionHint && (
<p className="text-muted-foreground">{failureInfo.actionHint}</p>
<div className="rounded-md border border-border/70 bg-muted/35 px-2.5 py-2 text-foreground/80">
{failureInfo.actionHint}
</div>
)}
{failureInfo?.technicalDetail && (
<p className="font-mono text-[11px] text-muted-foreground">
<div className="rounded-md border border-border/60 bg-muted/25 px-2 py-1.5 font-mono text-[11px] text-foreground/75">
{failureInfo.technicalDetail}
</p>
</div>
)}
{failureInfo?.rawDetail && (
<pre className="whitespace-pre-wrap break-all rounded bg-muted/40 px-2 py-1 font-mono text-[10px] text-muted-foreground">
<pre className="max-h-56 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border/70 bg-muted/55 px-2.5 py-2 font-mono text-[11px] leading-relaxed text-foreground/85">
{failureInfo.rawDetail}
</pre>
)}
@@ -116,7 +129,7 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
const tierOrder: ModelTier[] = ['primary', 'gemini-3', 'gemini-2', 'other'];
return (
<div className="text-xs space-y-1">
<div className="text-xs space-y-1.5">
<p className="font-medium">Model Quotas:</p>
{tierOrder.map((tier, idx) => {
const models = groups.get(tier);
@@ -124,7 +137,7 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
const isFirst = tierOrder.slice(0, idx).every((t) => !groups.get(t)?.length);
return (
<div key={tier}>
{!isFirst && <div className="border-t border-border/40 my-1" />}
{!isFirst && <div className="my-1 border-t border-border/40" />}
{models.map((m) => (
<div key={m.name} className="flex justify-between gap-4">
<span className={cn('truncate', m.exhausted && 'text-red-500')}>
@@ -159,7 +172,7 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
);
return (
<div className="text-xs space-y-1">
<div className="text-xs space-y-1.5">
<p className="font-medium">Rate Limits:</p>
{quota.planType && <p className="text-muted-foreground">Plan: {quota.planType}</p>}
{orderedWindows.map((w, index) => (
@@ -228,7 +241,7 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
null;
return (
<div className="text-xs space-y-1">
<div className="text-xs space-y-1.5">
<p className="font-medium">Rate Limits:</p>
{orderedWindows.map((window, index) => (
<div
@@ -255,7 +268,7 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
const hasBucketResetTime = quota.buckets.some((bucket) => !!bucket.resetTime);
return (
<div className="text-xs space-y-1">
<div className="text-xs space-y-1.5">
{quota.tierLabel && (
<div className="flex justify-between gap-4">
<span className="text-muted-foreground">Tier</span>
@@ -306,7 +319,7 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
const planLabel = formatPlanLabel(quota.planType);
return (
<div className="text-xs space-y-1">
<div className="text-xs space-y-1.5">
<p className="font-medium">Quota Snapshots:</p>
{planLabel && <p className="text-muted-foreground">Plan: {planLabel}</p>}
{snapshotRows.map(({ label, snapshot }) => {
@@ -344,9 +357,11 @@ function ResetTimeIndicator({ resetTime }: { resetTime: string | null }) {
if (!resetTime) return null;
return (
<div className="flex items-center gap-1.5 pt-1 border-t border-border/50">
<Clock className="w-3 h-3 text-blue-400" />
<span className="text-blue-400 font-medium">Resets {formatResetTime(resetTime)}</span>
<div className="flex items-center gap-1.5 border-t border-border/60 pt-1">
<Clock className="h-3 w-3 text-sky-600 dark:text-sky-300" />
<span className="font-medium text-sky-600 dark:text-sky-300">
Resets {formatResetTime(resetTime)}
</span>
</div>
);
}
@@ -364,19 +379,19 @@ function CodexResetIndicators({
if (!hasSpecificReset && !fallbackResetTime) return null;
return (
<div className="pt-1 border-t border-border/50 space-y-1">
<div className="space-y-1 border-t border-border/60 pt-1">
{fiveHourResetTime && (
<div className="flex items-center gap-1.5">
<Clock className="w-3 h-3 text-blue-400" />
<span className="text-blue-400 font-medium">
<Clock className="h-3 w-3 text-sky-600 dark:text-sky-300" />
<span className="font-medium text-sky-600 dark:text-sky-300">
5h resets {formatResetTime(fiveHourResetTime)}
</span>
</div>
)}
{weeklyResetTime && (
<div className="flex items-center gap-1.5">
<Clock className="w-3 h-3 text-indigo-400" />
<span className="text-indigo-400 font-medium">
<Clock className="h-3 w-3 text-indigo-600 dark:text-indigo-300" />
<span className="font-medium text-indigo-600 dark:text-indigo-300">
Weekly resets {formatResetTime(weeklyResetTime)}
</span>
</div>
+1 -1
View File
@@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 w-80 max-w-[calc(100vw-2rem)] rounded-lg border border-border/70 bg-popover p-4 text-popover-foreground shadow-xl shadow-black/10 outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
+3 -3
View File
@@ -32,7 +32,7 @@ function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimiti
function TooltipContent({
className,
sideOffset = 0,
sideOffset = 6,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
@@ -42,13 +42,13 @@ function TooltipContent({
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
'animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-w-sm origin-(--radix-tooltip-content-transform-origin) rounded-lg border border-border/70 bg-popover px-3 py-2 text-left text-xs leading-relaxed text-popover-foreground shadow-xl shadow-black/10',
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 fill-popover stroke-border stroke-[0.75px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
@@ -133,6 +133,9 @@ describe('AccountCard grouped quota tooltip', () => {
await userEvent.hover(screen.getByText('Business'));
expect((await screen.findAllByText('Plan: team')).length).toBeGreaterThan(0);
expect(screen.getAllByText('5h usage limit').length).toBeGreaterThan(0);
const tooltipContent = document.querySelector('[data-slot="tooltip-content"]');
expect(tooltipContent?.className).toContain('bg-popover');
expect(tooltipContent?.className).toContain('text-popover-foreground');
await userEvent.hover(screen.getByText('Personal'));
expect((await screen.findAllByText('Plan: plus')).length).toBeGreaterThan(0);
@@ -90,4 +90,28 @@ describe('QuotaTooltipContent', () => {
vi.useRealTimers();
});
it('renders failure summaries, action hints, and raw details with readable structure', () => {
const quota = createGeminiQuotaResult({
success: false,
buckets: [],
error: 'Request had invalid authentication credentials.',
httpStatus: 401,
errorCode: 'UNAUTHENTICATED',
errorDetail:
'{"error":{"code":401,"message":"Request had invalid authentication credentials.","status":"UNAUTHENTICATED"}}',
actionHint: 'Run ccs gemini --auth to reconnect this account.',
needsReauth: true,
});
render(<QuotaTooltipContent quota={quota} resetTime={null} />);
expect(screen.getByText('Reauth')).toBeInTheDocument();
expect(screen.getByText('Request had invalid authentication credentials.')).toBeInTheDocument();
expect(
screen.getByText('Run ccs gemini --auth to reconnect this account.')
).toBeInTheDocument();
expect(screen.getByText('HTTP 401 | UNAUTHENTICATED')).toBeInTheDocument();
expect(screen.getByText(/"status":"UNAUTHENTICATED"/)).toBeInTheDocument();
});
});