Skip to content

fix(ui): add missing field in secondary window#1457

Merged
looplj merged 5 commits into
looplj:unstablefrom
LazuliKao:fix/missing_secondary_window
Apr 24, 2026
Merged

fix(ui): add missing field in secondary window#1457
looplj merged 5 commits into
looplj:unstablefrom
LazuliKao:fix/missing_secondary_window

Conversation

@LazuliKao

@LazuliKao LazuliKao commented Apr 23, 2026

Copy link
Copy Markdown
Contributor
Before After(1 commit) Now
image image image
  • Duration Bar(时长进度): 采用灰色,它展示的是当前时间窗口已经流逝的时间比例。计算公式为:(总时长 - 剩余重置时长) / 总时长。
  • Usage Bar(用量进度): 展示当前配额被消耗的百分比。进度条随着用量变化,配合时间加权上色。

用量进度条的颜色并非单纯由“用量高低”决定,而是会参考“时间的流逝”(即比较 Usage 与 Duration),核心逻辑如下:

  • 安全豁免: 如果当前时间窗口才刚刚开始(比如刚过了 5% 的时间),但你的用量已经飙升到了 30%,算法会判定当前消耗过快,颜色会提前向黄色或红色偏移。相反,如果时间已经过去了 90%,你的用量是 80%,算法会认为消耗非常健康,依然保持相对安全的颜色。

Copilot AI review requested due to automatic review settings April 23, 2026 08:54
@greptile-apps

greptile-apps Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes missing reset_after_seconds fields in both the primary_window and secondary_window type definitions and adds a substantial UI overhaul: a time-weighted ProgressBar component with HSL gradient coloring, separate duration bars per quota window, and a revised layout using Badge components. The secondary-window display now correctly shows duration and reset-time rows that were previously missing.

Confidence Score: 5/5

Safe 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 reset_after_seconds) is straightforward. The one open issue — three hardcoded labels that should use t() — is a non-blocking style nit.

quota-badges.tsx — three hardcoded English labels (5h duration, 7d duration, Overage window) should be moved to locale files.

Important Files Changed

Filename Overview
frontend/src/components/quota-badges.tsx Significant refactor of quota display: adds ProgressBar component with time-weighted coloring, adds reset_after_seconds field to both window types (the core fix), and revamps the claudecode/codex layout. Three hardcoded English labels bypass i18n.
frontend/src/locales/en/system.json Adds new i18n keys for time units, reset labels, and window limiters; removes deprecated keys. No issues found.
frontend/src/locales/zh-CN/system.json Mirrors en locale changes with Chinese translations. The abbreviated time keys (d/h/m) use full Chinese words with leading spaces, which is correct for CJK formatting.

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
Loading

Reviews (3): Last reviewed commit: "fix: claude window" | Re-trigger Greptile

Comment thread frontend/src/components/quota-badges.tsx Outdated
Comment thread frontend/src/components/quota-badges.tsx Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 include reset_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.

Comment on lines +110 to +116
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 {

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
const reset = new Date(resetAt).getTime();
const diffMs = reset - now;
const diffMs = resetTimeMs - now;
if (diffMs < 0) return 'Reset now';

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if (diffMs < 0) return 'Reset now';
if (diffMs < 0) return t('quota.resetNow', { defaultValue: 'Reset now' });

Copilot uses AI. Check for mistakes.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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' });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
return format(date, 'HH:mm');

Comment on lines +211 to +218
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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>
            )}

Comment on lines +238 to +245
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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>
                )}

@LazuliKao LazuliKao marked this pull request as draft April 23, 2026 08:59
@LazuliKao

LazuliKao commented Apr 23, 2026

Copy link
Copy Markdown
Contributor Author

改进了一下,是否有必要提交上来 @looplj
image

image image

中文状态:
Image_1776940837070_275.png

@looplj

looplj commented Apr 23, 2026

Copy link
Copy Markdown
Owner

可以啊,很不错

@LazuliKao

Copy link
Copy Markdown
Contributor Author

我这里只测试了codex,希望可以确认下claude额度是否正常显示。

@LazuliKao LazuliKao marked this pull request as ready for review April 24, 2026 02:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +291 to +301
{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}
/>

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +82
import { Badge } from '@/components/ui/badge';

function ProgressBar({ percentage, type = 'usage', durationPercentage }: { percentage: number; type?: 'usage' | 'duration'; durationPercentage?: number }) {

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
};

const formatTimeToReset = (resetAtOrSeconds?: string | number | null) => {
if (!resetAtOrSeconds) return '';

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if (!resetAtOrSeconds) return '';
if (resetAtOrSeconds == null) return '';

Copilot uses AI. Check for mistakes.
Comment on lines +223 to +229
{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} />

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +255 to +265
{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}
/>

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@looplj

looplj commented Apr 24, 2026

Copy link
Copy Markdown
Owner

木有 claude 账号。
不过应该问题不大,应该是复用的吧。

@looplj

looplj commented Apr 24, 2026

Copy link
Copy Markdown
Owner

还有问题吗,没有我就准备合并了

@LazuliKao

Copy link
Copy Markdown
Contributor Author

没问题了

@looplj looplj merged commit b4ba99c into looplj:unstable Apr 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants