fix(ui): add missing field in secondary window#1457
Conversation
Greptile SummaryThis PR fixes missing Confidence Score: 5/5Safe to merge; the only remaining finding is a P2 i18n style gap that doesn't break functionality. All changes are additive UI improvements. The core fix (adding
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[QuotaRow] --> B{channel.type}
B -- claudecode --> C[windows 5h / 7d / overage]
B -- codex --> D[rate_limit primary / secondary]
C --> E[ProgressBar usage\ngetClaudeDurationPercent → hardcoded 5h/7d limits]
C --> F[ProgressBar duration\ntype=duration → zinc color]
C --> G[Reset info: formatTimeToReset + formatDate]
D --> H[ProgressBar usage\ncalcDurationPercent → reset_after_seconds NEW]
D --> I[ProgressBar duration\ntype=duration]
D --> J[Reset info: formatTimeToReset reset_after_seconds + formatDate reset_at]
E --> K[severity = u × u/d\nHSL green→yellow→red]
H --> K
Reviews (3): Last reviewed commit: "fix: claude window" | Re-trigger Greptile |
There was a problem hiding this comment.
Pull request overview
Updates the provider quota popover UI so Codex channels display complete reset information (including the previously missing secondary-window reset details), improving visibility into when rate limits will refresh.
Changes:
- Extends
QuotaData.rate_limit.{primary_window,secondary_window}to includereset_after_seconds. - Enhances reset formatting to support both absolute reset timestamps and relative “reset in” seconds, including multi-day durations.
- Adds “Reset in” / “Resets at” rows for both primary and secondary Codex windows and visually separates the secondary window section.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const formatTimeToReset = (resetAtOrSeconds?: string | number | null) => { | ||
| if (!resetAtOrSeconds) return t('quota.unknown'); | ||
|
|
||
| let resetTimeMs: number; | ||
| if (typeof resetAtOrSeconds === 'number') { | ||
| resetTimeMs = Date.now() + resetAtOrSeconds * 1000; | ||
| } else { |
There was a problem hiding this comment.
formatTimeToReset treats 0 as “unknown” due to if (!resetAtOrSeconds). Since reset_after_seconds can be 0 (backend includes the field even when it’s zero), this will incorrectly display “Unknown” instead of an immediate reset/0m. Consider checking resetAtOrSeconds == null (and optionally === '') rather than truthiness so 0 is handled correctly.
| const reset = new Date(resetAt).getTime(); | ||
| const diffMs = reset - now; | ||
| const diffMs = resetTimeMs - now; | ||
| if (diffMs < 0) return 'Reset now'; |
There was a problem hiding this comment.
formatTimeToReset returns the hardcoded string "Reset now" when the computed reset time is in the past. This bypasses i18n (and differs from other quota labels using t(...)). Please use a translated string/key (or reuse an existing one) for this state.
| if (diffMs < 0) return 'Reset now'; | |
| if (diffMs < 0) return t('quota.resetNow', { defaultValue: 'Reset now' }); |
There was a problem hiding this comment.
Code Review
This pull request enhances the quota-badges.tsx component by adding support for reset_after_seconds and improving the formatting of reset times and dates. Key changes include a more robust formatTimeToReset function, a new formatDate utility, and UI updates to display both relative and absolute reset times for primary and secondary windows. Review feedback suggests refining the falsy check in formatTimeToReset to correctly handle 0, maintaining consistent time formatting by avoiding toLocaleTimeString, and restoring conditional rendering for reset fields to prevent the UI from displaying "Unknown" when data is unavailable.
| const formatTimeToReset = (resetAt?: string | null) => { | ||
| if (!resetAt) return t('quota.unknown'); | ||
| const formatTimeToReset = (resetAtOrSeconds?: string | number | null) => { | ||
| if (!resetAtOrSeconds) return t('quota.unknown'); |
There was a problem hiding this comment.
The check !resetAtOrSeconds will return "Unknown" if the value is 0. However, 0 is a valid value for reset_after_seconds (indicating the quota resets immediately). It is safer to check specifically for null, undefined, or empty strings.
| if (!resetAtOrSeconds) return t('quota.unknown'); | |
| if (resetAtOrSeconds === undefined || resetAtOrSeconds === null || resetAtOrSeconds === '') return t('quota.unknown'); |
|
|
||
| // If it's today, only show time | ||
| if (date.toDateString() === now.toDateString()) { | ||
| return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); |
There was a problem hiding this comment.
Using toLocaleTimeString here introduces inconsistency with the other date formats in this function (lines 145 and 148), which use a hardcoded 24-hour format (HH:mm). Depending on the user's browser locale, this might show 12-hour time (AM/PM) for today's resets but 24-hour time for others. It is better to use format(date, 'HH:mm') for consistency.
| return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| return format(date, 'HH:mm'); |
| <div className="flex justify-between items-center text-muted-foreground"> | ||
| <span>{t('quota.label.reset_in')}</span> | ||
| <span>{formatTimeToReset(quotaData.rate_limit?.primary_window?.reset_after_seconds)}</span> | ||
| </div> | ||
| <div className="flex justify-between items-center text-muted-foreground"> | ||
| <span>{t('quota.label.resets_at')}</span> | ||
| <span>{formatDate(quotaData.rate_limit?.primary_window?.reset_at)}</span> | ||
| </div> |
There was a problem hiding this comment.
These fields are now displayed unconditionally, which will result in "Unknown" being shown in the UI if the data is missing from the API response. Previously, the resets_at field was conditionally rendered. It is better to wrap these in checks to keep the UI clean when data is unavailable.
{quotaData.rate_limit?.primary_window?.reset_after_seconds !== undefined && (
<div className="flex justify-between items-center text-muted-foreground">
<span>{t('quota.label.reset_in')}</span>
<span>{formatTimeToReset(quotaData.rate_limit.primary_window.reset_after_seconds)}</span>
</div>
)}
{quotaData.rate_limit?.primary_window?.reset_at && (
<div className="flex justify-between items-center text-muted-foreground">
<span>{t('quota.label.resets_at')}</span>
<span>{formatDate(quotaData.rate_limit.primary_window.reset_at)}</span>
</div>
)}
| <div className="flex justify-between items-center text-muted-foreground"> | ||
| <span>{t('quota.label.reset_in')}</span> | ||
| <span>{formatTimeToReset(quotaData.rate_limit.secondary_window.reset_after_seconds)}</span> | ||
| </div> | ||
| <div className="flex justify-between items-center text-muted-foreground"> | ||
| <span>{t('quota.label.resets_at')}</span> | ||
| <span>{formatDate(quotaData.rate_limit.secondary_window.reset_at)}</span> | ||
| </div> |
There was a problem hiding this comment.
Similar to the primary window, these fields should be conditionally rendered to avoid showing "Unknown" when the specific fields are missing from the secondary window data.
{quotaData.rate_limit.secondary_window.reset_after_seconds !== undefined && (
<div className="flex justify-between items-center text-muted-foreground">
<span>{t('quota.label.reset_in')}</span>
<span>{formatTimeToReset(quotaData.rate_limit.secondary_window.reset_after_seconds)}</span>
</div>
)}
{quotaData.rate_limit.secondary_window.reset_at && (
<div className="flex justify-between items-center text-muted-foreground">
<span>{t('quota.label.resets_at')}</span>
<span>{formatDate(quotaData.rate_limit.secondary_window.reset_at)}</span>
</div>
)}
|
改进了一下,是否有必要提交上来 @looplj
|
|
可以啊,很不错 |
|
我这里只测试了codex,希望可以确认下claude额度是否正常显示。 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {quotaData.rate_limit?.secondary_window?.used_percent !== undefined && ( | ||
| <div className="space-y-2.5 pt-3 mt-3 border-t border-dashed border-border/60"> | ||
| <div className="space-y-1"> | ||
| <div className="flex justify-between items-center text-xs"> | ||
| <span className="font-medium text-muted-foreground">{t('quota.label.secondary_window')}</span> | ||
| <span className="font-medium text-foreground">{Math.round(quotaData.rate_limit.secondary_window.used_percent)}%</span> | ||
| </div> | ||
| <ProgressBar | ||
| percentage={quotaData.rate_limit.secondary_window.used_percent} | ||
| durationPercentage={quotaData.rate_limit.secondary_window.limit_window_seconds ? calcDurationPercent(quotaData.rate_limit.secondary_window.limit_window_seconds, quotaData.rate_limit.secondary_window.reset_after_seconds) : undefined} | ||
| /> |
There was a problem hiding this comment.
The secondary window block is rendered based on quotaData.rate_limit?.secondary_window?.used_percent !== undefined, but then directly accesses quotaData.rate_limit.secondary_window... within the block. In strict mode, this pattern usually doesn’t narrow rate_limit/secondary_window for TypeScript and can fail type-checking. Suggest storing const secondary = quotaData.rate_limit?.secondary_window; and using secondary after checking it’s defined.
| import { Badge } from '@/components/ui/badge'; | ||
|
|
||
| function ProgressBar({ percentage, type = 'usage', durationPercentage }: { percentage: number; type?: 'usage' | 'duration'; durationPercentage?: number }) { |
There was a problem hiding this comment.
import { Badge ... } is declared mid-file, and the file currently has several imports that are no longer referenced (useMutation, useQueryClient, checkProviderQuotas, toast). With noUnusedLocals: true this will break the TypeScript build. Please move the Badge import up with the other imports and remove any now-unused imports.
| }; | ||
|
|
||
| const formatTimeToReset = (resetAtOrSeconds?: string | number | null) => { | ||
| if (!resetAtOrSeconds) return ''; |
There was a problem hiding this comment.
formatTimeToReset treats 0 as “missing” because it uses a falsy check (if (!resetAtOrSeconds)). If reset_after_seconds can be 0, the UI will show an empty value instead of something like “0m” / “Reset now”. Use a nullish check (e.g., resetAtOrSeconds == null) so 0 is handled correctly.
| if (!resetAtOrSeconds) return ''; | |
| if (resetAtOrSeconds == null) return ''; |
| {quotaData.windows?.['5h'] && ( | ||
| <div className="space-y-1"> | ||
| <div className="flex justify-between items-center text-xs"> | ||
| <span className="font-medium text-muted-foreground">{t('quota.window.5h')}</span> | ||
| <span className="font-medium text-foreground">{Math.round((quotaData.windows['5h'].utilization || 0) * 100)}%</span> | ||
| </div> | ||
| <ProgressBar percentage={(quotaData.windows['5h'].utilization || 0) * 100} /> |
There was a problem hiding this comment.
In the 5h window block, the condition uses optional chaining (quotaData.windows?.['5h']) but the body accesses quotaData.windows['5h'] directly. In strict mode this typically won’t narrow windows/['5h'] for TypeScript, and can lead to type errors. Consider assigning const w5h = quotaData.windows?.['5h']; and using w5h inside the block.
| {quotaData.rate_limit?.primary_window && ( | ||
| <div className="space-y-2.5"> | ||
| <div className="space-y-1"> | ||
| <div className="flex justify-between items-center text-xs"> | ||
| <span className="font-medium text-muted-foreground">{t('quota.label.primary_window')}</span> | ||
| <span className="font-medium text-foreground">{Math.round(quotaData.rate_limit.primary_window.used_percent || 0)}%</span> | ||
| </div> | ||
| {quotaData.rate_limit?.secondary_window?.limit_window_seconds && ( | ||
| <div className="flex justify-between items-center text-muted-foreground"> | ||
| <span>{t('quota.label.secondary_duration')}</span> | ||
| <span>{formatWindowDuration(quotaData.rate_limit.secondary_window.limit_window_seconds)}</span> | ||
| <ProgressBar | ||
| percentage={quotaData.rate_limit.primary_window.used_percent || 0} | ||
| durationPercentage={quotaData.rate_limit.primary_window.limit_window_seconds ? calcDurationPercent(quotaData.rate_limit.primary_window.limit_window_seconds, quotaData.rate_limit.primary_window.reset_after_seconds) : undefined} | ||
| /> |
There was a problem hiding this comment.
In the primary window section, the guard is quotaData.rate_limit?.primary_window && (...) but inside the block the code accesses quotaData.rate_limit.primary_window... without optional chaining. With strict: true, optional chaining in the condition generally doesn’t narrow rate_limit for the body, so this can cause TS errors. Prefer capturing const primary = quotaData.rate_limit?.primary_window; and using primary throughout the block.
|
木有 claude 账号。 |
|
还有问题吗,没有我就准备合并了 |
|
没问题了 |




Uh oh!
There was an error while loading. Please reload this page.