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
This commit is contained in:
@@ -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 (
|
||||
<div key={`${group.providerId}-${label}`} className="flex flex-col gap-1.5">
|
||||
<div className="flex min-w-0 items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex items-center gap-2">
|
||||
<span className="truncate typography-ui-label text-foreground">{formatWindowLabel(label)}</span>
|
||||
{window.resetAfterFormatted ?? window.resetAtFormatted ? (
|
||||
{resetLabel ? (
|
||||
<span className="truncate typography-micro text-muted-foreground">
|
||||
{window.resetAfterFormatted ?? window.resetAtFormatted}
|
||||
{resetLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -2248,14 +2249,15 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
: calculateExpectedUsagePercent(paceInfo.elapsedRatio))
|
||||
: null;
|
||||
const metricLabel = formatQuotaValueLabel(window.valueLabel, displayPercent);
|
||||
const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted);
|
||||
return (
|
||||
<div key={`${group.providerId}-${label}`} className="flex flex-col gap-1.5">
|
||||
<div className="flex min-w-0 items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex items-center gap-2">
|
||||
<span className="truncate typography-ui-label text-foreground">{formatWindowLabel(label)}</span>
|
||||
{(window.resetAfterFormatted ?? window.resetAtFormatted) ? (
|
||||
{resetLabel ? (
|
||||
<span className="truncate typography-micro text-muted-foreground">
|
||||
{window.resetAfterFormatted ?? window.resetAtFormatted}
|
||||
{resetLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -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<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
||||
</div>
|
||||
)}
|
||||
<span className="flex items-center justify-between typography-micro text-muted-foreground text-[10px]">
|
||||
<span>{window.resetAfterFormatted ?? window.resetAtFormatted ?? ''}</span>
|
||||
<span>{formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted)}</span>
|
||||
</span>
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -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<UsageCardProps> = ({
|
||||
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(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ export {
|
||||
clampPercent,
|
||||
formatPercent,
|
||||
formatQuotaValueLabel,
|
||||
formatQuotaResetLabel,
|
||||
resolveUsageTone,
|
||||
formatWindowLabel,
|
||||
calculatePace,
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user