Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ ccstatusline --version
- **Input Speed** / **Output Speed** / **Total Speed** - Show session-average token throughput with an optional per-widget rolling window (`0-120` seconds; `0` = full-session average).
- **Context Length** / **Context Window** / **Context %** / **Context % (usable)** / **Context Bar** - Show current context length, total context window size, used/remaining percentage, usable-window percentage, or a progress bar. The window size is taken from Claude Code's reported `context_window.context_window_size` when present, then from a model-name hint (e.g. a `[1m]` suffix), and finally from a fixed fallback. Set `CCSTATUSLINE_CONTEXT_SIZE_FALLBACK` to a positive integer to override that last-resort fallback (defaults to `200000`) — useful when an older Claude Code does not report the window size for a 1M-context model, so the bar would otherwise read against 200k. Immediately after `/compact`, transcript fallback uses the latest `compact_boundary.postTokens` value until a new main-chain turn reports the current size, so the widgets do not retain the pre-compaction context.
- **Compaction Counter** - Show how many context compactions have been detected in the current session by scanning transcript compaction markers. It can render as icon plus number, text plus number, or number-only, and can hide while the count is zero. Two optional, independent per-item add-ons toggle extra detail: a trigger split (`↻ 3 (2 auto, 1 manual)`; a compaction whose trigger is missing or unrecognized is bucketed as `unknown`) and tokens reclaimed (`↻ 3 ↓887.0k`, each compaction's `preTokens - postTokens` floored at 0 and summed, shown only when greater than 0 — so very old transcripts predating the `postTokens` field display nothing). Its value selector can instead render the total count, one trigger count (`auto`, `manual`, or `unknown`), or reclaimed tokens as a standalone value; hide-when-zero applies to the selected value.
- **Session Usage** / **Weekly Usage** / **Weekly Sonnet Usage** / **Weekly Opus Usage** / **Weekly Fable Usage** / **Extra Usage Utilization** / **Extra Usage Remaining** / **Extra Usage Used** / **Block Timer** / **Block Reset Timer** / **Weekly Reset Timer** - Show usage percentages, monthly pay-as-you-go overage usage, and current block/reset timing. Session and all-model weekly usage read the legacy flat API buckets when available and fall back per field to `session` and `weekly_all` entries in the newer `limits[]` response. The per-model widgets prefer `weekly_scoped` entries for Sonnet, Opus, and Fable, with legacy Sonnet/Opus buckets as fallbacks; Weekly Fable Usage renders nothing when the API reports no Fable quota. Session Usage, the weekly percentage widgets, and Extra Usage Utilization can show either used or remaining percentage in every display mode. Session and weekly usage bars can also show a time cursor. Extra usage widgets accept known extra-usage state as complete when an account has no monthly limit configured, avoid repeated refetches and stale `[Timeout]` output, and format amounts with the API-reported billing currency when available. On accounts with no plan limits (usage-based Enterprise plans), extra usage is all of the account's spend rather than overage beyond a plan; the label editor can change the Extra Usage widgets' `Overage` labels to read `Spend`. Reset timers can show remaining time, progress, or exact reset date/time with timezone and locale controls; while reset data is still arriving at startup, they show a labeled loading placeholder instead of a transient API error.
- **Session Usage** / **Weekly Usage** / **Weekly Sonnet Usage** / **Weekly Opus Usage** / **Weekly Fable Usage** / **Extra Usage Utilization** / **Extra Usage Remaining** / **Extra Usage Used** / **Block Timer** / **Block Reset Timer** / **Weekly Reset Timer** - Show usage percentages, monthly pay-as-you-go overage usage, and current block/reset timing. Session and all-model weekly usage read the legacy flat API buckets when available and fall back per field to `session` and `weekly_all` entries in the newer `limits[]` response. The per-model widgets prefer `weekly_scoped` entries for Sonnet, Opus, and Fable, with legacy Sonnet/Opus buckets as fallbacks; Weekly Fable Usage renders nothing when the API reports no Fable quota. Session Usage, the weekly percentage widgets, and Extra Usage Utilization can show either used or remaining percentage in every display mode. Session and weekly usage bars can also show a time cursor. Extra usage widgets accept known extra-usage state as complete when an account has no monthly limit configured, avoid repeated refetches and stale `[Timeout]` output, and format amounts with the API-reported billing currency when available. On accounts with no plan limits (usage-based Enterprise plans), extra usage is all of the account's spend rather than overage beyond a plan; the label editor can change the Extra Usage widgets' `Overage` labels to read `Spend`. Before the first charge of the month the API reports no utilization, so Extra Usage Utilization works it out from the amount spent and the monthly limit. Reset timers can show remaining time, progress, or exact reset date/time with timezone and locale controls; while reset data is still arriving at startup, they show a labeled loading placeholder instead of a transient API error.

An explicit per-model quota at 0% is shown even when its reset timestamp is still missing; the time cursor appears only once a usable window is available. Weekly Fable Usage uses the label `Weekly Fable:` and stays empty when the API reports no Fable quota.

Expand Down
23 changes: 19 additions & 4 deletions src/widgets/ExtraUsageUtilization.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { RenderContext } from '../types/RenderContext';
import type {
RenderContext,
RenderUsageData
} from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
CustomKeybind,
Expand Down Expand Up @@ -31,6 +34,17 @@ import {
toggleUsageInverted
} from './shared/usage-display';

// The usage API reports `utilization: null` until the first charge of the month,
// while still reporting the amount spent and the monthly limit (both in cents).
function getExtraUsageUtilization(data: RenderUsageData): number | undefined {
if (data.extraUsageUtilization !== undefined) {
return data.extraUsageUtilization;
}
if (data.extraUsageUsed === undefined || data.extraUsageLimit === undefined || data.extraUsageLimit <= 0) {
return undefined;
}
return data.extraUsageUsed / data.extraUsageLimit * 100;
}
const LABEL = 'Overage: ';

export class ExtraUsageUtilizationWidget implements Widget {
Expand Down Expand Up @@ -93,7 +107,8 @@ export class ExtraUsageUtilizationWidget implements Widget {
? null
: formatRawOrLabeledValue(item, this.getLabelPrefix(), 'n/a');
}
if (data.extraUsageEnabled !== true || data.extraUsageUtilization === undefined) {
const utilization = getExtraUsageUtilization(data);
if (data.extraUsageEnabled !== true || utilization === undefined) {
if (data.error) {
return isHidden(item, USAGE_NO_DATA_HIDEABLE_STATE.key)
? null
Expand All @@ -102,8 +117,8 @@ export class ExtraUsageUtilizationWidget implements Widget {
return null;
}

// extraUsageUtilization is already a percentage (0-100), not a fraction
const percent = Math.max(0, Math.min(100, data.extraUsageUtilization));
// utilization is a percentage (0-100), not a fraction
const percent = Math.max(0, Math.min(100, utilization));
const renderedPercent = inverted ? 100 - percent : percent;

if (isUsageProgressMode(displayMode)) {
Expand Down
54 changes: 54 additions & 0 deletions src/widgets/__tests__/ExtraUsageUtilization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,60 @@ describe('ExtraUsageUtilizationWidget', () => {
})).toBe('Overage: 2.6%');
});

// The usage API reports `utilization: null` until the first charge of the
// month, while still reporting the amount spent and the monthly limit.
it('derives utilization from spent and limit when the API reports none', () => {
const widget = new ExtraUsageUtilizationWidget();
const item: WidgetItem = { id: 'extra', type: 'extra-usage-utilization' };

expect(render(widget, item, {
usageData: {
extraUsageEnabled: true,
extraUsageLimit: 5000,
extraUsageUsed: 0
}
})).toBe('Overage: 0.0%');
expect(render(widget, item, {
usageData: {
extraUsageEnabled: true,
extraUsageLimit: 5000,
extraUsageUsed: 1250
}
})).toBe('Overage: 25.0%');
});

it('prefers the API-reported utilization over the derived one', () => {
const widget = new ExtraUsageUtilizationWidget();

expect(render(widget, { id: 'extra', type: 'extra-usage-utilization' }, {
usageData: {
extraUsageEnabled: true,
extraUsageLimit: 5000,
extraUsageUsed: 1250,
extraUsageUtilization: 40
}
})).toBe('Overage: 40.0%');
});

it('renders nothing without a usable monthly limit to derive utilization from', () => {
const widget = new ExtraUsageUtilizationWidget();
const item: WidgetItem = { id: 'extra', type: 'extra-usage-utilization' };

expect(render(widget, item, {
usageData: {
extraUsageEnabled: true,
extraUsageUsed: 1250
}
})).toBeNull();
expect(render(widget, item, {
usageData: {
extraUsageEnabled: true,
extraUsageLimit: 0,
extraUsageUsed: 0
}
})).toBeNull();
});

it('declares the disabled and no-data hideable states alongside display keybinds', () => {
const widget = new ExtraUsageUtilizationWidget();
const baseItem: WidgetItem = { id: 'extra', type: 'extra-usage-utilization' };
Expand Down
Loading