From 29811ec03edb14176cb5e1edbaf13299c22b71da Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Fri, 15 May 2026 01:03:36 +0300 Subject: [PATCH] fix: show quota reset times in local timezone (#1128) Formats rate-limit reset timestamps in the client timezone Keeps server-provided reset labels as fallbacks Applies consistent reset labels across header, usage, and VS Code views --- packages/ui/src/components/layout/Header.tsx | 12 ++++--- .../ui/src/components/layout/VSCodeLayout.tsx | 4 +-- .../components/sections/usage/UsageCard.tsx | 4 +-- packages/ui/src/lib/quota/index.ts | 1 + packages/ui/src/lib/quota/utils.ts | 36 +++++++++++++++++++ 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index 81defbdc..a8db774c 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -38,7 +38,7 @@ import { cn, hasModifier } from '@/lib/utils'; import { McpDropdownContent } from '@/components/mcp/McpDropdown'; import { McpIcon } from '@/components/icons/McpIcon'; import { ProviderLogo } from '@/components/ui/ProviderLogo'; -import { formatQuotaValueLabel, formatWindowLabel, QUOTA_PROVIDERS, calculatePace, calculateExpectedUsagePercent } from '@/lib/quota'; +import { formatQuotaValueLabel, formatQuotaResetLabel, formatWindowLabel, QUOTA_PROVIDERS, calculatePace, calculateExpectedUsagePercent } from '@/lib/quota'; import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar'; import { PaceIndicator } from '@/components/sections/usage/PaceIndicator'; import { updateDesktopSettings } from '@/lib/persistence'; @@ -447,14 +447,15 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({ : calculateExpectedUsagePercent(paceInfo.elapsedRatio)) : null; const metricLabel = formatQuotaValueLabel(window.valueLabel, displayPercent); + const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted); return (
{formatWindowLabel(label)} - {window.resetAfterFormatted ?? window.resetAtFormatted ? ( + {resetLabel ? ( - {window.resetAfterFormatted ?? window.resetAtFormatted} + {resetLabel} ) : null}
@@ -2248,14 +2249,15 @@ export const Header: React.FC = ({ : calculateExpectedUsagePercent(paceInfo.elapsedRatio)) : null; const metricLabel = formatQuotaValueLabel(window.valueLabel, displayPercent); + const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted); return (
{formatWindowLabel(label)} - {(window.resetAfterFormatted ?? window.resetAtFormatted) ? ( + {resetLabel ? ( - {window.resetAfterFormatted ?? window.resetAtFormatted} + {resetLabel} ) : null}
diff --git a/packages/ui/src/components/layout/VSCodeLayout.tsx b/packages/ui/src/components/layout/VSCodeLayout.tsx index 5cf478c9..e74bb693 100644 --- a/packages/ui/src/components/layout/VSCodeLayout.tsx +++ b/packages/ui/src/components/layout/VSCodeLayout.tsx @@ -24,7 +24,7 @@ import { ProviderLogo } from '@/components/ui/ProviderLogo'; import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar'; import { PaceIndicator } from '@/components/sections/usage/PaceIndicator'; import { Icon } from "@/components/icon/Icon"; -import { formatQuotaValueLabel, formatWindowLabel, QUOTA_PROVIDERS, calculatePace, calculateExpectedUsagePercent } from '@/lib/quota'; +import { formatQuotaValueLabel, formatQuotaResetLabel, formatWindowLabel, QUOTA_PROVIDERS, calculatePace, calculateExpectedUsagePercent } from '@/lib/quota'; import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore'; import { useUpdateStore } from '@/stores/useUpdateStore'; import { updateDesktopSettings } from '@/lib/persistence'; @@ -850,7 +850,7 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on
)} - {window.resetAfterFormatted ?? window.resetAtFormatted ?? ''} + {formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted)} diff --git a/packages/ui/src/components/sections/usage/UsageCard.tsx b/packages/ui/src/components/sections/usage/UsageCard.tsx index ebd50105..96f642a7 100644 --- a/packages/ui/src/components/sections/usage/UsageCard.tsx +++ b/packages/ui/src/components/sections/usage/UsageCard.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type { UsageWindow } from '@/types'; -import { formatQuotaValueLabel, formatWindowLabel, calculatePace, calculateExpectedUsagePercent } from '@/lib/quota'; +import { formatQuotaValueLabel, formatQuotaResetLabel, formatWindowLabel, calculatePace, calculateExpectedUsagePercent } from '@/lib/quota'; import { UsageProgressBar } from './UsageProgressBar'; import { PaceIndicator } from './PaceIndicator'; import { useQuotaStore } from '@/stores/useQuotaStore'; @@ -27,7 +27,7 @@ export const UsageCard: React.FC = ({ const displayPercent = displayMode === 'remaining' ? window.remainingPercent : window.usedPercent; const barLabel = displayMode === 'remaining' ? 'remaining' : 'used'; const percentLabel = formatQuotaValueLabel(window.valueLabel, displayPercent); - const resetLabel = window.resetAfterFormatted ?? window.resetAtFormatted ?? ''; + const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted); const windowLabel = formatWindowLabel(title); const paceInfo = React.useMemo(() => { diff --git a/packages/ui/src/lib/quota/index.ts b/packages/ui/src/lib/quota/index.ts index 82b37dd9..a2a4cc7c 100644 --- a/packages/ui/src/lib/quota/index.ts +++ b/packages/ui/src/lib/quota/index.ts @@ -4,6 +4,7 @@ export { clampPercent, formatPercent, formatQuotaValueLabel, + formatQuotaResetLabel, resolveUsageTone, formatWindowLabel, calculatePace, diff --git a/packages/ui/src/lib/quota/utils.ts b/packages/ui/src/lib/quota/utils.ts index ad7dafd9..a0d87a2b 100644 --- a/packages/ui/src/lib/quota/utils.ts +++ b/packages/ui/src/lib/quota/utils.ts @@ -19,6 +19,42 @@ export const formatQuotaValueLabel = ( return valueLabel ?? formatPercent(percent); }; +export const formatQuotaResetLabel = ( + resetAt: number | null, + fallback?: string | null, +): string => { + if (!resetAt) { + return fallback ?? ''; + } + + try { + const resetDate = new Date(resetAt); + if (!Number.isFinite(resetDate.getTime())) { + return fallback ?? ''; + } + + const now = new Date(); + const isToday = resetDate.toDateString() === now.toDateString(); + + if (isToday) { + return resetDate.toLocaleTimeString(undefined, { + hour: 'numeric', + minute: '2-digit', + }); + } + + return resetDate.toLocaleString(undefined, { + month: 'short', + day: 'numeric', + weekday: 'short', + hour: 'numeric', + minute: '2-digit', + }); + } catch { + return fallback ?? ''; + } +}; + export const resolveUsageTone = (percent: number | null): 'safe' | 'warn' | 'critical' => { if (percent === null) { return 'safe';