Improve usage limit display

This commit is contained in:
murashit 2026-06-04 12:15:31 +09:00
parent d06e329a78
commit 7f4387706b
7 changed files with 82 additions and 19 deletions

View file

@ -215,7 +215,15 @@ function RateLimitPanel({ rateLimit }: { rateLimit: RateLimitSummary | null }):
>
<div className="codex-panel__limit-panel-label">{row.label}</div>
<div className="codex-panel__limit-panel-value">{row.value}</div>
<div className="codex-panel__limit-panel-meter">
<div
className={[
"codex-panel__limit-panel-meter",
row.meterDivisions ? "codex-panel__limit-panel-meter--divided" : "",
row.meterDivisions ? `codex-panel__limit-panel-meter--${String(row.meterDivisions)}` : "",
]
.filter(Boolean)
.join(" ")}
>
<div className="codex-panel__limit-panel-fill" style={{ width: `${String(row.percent)}%` }} />
</div>
<div className="codex-panel__limit-panel-reset">{row.resetLabel ?? ""}</div>

View file

@ -35,6 +35,7 @@ export interface RateLimitSummaryRow {
resetLabel: string | null;
title: string;
percent: number;
meterDivisions: number | null;
level: "ok" | "warn" | "danger";
}
@ -228,6 +229,7 @@ function rateLimitWindowSummary(
resetLabel,
title: `${name} ${label}: ${String(percent)}% used.${resetText}${reachedText}`,
percent,
meterDivisions: rateLimitMeterDivisions(window.windowDurationMins),
level,
};
}
@ -247,10 +249,17 @@ function spendControlLimitSummary(limit: SpendControlLimitSnapshot | null, name:
resetLabel,
title: `${name} monthly limit: ${value} used, ${String(remainingPercent)}% remaining.${resetText}`,
percent,
meterDivisions: null,
level,
};
}
function rateLimitMeterDivisions(minutes: number | null): number | null {
if (minutes === 300) return 5;
if (minutes === 10_080) return 7;
return null;
}
function formatRateLimitDuration(minutes: number): string {
if (minutes === 10_080) return "1w";
if (minutes % 60 === 0) return `${String(minutes / 60)}h`;

View file

@ -116,19 +116,15 @@
}
.codex-panel__limit-panel-list {
display: flex;
flex-direction: column;
gap: var(--codex-panel-panel-gap);
display: grid;
grid-template-columns: max-content max-content minmax(0, 1fr) max-content;
gap: var(--codex-panel-panel-gap) var(--codex-panel-section-gap);
cursor: text;
user-select: text;
}
.codex-panel__limit-panel-row {
display: grid;
grid-template-columns: 2ch 4ch minmax(64px, 1fr) max-content;
gap: var(--codex-panel-control-gap) var(--codex-panel-item-gap);
align-items: center;
padding: var(--codex-panel-control-gap) var(--codex-panel-section-gap);
display: contents;
}
.codex-panel__limit-panel-label,
@ -142,22 +138,53 @@
user-select: text;
}
.codex-panel__limit-panel-label {
padding: var(--codex-panel-control-gap) 0 var(--codex-panel-control-gap) var(--codex-panel-section-gap);
}
.codex-panel__limit-panel-value {
padding: var(--codex-panel-control-gap) 0;
font-variant-numeric: tabular-nums;
}
.codex-panel__limit-panel-reset {
padding: var(--codex-panel-control-gap) var(--codex-panel-section-gap) var(--codex-panel-control-gap) 0;
color: var(--codex-panel-text-faint);
}
.codex-panel__limit-panel-meter {
align-self: center;
position: relative;
height: var(--codex-panel-meter-bar-height);
margin: 0 var(--codex-panel-panel-gap);
margin: 0;
overflow: hidden;
border-radius: var(--codex-panel-radius-pill);
background: var(--codex-panel-border-color);
}
.codex-panel__limit-panel-meter--divided::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background: repeating-linear-gradient(
to right,
transparent 0,
transparent calc((100% / var(--codex-panel-limit-panel-divisions)) - 1px),
color-mix(in srgb, var(--codex-panel-surface) 58%, transparent) calc((100% / var(--codex-panel-limit-panel-divisions)) - 1px),
color-mix(in srgb, var(--codex-panel-surface) 58%, transparent) calc(100% / var(--codex-panel-limit-panel-divisions))
);
}
.codex-panel__limit-panel-meter--5 {
--codex-panel-limit-panel-divisions: 5;
}
.codex-panel__limit-panel-meter--7 {
--codex-panel-limit-panel-divisions: 7;
}
.codex-panel__limit-panel-fill {
display: block;
height: 100%;

View file

@ -17,8 +17,16 @@ describe("status line helpers", () => {
title: "Codex usage limits",
level: "warn",
rows: [
{ label: "5h", value: "72%", percent: 72, level: "warn", title: "5h usage", resetLabel: "reset in 2h" },
{ label: "1w", value: "15%", percent: 15, level: "ok", title: "1w usage", resetLabel: null },
{
label: "5h",
value: "72%",
percent: 72,
meterDivisions: 5,
level: "warn",
title: "5h usage",
resetLabel: "reset in 2h",
},
{ label: "1w", value: "15%", percent: 15, meterDivisions: 7, level: "ok", title: "1w usage", resetLabel: null },
],
};

View file

@ -114,6 +114,7 @@ describe("toolbar renderer decisions", () => {
resetLabel: "reset in 2h",
title: "Codex 5h: 42% used.",
percent: 42,
meterDivisions: 5,
level: "ok",
},
{
@ -122,6 +123,7 @@ describe("toolbar renderer decisions", () => {
resetLabel: "reset in 3d 4h",
title: "Codex 1w: 21% used.",
percent: 21,
meterDivisions: 7,
level: "ok",
},
],
@ -138,6 +140,10 @@ describe("toolbar renderer decisions", () => {
expect(parent.querySelector(".codex-panel__limit-panel")?.textContent).toContain("1w");
expect(parent.querySelector(".codex-panel__limit-panel")?.textContent).toContain("21%");
expect(parent.querySelector(".codex-panel__limit-panel")?.textContent).toContain("reset in 3d 4h");
const meters = [...parent.querySelectorAll<HTMLElement>(".codex-panel__limit-panel-meter")];
expect(meters.map((meter) => meter.classList.contains("codex-panel__limit-panel-meter--divided"))).toEqual([true, true]);
expect(meters[0]?.classList.contains("codex-panel__limit-panel-meter--5")).toBe(true);
expect(meters[1]?.classList.contains("codex-panel__limit-panel-meter--7")).toBe(true);
});
it("renders connection diagnostics in the status menu", () => {

View file

@ -573,7 +573,7 @@ describe("runtime settings", () => {
1_799_991_600_000,
),
).toMatchObject({
rows: [{ label: "5h", value: "72%", resetLabel: "reset in 2h 20m", percent: 72 }],
rows: [{ label: "5h", value: "72%", resetLabel: "reset in 2h 20m", percent: 72, meterDivisions: 5 }],
level: "warn",
});
@ -614,8 +614,8 @@ describe("runtime settings", () => {
),
).toMatchObject({
rows: [
{ label: "5h", value: "15%" },
{ label: "1w", value: "38%" },
{ label: "5h", value: "15%", meterDivisions: 5 },
{ label: "1w", value: "38%", meterDivisions: 7 },
],
level: "ok",
});
@ -657,7 +657,7 @@ describe("runtime settings", () => {
1_799_991_600_000,
),
).toMatchObject({
rows: [{ label: "monthly", value: "$72 / $100", resetLabel: "reset in 2h 20m", percent: 72 }],
rows: [{ label: "monthly", value: "$72 / $100", resetLabel: "reset in 2h 20m", percent: 72, meterDivisions: null }],
level: "warn",
});
});

View file

@ -130,12 +130,17 @@ describe("chat toolbar CSS", () => {
expect(threadsRenameInput).not.toContain("appearance: none");
});
it("keeps usage limit meter columns aligned across percent widths", () => {
it("lets the usage limit meter absorb panel width changes", () => {
const limitList = /\.codex-panel__limit-panel-list \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const limitRow = /\.codex-panel__limit-panel-row \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const limitMeter = /\.codex-panel__limit-panel-meter \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const limitValue = /\.codex-panel__limit-panel-value \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
expect(limitRow).toContain("grid-template-columns: 2ch 4ch minmax(64px, 1fr) max-content");
expect(limitRow).toContain("gap: var(--codex-panel-control-gap) var(--codex-panel-item-gap)");
expect(limitList).toContain("display: grid");
expect(limitList).toContain("grid-template-columns: max-content max-content minmax(0, 1fr) max-content");
expect(limitList).toContain("gap: var(--codex-panel-panel-gap) var(--codex-panel-section-gap)");
expect(limitRow).toContain("display: contents");
expect(limitMeter).toContain("margin: 0");
expect(limitValue).toContain("font-variant-numeric: tabular-nums");
expect(limitValue).not.toContain("text-align: right");
});