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
39 changes: 8 additions & 31 deletions src/widgets/BlockResetTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import type {
WidgetEditorProps,
WidgetItem
} from '../types/Widget';
import {
formatPercent,
resolveNumberFormat
} from '../utils/number-format';
import { resolveNumberFormat } from '../utils/number-format';
import {
formatUsageDuration,
formatUsageResetAt,
Expand All @@ -35,6 +32,7 @@ import {
SESSION_UNDER_LIMIT_HIDEABLE_STATE,
USAGE_NO_DATA_HIDEABLE_STATE,
cycleUsageDisplayMode,
formatUsageBar,
getUsageDisplayMode,
getUsageDisplayModifierText,
getUsageLocale,
Expand All @@ -47,8 +45,6 @@ import {
isUsageInverted,
isUsageProgressMode,
isUsageSliderMode,
makeSliderBar,
renderUsageProgressBar,
toggleUsageCompact,
toggleUsageDateMode,
toggleUsageHourFormat,
Expand Down Expand Up @@ -107,7 +103,6 @@ export class BlockResetTimerWidget implements Widget {
}

render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
const displayMode = getUsageDisplayMode(item);
const inverted = isUsageInverted(item);
const compact = isUsageCompact(item);
const dateMode = isUsageDateMode(item);
Expand All @@ -116,17 +111,9 @@ export class BlockResetTimerWidget implements Widget {
if (context.isPreview) {
const previewPercent = inverted ? 90.0 : 10.0;

if (isUsageProgressMode(displayMode)) {
const progressBar = renderUsageProgressBar(previewPercent, displayMode);
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), `${progressBar} ${formatPercent(previewPercent, format)}`);
}

if (isUsageSliderMode(displayMode)) {
const slider = makeSliderBar(previewPercent);
const sliderDisplay = displayMode === 'slider'
? `${slider} ${formatPercent(previewPercent, format)}`
: slider;
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), sliderDisplay);
const bar = formatUsageBar(item, previewPercent, format);
if (bar !== null) {
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), bar);
}

if (dateMode) {
Expand Down Expand Up @@ -162,19 +149,9 @@ export class BlockResetTimerWidget implements Widget {
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), USAGE_TIMER_LOADING_MESSAGE);
}

if (isUsageProgressMode(displayMode)) {
const percent = inverted ? window.remainingPercent : window.elapsedPercent;
const progressBar = renderUsageProgressBar(percent, displayMode);
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), `${progressBar} ${formatPercent(percent, format)}`);
}

if (isUsageSliderMode(displayMode)) {
const percent = inverted ? window.remainingPercent : window.elapsedPercent;
const slider = makeSliderBar(percent);
const sliderDisplay = displayMode === 'slider'
? `${slider} ${formatPercent(percent, format)}`
: slider;
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), sliderDisplay);
const bar = formatUsageBar(item, inverted ? window.remainingPercent : window.elapsedPercent, format);
if (bar !== null) {
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), bar);
}

if (dateMode) {
Expand Down
55 changes: 11 additions & 44 deletions src/widgets/BlockTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import type {
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
import {
formatPercent,
resolveNumberFormat
} from '../utils/number-format';
import { resolveNumberFormat } from '../utils/number-format';
import {
formatUsageDuration,
resolveUsageWindowWithFallback
Expand All @@ -20,15 +17,14 @@ import { isHidden } from './shared/hideable';
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
import {
cycleUsageDisplayMode,
formatUsageBar,
getUsageDisplayMode,
getUsageDisplayModifierText,
getUsageTimerCustomKeybinds,
isUsageCompact,
isUsageInverted,
isUsageProgressMode,
isUsageSliderMode,
makeSliderBar,
renderUsageProgressBar,
toggleUsageCompact,
toggleUsageInverted
} from './shared/usage-display';
Expand Down Expand Up @@ -72,25 +68,16 @@ export class BlockTimerWidget implements Widget {
}

render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
const displayMode = getUsageDisplayMode(item);
const inverted = isUsageInverted(item);
const compact = isUsageCompact(item);
const format = resolveNumberFormat('percent', item, settings);

if (context.isPreview) {
const previewPercent = inverted ? 26.1 : 73.9;

if (isUsageProgressMode(displayMode)) {
const progressBar = renderUsageProgressBar(previewPercent, displayMode);
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), `${progressBar} ${formatPercent(previewPercent, format)}`);
}

if (isUsageSliderMode(displayMode)) {
const slider = makeSliderBar(previewPercent);
const sliderDisplay = displayMode === 'slider'
? `${slider} ${formatPercent(previewPercent, format)}`
: slider;
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), sliderDisplay);
const bar = formatUsageBar(item, previewPercent, format);
if (bar !== null) {
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), bar);
}

return formatRawOrLabeledValue(item, this.getLabelPrefix(item), compact ? '3h45m' : '3hr 45m');
Expand All @@ -103,37 +90,17 @@ export class BlockTimerWidget implements Widget {
if (isHidden(item, NO_DATA_HIDEABLE_STATE.key)) {
return null;
}

const emptyPercent = formatPercent(0, format);
if (isUsageProgressMode(displayMode)) {
const emptyBar = renderUsageProgressBar(0, displayMode);
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), `${emptyBar} ${emptyPercent}`);
}

if (isUsageSliderMode(displayMode)) {
const emptySlider = makeSliderBar(0);
const sliderDisplay = displayMode === 'slider'
? `${emptySlider} ${emptyPercent}`
: emptySlider;
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), sliderDisplay);
const bar = formatUsageBar(item, 0, format);
if (bar !== null) {
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), bar);
}

return formatRawOrLabeledValue(item, this.getLabelPrefix(item), compact ? '0h' : '0hr 0m');
}

if (isUsageProgressMode(displayMode)) {
const percent = inverted ? window.remainingPercent : window.elapsedPercent;
const progressBar = renderUsageProgressBar(percent, displayMode);
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), `${progressBar} ${formatPercent(percent, format)}`);
}

if (isUsageSliderMode(displayMode)) {
const percent = inverted ? window.remainingPercent : window.elapsedPercent;
const slider = makeSliderBar(percent);
const sliderDisplay = displayMode === 'slider'
? `${slider} ${formatPercent(percent, format)}`
: slider;
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), sliderDisplay);
const bar = formatUsageBar(item, inverted ? window.remainingPercent : window.elapsedPercent, format);
if (bar !== null) {
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), bar);
}

const elapsedTime = formatUsageDuration(window.elapsedMs, compact);
Expand Down
83 changes: 7 additions & 76 deletions src/widgets/ContextPercentage.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,17 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
CustomKeybind,
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
import { calculateContextPercentageMetrics } from '../utils/context-percentage';
import {
formatPercent,
resolveNumberFormat
} from '../utils/number-format';

import {
getContextInverseModifierText,
handleContextInverseAction,
isContextInverse
} from './shared/context-inverse';
import {
cycleContextSliderMode,
getContextSliderKeybinds,
getContextSliderMode,
getContextSliderModifierText,
renderContextSlider
} from './shared/context-slider';
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
import { ContextPercentageWidgetBase } from './shared/context-percentage-widget-base';

export class ContextPercentageWidget extends ContextPercentageWidgetBase {
protected readonly labelPrefix = 'Ctx';
protected readonly previewUsedPercent = 9.3;

export class ContextPercentageWidget implements Widget {
getDefaultColor(): string { return 'blue'; }
getDescription(): string { return 'Shows percentage of context window used or remaining'; }
getDisplayName(): string { return 'Context %'; }
getCategory(): string { return 'Context'; }
getLabelPrefix(item: WidgetItem): string { return isContextInverse(item) ? 'Ctx Left: ' : 'Ctx Used: '; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
const modifiers = [
getContextInverseModifierText(item),
getContextSliderModifierText(item)
].filter((m): m is string => m !== undefined);
return {
displayText: this.getDisplayName(),
modifierText: modifiers.length > 0 ? `(${modifiers.map(m => m.replace(/^\(|\)$/g, '')).join(', ')})` : undefined
};
}

handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
if (action === 'toggle-slider') {
return cycleContextSliderMode(item);
}
return handleContextInverseAction(action, item);
protected getUsedPercentage(context: RenderContext): number | null {
return calculateContextPercentageMetrics(context)?.usedPercentage ?? null;
}

render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
const isInverse = isContextInverse(item);
const sliderMode = getContextSliderMode(item);
const contextPercentageMetrics = calculateContextPercentageMetrics(context);
const format = resolveNumberFormat('percent', item, settings);

const formatContextPercentage = (displayPercentage: number): string => {
const sliderResult = renderContextSlider(sliderMode, displayPercentage, format);
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), sliderResult ?? formatPercent(displayPercentage, format));
};

if (context.isPreview) {
return formatContextPercentage(isInverse ? 90.7 : 9.3);
}

