mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(ui): keep the plan-usage chip's 5h slot when no session window is open
The header chip silently shrank from "5h 4% · 7d 52%" to a lone "7d 52%", which reads as half the feature breaking rather than as an idle window. Nothing was broken. Claude Code documents `rate_limits.five_hour` as "present only while the API reports it and its resets_at has not passed", so between 5-hour session windows the key simply leaves the statusline payload. Codeman's snapshot replaces the Claude half wholesale on every sample, so the segment disappeared until usage opened a new window. Confirmed against a live 2.1.252 session by capturing real statusline payloads on an isolated tmux socket: the boot render carries no `rate_limits` at all, and the first post-response render carries both windows. The slot now stays, with a dimmed em dash. Claude only: a missing CODEX bucket means that plan has no such limit rather than an idle window, so those stay omitted (pinned by the existing test). The placeholder can never stand alone either — hasWindows() still gates the row, so a provider reporting nothing renders nothing rather than a row of dashes. The tooltip says "5-hour limit: no active session window" instead of dropping the line. Verified in a real browser against a dev instance: the idle chip renders "5h — · 7d 52%" with the dash at opacity 0.55 in --text-dim while the live value keeps its green, and the chip holds its shape (100px idle vs 107px with both windows).
This commit is contained in:
+25
-7
@@ -2634,8 +2634,19 @@ class CodemanApp {
|
||||
// string field (e.g. modelDisplayName, which the route also broadcasts) is
|
||||
// ever shown in this chip, render it via textContent — never interpolate an
|
||||
// untrusted string into this template.
|
||||
const seg = (label, p) => {
|
||||
if (p === null) return '';
|
||||
// `idle: true` keeps a missing window's SLOT with a dimmed em dash instead of
|
||||
// dropping it. Claude only: Claude Code documents `five_hour` as "present
|
||||
// only while the API reports it and its resets_at has not passed", so that
|
||||
// key leaves the statusline payload whenever no 5-hour session window is
|
||||
// open, and a chip that silently shrank from two windows to one read as a
|
||||
// broken feature rather than as an idle window (reported 2026-09-01). A
|
||||
// missing CODEX bucket means the opposite — that plan has no such limit —
|
||||
// so those stay omitted rather than showing a dash forever.
|
||||
const seg = (label, p, idle) => {
|
||||
if (p === null) {
|
||||
if (!idle) return '';
|
||||
return `<span class="pu-win pu-win-idle"><span class="pu-label">${label}</span><span class="pu-val">—</span></span>`;
|
||||
}
|
||||
const n = Math.round(Number(p));
|
||||
if (!Number.isFinite(n)) return '';
|
||||
return `<span class="pu-win"><span class="pu-label">${label}</span><span class="pu-val ${colorClass(n)}">${n}%</span></span>`;
|
||||
@@ -2644,24 +2655,31 @@ class CodemanApp {
|
||||
// provider to tell apart: a machine with Claude alone shows bare windows.
|
||||
const hasWindows = (usage) => pct(usage?.fiveHour) !== null || pct(usage?.sevenDay) !== null;
|
||||
const labelled = hasWindows(data) && hasWindows(data.codex);
|
||||
const row = (provider, usage) => {
|
||||
const windows = [seg('5h', pct(usage?.fiveHour)), seg('7d', pct(usage?.sevenDay))].filter(Boolean);
|
||||
const row = (provider, usage, idle) => {
|
||||
// hasWindows() gates the row, so a placeholder can only ever appear
|
||||
// ALONGSIDE a real reading — a provider reporting nothing still renders
|
||||
// nothing, never a row of em dashes.
|
||||
if (!hasWindows(usage)) return '';
|
||||
const windows = [seg('5h', pct(usage?.fiveHour), idle), seg('7d', pct(usage?.sevenDay), idle)].filter(Boolean);
|
||||
if (!windows.length) return '';
|
||||
const label = labelled ? `<span class="pu-provider">${provider}</span>` : '';
|
||||
return `<span class="pu-row">${label}<span class="pu-windows">${windows.join('<span class="pu-sep">·</span>')}</span></span>`;
|
||||
};
|
||||
const rows = [row('Claude', data), row('Codex', data.codex)].filter(Boolean);
|
||||
const rows = [row('Claude', data, true), row('Codex', data.codex, false)].filter(Boolean);
|
||||
chip.innerHTML = rows.length ? rows.join('') : '—';
|
||||
const resetStr = (w) => (w && w.resetAt ? new Date(w.resetAt).toLocaleString() : '—');
|
||||
const details = (provider, usage) => {
|
||||
const details = (provider, usage, idle) => {
|
||||
const lines = [];
|
||||
const five = pct(usage?.fiveHour);
|
||||
const seven = pct(usage?.sevenDay);
|
||||
if (five !== null) lines.push(`5-hour limit: ${five}% used (resets ${resetStr(usage.fiveHour)})`);
|
||||
else if (idle && seven !== null) lines.push('5-hour limit: no active session window');
|
||||
if (seven !== null) lines.push(`Weekly limit: ${seven}% used (resets ${resetStr(usage.sevenDay)})`);
|
||||
return lines.length ? `${provider} plan usage\n${lines.join('\n')}` : '';
|
||||
};
|
||||
chip.title = [details('Claude', data), details('Codex', data.codex)].filter(Boolean).join('\n\n') || 'Plan usage limits';
|
||||
chip.title =
|
||||
[details('Claude', data, true), details('Codex', data.codex, false)].filter(Boolean).join('\n\n') ||
|
||||
'Plan usage limits';
|
||||
}
|
||||
|
||||
// Scheduled runs
|
||||
|
||||
@@ -12470,6 +12470,13 @@ kbd {
|
||||
color: var(--text-dim);
|
||||
opacity: 0.45;
|
||||
}
|
||||
/* A window Claude is not currently reporting: the slot stays, dimmed, so the
|
||||
chip keeps its shape instead of looking like half of it broke. */
|
||||
.header-plan-usage .pu-win-idle .pu-label,
|
||||
.header-plan-usage .pu-win-idle .pu-val {
|
||||
color: var(--text-dim);
|
||||
opacity: 0.55;
|
||||
}
|
||||
/* Green/yellow/red by how much of the window is used up. */
|
||||
.header-plan-usage .pu-green {
|
||||
color: #3fb950;
|
||||
|
||||
@@ -80,6 +80,34 @@ describe('header plan usage chip', () => {
|
||||
expect(codexRow).toContain('7d');
|
||||
});
|
||||
|
||||
it("keeps Claude's 5h slot as a dash when no session window is open", () => {
|
||||
// Claude Code ships `five_hour` "only while the API reports it and its
|
||||
// resets_at has not passed", so between session windows the key is simply
|
||||
// absent. The chip used to shrink to a lone 7d segment, which reads as a
|
||||
// broken feature rather than an idle window (reported 2026-09-01).
|
||||
const { CodemanApp, chip } = loadCodemanAppClass();
|
||||
const app = Object.create((CodemanApp as { prototype: object }).prototype) as UsageApp;
|
||||
|
||||
app.updatePlanUsageChip({ sevenDay: { usedPercentage: 52, resetAt: 2000 } });
|
||||
|
||||
expect(chip.innerHTML).toContain('pu-win-idle');
|
||||
expect(chip.innerHTML).toContain('5h');
|
||||
expect(chip.innerHTML).toContain('52%');
|
||||
expect(chip.title).toContain('no active session window');
|
||||
});
|
||||
|
||||
it('renders no row at all for a provider reporting nothing', () => {
|
||||
// The placeholder must never stand alone: a row of em dashes would claim a
|
||||
// provider is idle when it is really absent.
|
||||
const { CodemanApp, chip } = loadCodemanAppClass();
|
||||
const app = Object.create((CodemanApp as { prototype: object }).prototype) as UsageApp;
|
||||
|
||||
app.updatePlanUsageChip({ codex: { sevenDay: { usedPercentage: 40, resetAt: 3000 } } });
|
||||
|
||||
expect(chip.innerHTML).not.toContain('pu-win-idle');
|
||||
expect(chip.innerHTML).toContain('40%');
|
||||
});
|
||||
|
||||
it('drops the provider label when Claude is the only provider with limits', () => {
|
||||
const { CodemanApp, chip } = loadCodemanAppClass();
|
||||
const app = Object.create((CodemanApp as { prototype: object }).prototype) as UsageApp;
|
||||
|
||||
Reference in New Issue
Block a user