Skip to content
Open
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
6 changes: 3 additions & 3 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,13 +337,13 @@ Supported states by widget family:
- **Session Cost**: `zero` hides `$0.00`
- **Session Clock**: `zero` hides durations under one minute
- **Block Timer**: `no-data` hides the `0hr 0m` / empty-bar display when no block is active
- **Block Reset Timer / Weekly Reset Timer**: `no-data` hides both the `[Loading]` placeholder and the usage-error placeholders while no reset window is available
- **Block Reset Timer / Weekly Reset Timer**: `no-data` hides both the `[Loading]` placeholder and the usage-error placeholders while no reset window is available, `zero` hides the timer while its period's usage (session or weekly) is 0% and no reset window has started
- **Input/Output/Total Speed**: `no-data` hides the `—` placeholder when no speed data exists
- **Output Style**: `default-value` hides the widget when the style is `default`
- **Compaction Counter**: `zero` hides the counter before any compaction occurs
- **Skills**: `empty` hides the widget before any skill is used
- **Extra Usage widgets**: `disabled` hides the `n/a` display when extra usage is off, `no-data` hides the error placeholder when usage data is unavailable
- **Session / Weekly / Weekly Sonnet / Weekly Opus / Weekly Fable Usage**: `no-data` hides the error placeholder (`[No credentials]`, `[Timeout]`, `[Rate limited]`, `[API Error]`, `[Parse Error]`) when usage data is unavailable
- **Extra Usage widgets**: `disabled` hides the `n/a` display when extra usage is off, `no-data` hides the error placeholder when usage data is unavailable; on Extra Usage Used and Extra Usage Utilization, `zero` hides the widget while nothing has been spent (Extra Usage Remaining has no `zero` state)
- **Session / Weekly / Weekly Sonnet / Weekly Opus / Weekly Fable Usage**: `no-data` hides the error placeholder (`[No credentials]`, `[Timeout]`, `[Rate limited]`, `[API Error]`, `[Parse Error]`) when usage data is unavailable, `zero` hides the widget while usage is 0% (also when showing remaining)
- **Cache widgets** (`Cache Hit Rate`, `Cache Read`, `Cache Write`, `Cache Timer`): `empty` hides the widget when there is no cache activity, and on Cache Timer when no cache anchor is available
- **Custom Text / Custom Symbol**: `merge-target-hidden` hides the item when the widget it is merged with renders nothing, so icon prefixes/suffixes disappear together with their widget