if (contextPercentageMetrics !== null) {
const displayPercentage = isInverse ? (100 - contextPercentageMetrics.usedPercentage) : contextPercentageMetrics.usedPercentage;
return formatContextPercentage(displayPercentage);
}

return null;
}

getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
return [
{ key: 'u', label: '(u)sed/remaining', action: 'toggle-inverse' },
...getContextSliderKeybinds()
];
}

supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
supportsNumberFormat(): boolean { return true; }
}
90 changes: 8 additions & 82 deletions src/widgets/ContextPercentageUsable.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
CustomKeybind,
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
import {
getAutoCompactConfig,
resolveClaudeConfigCwd
Expand All @@ -15,90 +8,23 @@ import {
getContextConfig,
getModelContextIdentifier
} from '../utils/model-context';
import {
formatPercent,
resolveNumberFormat
} from '../utils/number-format';

import {
getContextInverseModifierText,
handleContextInverseAction,
isContextInverse
} from './shared/context-inverse';
import {
cycleContextSliderMode,
getContextSliderKeybinds,
getContextSliderMode,
getContextSliderModifierText,
renderContextSlider
} from './shared/context-slider';
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
import { ContextPercentageWidgetBase } from './shared/context-percentage-widget-base';

export class ContextPercentageUsableWidget extends ContextPercentageWidgetBase {
protected readonly labelPrefix = 'Ctx(u)';
protected readonly previewUsedPercent = 11.6;

export class ContextPercentageUsableWidget implements Widget {
getDefaultColor(): string { return 'green'; }
getDescription(): string { return 'Shows used or remaining context against an estimated compaction budget.\nHonors Claude Code compaction settings and environment overrides; defaults to an 80% estimate.'; }
getDisplayName(): string { return 'Context % (usable)'; }
getCategory(): string { return 'Context'; }
getLabelPrefix(item: WidgetItem): string { return isContextInverse(item) ? 'Ctx(u) Left: ' : 'Ctx(u) Used: '; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
const modifiers = [
getContextInverseModifierText(item),
getContextSliderModifierText(item)
].filter((m): m is string => m !== undefined);
return {
displayText: this.getDisplayName(),
modifierText: modifiers.length > 0 ? `(${modifiers.map(m => m.replace(/^\(|\)$/g, '')).join(', ')})` : undefined
};
}

handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
if (action === 'toggle-slider') {
return cycleContextSliderMode(item);
}
return handleContextInverseAction(action, item);
}

render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
const isInverse = isContextInverse(item);
const sliderMode = getContextSliderMode(item);
const format = resolveNumberFormat('percent', item, settings);

const formatContextPercentage = (displayPercentage: number): string => {
const sliderResult = renderContextSlider(sliderMode, displayPercentage, format);
return formatRawOrLabeledValue(item, this.getLabelPrefix(item), sliderResult ?? formatPercent(displayPercentage, format));
};

if (context.isPreview) {
return formatContextPercentage(isInverse ? 88.4 : 11.6);
}

protected getUsedPercentage(context: RenderContext): number | null {
const modelIdentifier = getModelContextIdentifier(context.data?.model);
const contextWindowMetrics = getContextWindowMetrics(context.data);
const autoCompact = getAutoCompactConfig(resolveClaudeConfigCwd(context));
const contextConfig = getContextConfig(modelIdentifier, contextWindowMetrics.windowSize, autoCompact);

if (contextWindowMetrics.contextLengthTokens !== null) {
const usedPercentage = Math.min(100, (contextWindowMetrics.contextLengthTokens / contextConfig.usableTokens) * 100);
const displayPercentage = isInverse ? (100 - usedPercentage) : usedPercentage;
return formatContextPercentage(displayPercentage);
}

if (context.tokenMetrics) {
const usedPercentage = Math.min(100, (context.tokenMetrics.contextLength / contextConfig.usableTokens) * 100);
const displayPercentage = isInverse ? (100 - usedPercentage) : usedPercentage;
return formatContextPercentage(displayPercentage);
}
return null;
const contextLength = contextWindowMetrics.contextLengthTokens ?? context.tokenMetrics?.contextLength ?? null;
return contextLength === null ? null : Math.min(100, (contextLength / contextConfig.usableTokens) * 100);
}

getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
return [
{ key: 'u', label: '(u)sed/remaining', action: 'toggle-inverse' },
...getContextSliderKeybinds()
];
}

supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
supportsNumberFormat(): boolean { return true; }
}
Loading
Loading