Expand Down
6 changes: 4 additions & 2 deletions src/utils/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ const GIT_REVIEW_HIDE_RULE: HideFlagRule = {
// placeholder. hideIfDisabled never covered that placeholder, so it stays mapped
// to 'disabled' alone and only a hand-written list can enable it.
const EXTRA_USAGE_HIDE_RULE: HideFlagRule = { legacy: { hideIfDisabled: ['disabled'] }, stateOrder: ['disabled', 'no-data'] };
// Used and Utilization also offer `zero`, which a hand-written hide list can carry in.
const EXTRA_USAGE_ZERO_HIDE_RULE: HideFlagRule = { legacy: { hideIfDisabled: ['disabled'] }, stateOrder: ['disabled', 'no-data', 'zero'] };
const CACHE_HIDE_RULE: HideFlagRule = { legacy: { hideWhenEmpty: ['empty'] }, stateOrder: ['empty'] };

export const V4_HIDE_FLAG_RULES: Record<string, HideFlagRule> = {
Expand Down Expand Up @@ -209,9 +211,9 @@ export const V4_HIDE_FLAG_RULES: Record<string, HideFlagRule> = {
'cache-write': CACHE_HIDE_RULE,
'cache-hit-rate': CACHE_HIDE_RULE,
'cache-timer': CACHE_HIDE_RULE,
'extra-usage-utilization': EXTRA_USAGE_HIDE_RULE,
'extra-usage-utilization': EXTRA_USAGE_ZERO_HIDE_RULE,
'extra-usage-remaining': EXTRA_USAGE_HIDE_RULE,
'extra-usage-used': EXTRA_USAGE_HIDE_RULE
'extra-usage-used': EXTRA_USAGE_ZERO_HIDE_RULE
};

const V4_LEGACY_HIDE_KEYS = [
Expand Down
7 changes: 6 additions & 1 deletion src/widgets/BlockResetTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from './shared/timezone-editor';
import {
USAGE_NO_DATA_HIDEABLE_STATE,
USAGE_ZERO_HIDEABLE_STATE,
cycleUsageDisplayMode,
getUsageDisplayMode,
getUsageDisplayModifierText,
Expand Down Expand Up @@ -78,7 +79,7 @@ export class BlockResetTimerWidget implements Widget {
}

getHideableStates(): HideableState[] {
return [USAGE_NO_DATA_HIDEABLE_STATE];
return [USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE];
}

handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
Expand Down Expand Up @@ -144,6 +145,10 @@ export class BlockResetTimerWidget implements Widget {
}

const usageData = context.usageData ?? {};
if (usageData.sessionUsage === 0 && !usageData.sessionResetAt && isHidden(item, USAGE_ZERO_HIDEABLE_STATE.key)) {
return null;
}

const window = resolveUsageWindowWithFallback(usageData, context.blockMetrics);

if (!window) {
Expand Down
24 changes: 23 additions & 1 deletion src/widgets/ExtraUsageUsed.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import type { RenderUsageData } from '../types/RenderContext';
import type {
RenderContext,
RenderUsageData
} from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
HideableState,
WidgetItem
} from '../types/Widget';

import { ExtraUsageAmountWidget } from './shared/extra-usage-amount-widget';
import { isHidden } from './shared/hideable';
import { USAGE_ZERO_HIDEABLE_STATE } from './shared/usage-display';

export class ExtraUsageUsedWidget extends ExtraUsageAmountWidget {
protected readonly label = 'Overage Used: ';
Expand All @@ -9,6 +19,18 @@ export class ExtraUsageUsedWidget extends ExtraUsageAmountWidget {
getDescription(): string { return 'Shows extra usage spent: overage beyond Pro/Max plan limits, or your spend on Enterprise'; }
getDisplayName(): string { return 'Extra Usage Used'; }

override getHideableStates(): HideableState[] {
return [...super.getHideableStates(), USAGE_ZERO_HIDEABLE_STATE];
}

override render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
const data = context.usageData;
if (!context.isPreview && data?.extraUsageEnabled === true && data.extraUsageUsed === 0 && isHidden(item, USAGE_ZERO_HIDEABLE_STATE.key)) {
return null;
}
return super.render(item, context, settings);
}

protected getDollars(data: RenderUsageData): number | null {
// extraUsageUsed is in cents
return data.extraUsageUsed === undefined ? null : data.extraUsageUsed / 100;
Expand Down
7 changes: 6 additions & 1 deletion src/widgets/ExtraUsageUtilization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { makeTimerProgressBar } from './shared/progress-bar';
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
import {
USAGE_NO_DATA_HIDEABLE_STATE,
USAGE_ZERO_HIDEABLE_STATE,
cycleUsageDisplayMode,
getUsageDisplayMode,
getUsageDisplayModifierText,
Expand Down Expand Up @@ -62,7 +63,7 @@ export class ExtraUsageUtilizationWidget implements Widget {
}

getHideableStates(): HideableState[] {
return [EXTRA_USAGE_DISABLED_HIDEABLE_STATE, USAGE_NO_DATA_HIDEABLE_STATE];
return [EXTRA_USAGE_DISABLED_HIDEABLE_STATE, USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE];
}

handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
Expand Down Expand Up @@ -119,6 +120,10 @@ export class ExtraUsageUtilizationWidget implements Widget {

// utilization is a percentage (0-100), not a fraction
const percent = Math.max(0, Math.min(100, utilization));
if (percent === 0 && isHidden(item, USAGE_ZERO_HIDEABLE_STATE.key)) {
return null;
}

const renderedPercent = inverted ? 100 - percent : percent;

if (isUsageProgressMode(displayMode)) {
Expand Down
3 changes: 2 additions & 1 deletion src/widgets/FableWeeklyUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {

import {
USAGE_NO_DATA_HIDEABLE_STATE,
USAGE_ZERO_HIDEABLE_STATE,
getUsagePercentCustomKeybinds
} from './shared/usage-display';
import {
Expand All @@ -33,7 +34,7 @@ export class FableWeeklyUsageWidget implements Widget {
}

getHideableStates(): HideableState[] {
return [USAGE_NO_DATA_HIDEABLE_STATE];
return [USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE];
}

handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
Expand Down
3 changes: 2 additions & 1 deletion src/widgets/SessionUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {

import {
USAGE_NO_DATA_HIDEABLE_STATE,
USAGE_ZERO_HIDEABLE_STATE,
getUsagePercentCustomKeybinds
} from './shared/usage-display';
import {
Expand All @@ -33,7 +34,7 @@ export class SessionUsageWidget implements Widget {
}

getHideableStates(): HideableState[] {
return [USAGE_NO_DATA_HIDEABLE_STATE];
return [USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE];
}

handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
Expand Down
3 changes: 2 additions & 1 deletion src/widgets/WeeklyOpusUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {

import {
USAGE_NO_DATA_HIDEABLE_STATE,
USAGE_ZERO_HIDEABLE_STATE,
getUsagePercentCustomKeybinds
} from './shared/usage-display';
import {
Expand All @@ -33,7 +34,7 @@ export class WeeklyOpusUsageWidget implements Widget {
}

getHideableStates(): HideableState[] {
return [USAGE_NO_DATA_HIDEABLE_STATE];
return [USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE];
}

handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
Expand Down
7 changes: 6 additions & 1 deletion src/widgets/WeeklyResetTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
} from './shared/timezone-editor';
import {
USAGE_NO_DATA_HIDEABLE_STATE,
USAGE_ZERO_HIDEABLE_STATE,
cycleUsageDisplayMode,
getUsageDisplayMode,
getUsageLocale,
Expand Down Expand Up @@ -148,7 +149,7 @@ export class WeeklyResetTimerWidget implements Widget {
}

getHideableStates(): HideableState[] {
return [USAGE_NO_DATA_HIDEABLE_STATE];
return [USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE];
}

handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
Expand Down Expand Up @@ -228,6 +229,10 @@ export class WeeklyResetTimerWidget implements Widget {
}

const usageData = context.usageData ?? {};
if (usageData.weeklyUsage === 0 && !usageData.weeklyResetAt && isHidden(item, USAGE_ZERO_HIDEABLE_STATE.key)) {
return null;
}

const window = resolveWeeklyUsageWindow(usageData);

if (!window) {
Expand Down
3 changes: 2 additions & 1 deletion src/widgets/WeeklySonnetUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {

import {
USAGE_NO_DATA_HIDEABLE_STATE,
USAGE_ZERO_HIDEABLE_STATE,
getUsagePercentCustomKeybinds
} from './shared/usage-display';
import {
Expand All @@ -33,7 +34,7 @@ export class WeeklySonnetUsageWidget implements Widget {
}

getHideableStates(): HideableState[] {
return [USAGE_NO_DATA_HIDEABLE_STATE];
return [USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE];
}

handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
Expand Down
3 changes: 2 additions & 1 deletion src/widgets/WeeklyUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {

import {
USAGE_NO_DATA_HIDEABLE_STATE,
USAGE_ZERO_HIDEABLE_STATE,
getUsagePercentCustomKeybinds
} from './shared/usage-display';
import {
Expand All @@ -33,7 +34,7 @@ export class WeeklyUsageWidget implements Widget {
}

getHideableStates(): HideableState[] {
return [USAGE_NO_DATA_HIDEABLE_STATE];
return [USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE];
}

handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
Expand Down
24 changes: 22 additions & 2 deletions src/widgets/__tests__/BlockResetTimer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,28 @@ describe('BlockResetTimerWidget', () => {
expect(render(widget, { id: 'reset', type: 'reset-timer', metadata: { display: 'progress' } }, { usageData: {} })).toBe('Reset [Loading]');
});

it('declares the no-data hideable state', () => {
expect(new BlockResetTimerWidget().getHideableStates().map(state => state.key)).toEqual(['no-data']);
it('declares the no-data and zero hideable states', () => {
expect(new BlockResetTimerWidget().getHideableStates().map(state => state.key)).toEqual(['no-data', 'zero']);
});

it('hides when sessionUsage is zero and the zero state is enabled', () => {
const widget = new BlockResetTimerWidget();
const item: WidgetItem = { id: 'reset', type: 'reset-timer', metadata: { hide: 'zero' } };

mockResolveUsageWindowWithFallback.mockReturnValue({
sessionDurationMs: 18000000,
elapsedMs: 3600000,
remainingMs: 14400000,
elapsedPercent: 20,
remainingPercent: 80
});
mockFormatUsageDuration.mockReturnValue('4hr');

expect(render(widget, item, { usageData: { sessionUsage: 0 } })).toBeNull();
// A null bucket pins usage to 0 while limits[] still supplies the reset time
expect(render(widget, item, { usageData: { sessionUsage: 0, sessionResetAt: '2030-07-01T00:00:00.000Z' } })).toBe('Reset: 4hr');
expect(render(widget, item, { usageData: { sessionUsage: 5 } })).toBe('Reset: 4hr');
expect(render(widget, { id: 'reset', type: 'reset-timer' }, { usageData: { sessionUsage: 0 } })).toBe('Reset: 4hr');
});

// One state covers both placeholders, since either means the same thing to
Expand Down
13 changes: 11 additions & 2 deletions src/widgets/__tests__/ExtraUsageUsed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,22 @@ describe('ExtraUsageUsedWidget', () => {
expect(widget.supportsNumberFormat()).toBe(true);
});

it('declares the disabled and no-data hideable states', () => {
it('declares the disabled, no-data and zero hideable states', () => {
const widget = new ExtraUsageUsedWidget();

expect(widget.getHideableStates().map(state => state.key)).toEqual(['disabled', 'no-data']);
expect(widget.getHideableStates().map(state => state.key)).toEqual(['disabled', 'no-data', 'zero']);
expect(widget.getEditorDisplay({ id: 'extra', type: 'extra-usage-used' }).modifierText).toBeUndefined();
});

it('hides zero spend only when the zero state is enabled', () => {
const widget = new ExtraUsageUsedWidget();
const hideZeroItem: WidgetItem = { id: 'extra', type: 'extra-usage-used', metadata: { hide: 'zero' } };

expect(render(widget, hideZeroItem, { usageData: { extraUsageEnabled: true, extraUsageUsed: 0 } })).toBeNull();
expect(render(widget, hideZeroItem, { usageData: { extraUsageEnabled: true, extraUsageUsed: 10600 } })).toBe('Overage Used: $106.00');
expect(render(widget, { id: 'extra', type: 'extra-usage-used' }, { usageData: { extraUsageEnabled: true, extraUsageUsed: 0 } })).toBe('Overage Used: $0.00');
});

it('renders available used budget before unrelated usage errors', () => {
const widget = new ExtraUsageUsedWidget();

Expand Down
17 changes: 16 additions & 1 deletion src/widgets/__tests__/ExtraUsageUtilization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,22 @@ describe('ExtraUsageUtilizationWidget', () => {
metadata: { invert: 'true' }
}).modifierText).toBe('(remaining)');

expect(widget.getHideableStates().map(state => state.key)).toEqual(['disabled', 'no-data']);
expect(widget.getHideableStates().map(state => state.key)).toEqual(['disabled', 'no-data', 'zero']);
});

it('hides zero utilization only when the zero state is enabled, regardless of invert', () => {
const widget = new ExtraUsageUtilizationWidget();
const zeroContext: RenderContext = { usageData: { extraUsageEnabled: true, extraUsageUtilization: 0 } };

expect(render(widget, { id: 'extra', type: 'extra-usage-utilization', metadata: { hide: 'zero' } }, zeroContext)).toBeNull();
expect(render(widget, { id: 'extra', type: 'extra-usage-utilization', metadata: { hide: 'zero', invert: 'true' } }, zeroContext)).toBeNull();
const nonZeroContext: RenderContext = { usageData: { extraUsageEnabled: true, extraUsageUtilization: 25 } };
expect(render(widget, { id: 'extra', type: 'extra-usage-utilization', metadata: { hide: 'zero' } }, nonZeroContext)).toBe('Overage: 25.0%');
expect(render(widget, { id: 'extra', type: 'extra-usage-utilization' }, zeroContext)).toBe('Overage: 0.0%');

const derivedZeroContext: RenderContext = { usageData: { extraUsageEnabled: true, extraUsageUsed: 0, extraUsageLimit: 5000 } };
expect(render(widget, { id: 'extra', type: 'extra-usage-utilization', metadata: { hide: 'zero' } }, derivedZeroContext)).toBeNull();
expect(render(widget, { id: 'extra', type: 'extra-usage-utilization' }, derivedZeroContext)).toBe('Overage: 0.0%');
});

it('does not show a time cursor carried over from a usage widget', () => {
Expand Down
24 changes: 22 additions & 2 deletions src/widgets/__tests__/WeeklyResetTimer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,28 @@ describe('WeeklyResetTimerWidget', () => {
expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer', metadata: { display: 'progress' } }, { usageData: {} })).toBe('Weekly Reset [Loading]');
});

it('declares the no-data hideable state', () => {
expect(new WeeklyResetTimerWidget().getHideableStates().map(state => state.key)).toEqual(['no-data']);
it('declares the no-data and zero hideable states', () => {
expect(new WeeklyResetTimerWidget().getHideableStates().map(state => state.key)).toEqual(['no-data', 'zero']);
});

it('hides when weeklyUsage is zero and the zero state is enabled', () => {
const widget = new WeeklyResetTimerWidget();
const item: WidgetItem = { id: 'weekly-reset', type: 'weekly-reset-timer', metadata: { hide: 'zero' } };

mockResolveWeeklyUsageWindow.mockReturnValue({
sessionDurationMs: 18000000,
elapsedMs: 3600000,
remainingMs: 14400000,
elapsedPercent: 20,
remainingPercent: 80
});
mockFormatUsageDuration.mockReturnValue('4hr');

expect(render(widget, item, { usageData: { weeklyUsage: 0 } })).toBeNull();
// A null bucket pins usage to 0 while limits[] still supplies the reset time
expect(render(widget, item, { usageData: { weeklyUsage: 0, weeklyResetAt: '2030-07-01T00:00:00.000Z' } })).toBe('Weekly Reset: 4hr');
expect(render(widget, item, { usageData: { weeklyUsage: 5 } })).toBe('Weekly Reset: 4hr');
expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer' }, { usageData: { weeklyUsage: 0 } })).toBe('Weekly Reset: 4hr');
});

// One state covers both placeholders, since either means the same thing to
Expand Down
Loading
Loading