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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ More ways to use it:

- Check quota anywhere: use `opencode-quota show` in a terminal or the same slash commands in the TUI, Web, and Desktop.
- Automate quota checks with JSON output for scripts, status bars, and CI. Optional OpenTelemetry metrics support monitoring tools.
- Customize the display with [`tuiPromptBar.enabled`](docs/readme/configuration.md#tui-settings), OpenCode Go's preferred collapsed-sidebar window, reset precision, and [`accountingDetail`](docs/readme/configuration.md#show-accounting-detail).
- Customize the display with [`tuiPromptBar.enabled`](docs/readme/configuration.md#tui-settings), OpenCode Go's preferred collapsed-sidebar window, reset precision or spacing, bare percent labels, and [`accountingDetail`](docs/readme/configuration.md#show-accounting-detail).
- Choose current-session or descendant-tree token totals. Get reset popups for selected windows with [`resetNotifications`](docs/readme/configuration.md#notify-when-quota-becomes-available-again).
- Troubleshoot authentication, quota sources, pricing, and maintainer notices.

Expand Down
26 changes: 26 additions & 0 deletions docs/readme/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Strict `.json` files also work. Run `/quota_status` if you are unsure which file
| Show every reset period | `formatStyle: "allWindows"` |
| Show one quota window per provider | `formatStyle: "singleWindow"` |
| Show quota used instead of left | `percentDisplayMode: "used"` |
| Show percentages without `left` or `used` | `percentLabelStyle: "bare"` |
| Add spaces between reset countdown units | `resetTimeSpaced: true` |
| Show supplementary accounting facts | `accountingDetail: "detailed"` |
| Show slash results with messages | `tuiCommandDisplay: "inline"` |
| Show slash results in a TUI popup | `tuiCommandDisplay: "dialog"` |
Expand All @@ -48,6 +50,8 @@ The installer chooses `allWindows` by default. If the setting is absent, the bui
// Show every quota reset period as percentage remaining.
"formatStyle": "allWindows",
"percentDisplayMode": "remaining",
"percentLabelStyle": "bare",
"resetTimeSpaced": true,
"accountingDetail": "summary",

// Keep TUI slash-command results with normal messages.
Expand Down Expand Up @@ -274,6 +278,26 @@ Leave it unset to use the default exact-to-minute display.

</details>

<details>
<summary><strong>Space reset units and shorten percent labels</strong></summary>

Set `resetTimeSpaced` to `true` to add spaces between exact compound countdown units. For example, `2d5h14m` becomes `2d 5h 14m`, and `3h45m` becomes `3h 45m`. Minute-only values such as `14m`, expired values shown as `reset`, and partial-minute rounding stay unchanged. This setting applies to `/quota` in Web, Desktop, and the TUI, popup toasts, terminal `show`, the expanded and collapsed Sidebar, Compact status, and the prompt bar.

`resetTimeDecimals` keeps its existing largest-unit decimal format and takes precedence over spacing on the displays where decimal countdowns apply.

Set `percentLabelStyle` to `"bare"` to show `81%` instead of `81% left`, or `19%` instead of `19% used`. Full reports identify the mode as `Quota [Remaining]` or `Quota [Used]`. The Sidebar uses the same heading, keeps its collapse icon, and gives the freed columns to its bars. Compact status and prompt percentages remain bare.

```jsonc
{
"resetTimeSpaced": true,
"percentLabelStyle": "bare",
}
```

Both settings are optional. Leave them unset to keep the existing compact countdowns and full percent labels.

</details>

<details>
<summary><strong>Change maintainer notices</strong></summary>

Expand Down Expand Up @@ -349,8 +373,10 @@ Existing `experimental.quotaToast` settings remain supported. Quota settings do
| `requestTimeoutMs` | `5000` | Remote provider request timeout in milliseconds. |
| `formatStyle` | `singleWindow` | Shared quota reset-period display for TUI popup toasts, the Sidebar panel, and Compact status line unless a TUI surface override is set: `singleWindow` shows one reset period per provider; `allWindows` shows all reset periods per provider. Legacy `classic`/`grouped` aliases are still accepted. |
| `percentDisplayMode` | `remaining` | Percentage/bar direction across human surfaces: `remaining` shows the percentage left; `used` shows the percentage consumed. It does not rename literal basis facts. |
| `percentLabelStyle` | unset | Set to `bare` to remove `left` or `used` from full-report percentage labels. Full reports and the Sidebar name the direction in a `Quota [Remaining]` or `Quota [Used]` heading. `full` is also accepted. Unset keeps full labels. |
| `accountingDetail` | `summary` | Provider-neutral accounting detail across human surfaces: `summary` keeps primary rows; `detailed` also admits supplementary rows and fuller basis detail when width allows. Independent of `formatStyle` and `percentDisplayMode`. |
| `resetTimeDecimals` | unset | Decimal places for a largest-unit reset countdown override in popup toasts, the Sidebar panel, terminal `show`, and the prompt bar. Accepts integers `0`–`4`; when unset, the default shows exact remaining days, hours, and minutes as `DdHhMm`. |
| `resetTimeSpaced` | unset | Set to `true` to space exact compound countdowns such as `2d 5h 14m` on `/quota`, popup toasts, terminal `show`, the Sidebar, Compact status, and the prompt bar. `resetTimeDecimals` keeps its legacy decimal format where it applies. Unset or `false` keeps compact spelling. |
| `onlyCurrentModel` | `false` | Filter quota rows to the current model/provider when that session selection can be resolved. |
| `showSessionTokens` | `true` | Show the `Session input/output tokens` section when session token data is available. When cached input is present, the section keeps the legacy `in/out` layout and appends cached input in parentheses next to the input amount. |
| `sessionTokenScope` | `"current"` | Choose `current` for the active session only or `tree` for the active session plus recursive descendants/subagents, counted once. Applies to `/quota`, popup toasts, the Sidebar panel, and the compact input line when `showSessionTokens` is enabled. Does not change `/tokens_session` or `/tokens_session_all`. |
Expand Down
10 changes: 9 additions & 1 deletion src/lib/cli-show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { hasAnthropicCredentialsConfigured } from "./anthropic.js";
import { findGitWorktreeRoot, getEffectiveConfigRoot } from "./config-file-utils.js";
import { sanitizeQuotaRenderData } from "./display-sanitize.js";
import { formatQuotaRows } from "./format.js";
import { formatQuotaModeHeading } from "./format-utils.js";
import { DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, resolveKimiAuthCached } from "./kimi-auth.js";
import {
loadConfiguredOpenCodeConfig,
Expand Down Expand Up @@ -359,16 +360,23 @@ export async function runCliShowCommand(options: RunCliShowCommandOptions = {}):
errors: data.errors,
style: resolveQuotaFormatStyle(config.formatStyle),
percentDisplayMode: config.percentDisplayMode,
percentLabelStyle: config.percentLabelStyle,
accountingDetail: config.accountingDetail,
resetTimeDecimals: config.resetTimeDecimals,
resetTimeSpaced: config.resetTimeSpaced,
});

if (!output.trim()) {
writeLine(stderr, "No provider data available.");
return 1;
}

writeLine(stdout, output);
writeLine(
stdout,
config.percentLabelStyle === "bare"
? `${formatQuotaModeHeading(config.percentDisplayMode)}\n\n${output}`
: output,
);
return data.entries.length > 0 ? 0 : 1;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
Expand Down
33 changes: 33 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
CursorQuotaPlan,
GoogleModelId,
PercentDisplayMode,
PercentLabelStyle,
PricingSnapshotSource,
QuotaResetWindow,
QuotaToastConfig,
Expand All @@ -45,8 +46,10 @@ export const QUOTA_TOAST_SETTING_SOURCE_KEYS = [
"tuiCommandDisplay",
"formatStyle",
"percentDisplayMode",
"percentLabelStyle",
"accountingDetail",
"resetTimeDecimals",
"resetTimeSpaced",
"minIntervalMs",
"requestTimeoutMs",
"debug",
Expand Down Expand Up @@ -157,8 +160,10 @@ type ValidatedQuotaToastPatch = {
tuiCommandDisplay?: TuiCommandDisplay;
formatStyle?: QuotaToastConfig["formatStyle"];
percentDisplayMode?: PercentDisplayMode;
percentLabelStyle?: PercentLabelStyle;
accountingDetail?: QuotaToastConfig["accountingDetail"];
resetTimeDecimals?: number;
resetTimeSpaced?: boolean;
minIntervalMs?: number;
requestTimeoutMs?: number;
debug?: boolean;
Expand Down Expand Up @@ -245,6 +250,10 @@ function isValidPercentDisplayMode(value: unknown): value is PercentDisplayMode
return value === "remaining" || value === "used";
}

function isValidPercentLabelStyle(value: unknown): value is PercentLabelStyle {
return value === "full" || value === "bare";
}

function isValidAccountingDetail(value: unknown): value is QuotaToastConfig["accountingDetail"] {
return value === "summary" || value === "detailed";
}
Expand Down Expand Up @@ -677,6 +686,13 @@ function extractValidatedQuotaToastPatch(
patch.percentDisplayMode = quotaToastConfig.percentDisplayMode;
}

if (
hasOwnKey(quotaToastConfig, "percentLabelStyle") &&
isValidPercentLabelStyle(quotaToastConfig.percentLabelStyle)
) {
patch.percentLabelStyle = quotaToastConfig.percentLabelStyle;
}

if (hasOwnKey(quotaToastConfig, "accountingDetail")) {
if (isValidAccountingDetail(quotaToastConfig.accountingDetail)) {
patch.accountingDetail = quotaToastConfig.accountingDetail;
Expand All @@ -692,6 +708,13 @@ function extractValidatedQuotaToastPatch(
patch.resetTimeDecimals = quotaToastConfig.resetTimeDecimals;
}

if (
hasOwnKey(quotaToastConfig, "resetTimeSpaced") &&
typeof quotaToastConfig.resetTimeSpaced === "boolean"
) {
patch.resetTimeSpaced = quotaToastConfig.resetTimeSpaced;
}

if (
hasOwnKey(quotaToastConfig, "minIntervalMs") &&
isPositiveNumber(quotaToastConfig.minIntervalMs)
Expand Down Expand Up @@ -947,6 +970,11 @@ function applyValidatedQuotaToastPatch(
applySettingSource(settingSources, "percentDisplayMode", sourcePath);
}

if (hasOwnKey(patch, "percentLabelStyle")) {
config.percentLabelStyle = patch.percentLabelStyle;
applySettingSource(settingSources, "percentLabelStyle", sourcePath);
}

if (hasOwnKey(patch, "accountingDetail")) {
config.accountingDetail = patch.accountingDetail!;
applySettingSource(settingSources, "accountingDetail", sourcePath);
Expand All @@ -957,6 +985,11 @@ function applyValidatedQuotaToastPatch(
applySettingSource(settingSources, "resetTimeDecimals", sourcePath);
}

if (hasOwnKey(patch, "resetTimeSpaced")) {
config.resetTimeSpaced = patch.resetTimeSpaced;
applySettingSource(settingSources, "resetTimeSpaced", sourcePath);
}

if (hasOwnKey(patch, "minIntervalMs")) {
config.minIntervalMs = patch.minIntervalMs!;
applySettingSource(settingSources, "minIntervalMs", sourcePath);
Expand Down
21 changes: 17 additions & 4 deletions src/lib/format-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* - quota-command-format.ts (/quota command)
*/

import type { PercentDisplayMode } from "./types.js";
import type { PercentDisplayMode, PercentLabelStyle } from "./types.js";

/**
* Clamp a number to an integer within [min, max].
Expand Down Expand Up @@ -67,13 +67,23 @@ export function resolveDisplayedPercent(
export function formatDisplayedPercentLabel(
percentRemaining: number,
mode: PercentDisplayMode = "remaining",
style: PercentLabelStyle = "full",
): string {
const displayedPercent = resolveDisplayedPercent(percentRemaining, mode);
return `${displayedPercent}% ${mode === "used" ? "used" : "left"}`;
const percent = `${displayedPercent}%`;
return style === "bare" ? percent : `${percent} ${mode === "used" ? "used" : "left"}`;
}

export const DISPLAYED_PERCENT_LABEL_WIDTH = "100% used".length;

export function displayedPercentLabelWidth(style: PercentLabelStyle = "full"): number {
return style === "bare" ? "100%".length : DISPLAYED_PERCENT_LABEL_WIDTH;
}

export function formatQuotaModeHeading(mode: PercentDisplayMode = "remaining"): string {
return `Quota [${mode === "used" ? "Used" : "Remaining"}]`;
}

/**
* Format a token count with K/M suffix for compactness.
*
Expand Down Expand Up @@ -151,6 +161,8 @@ export interface FormatResetCountdownOptions {
* many decimal places.
*/
decimals?: number;
/** Join exact compound countdown units with spaces. */
spaced?: boolean;
}

const MS_PER_DAY = 86_400_000;
Expand Down Expand Up @@ -191,8 +203,9 @@ export function formatResetCountdown(iso?: string, opts?: FormatResetCountdownOp
return `0.5h`;
}

if (days > 0) return `${days}d${hours}h${minutes}m`;
if (hours > 0) return `${hours}h${minutes}m`;
const separator = opts?.spaced ? " " : "";
if (days > 0) return [`${days}d`, `${hours}h`, `${minutes}m`].join(separator);
if (hours > 0) return [`${hours}h`, `${minutes}m`].join(separator);
return `${minutes}m`;
}

Expand Down
24 changes: 18 additions & 6 deletions src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { QuotaToastEntry, QuotaToastError, SessionTokensData } from "./entr
import { isPercentEntry } from "./entries.js";
import {
bar,
DISPLAYED_PERCENT_LABEL_WIDTH,
displayedPercentLabelWidth,
formatDisplayedPercentLabel,
formatResetCountdown,
isResetTimeDecimals,
Expand Down Expand Up @@ -100,8 +100,10 @@ export function formatQuotaRows(params: {
errors?: QuotaToastError[];
style?: QuotaFormatStyle;
percentDisplayMode?: QuotaToastConfig["percentDisplayMode"];
percentLabelStyle?: QuotaToastConfig["percentLabelStyle"];
accountingDetail?: QuotaToastConfig["accountingDetail"];
resetTimeDecimals?: number;
resetTimeSpaced?: boolean;
sessionTokens?: SessionTokensData;
}): string {
const styleDefinition = getQuotaFormatStyleDefinition(params.style);
Expand All @@ -112,8 +114,10 @@ export function formatQuotaRows(params: {
entries: params.entries,
errors: params.errors,
percentDisplayMode: params.percentDisplayMode,
percentLabelStyle: params.percentLabelStyle,
accountingDetail: params.accountingDetail,
resetTimeDecimals: params.resetTimeDecimals,
resetTimeSpaced: params.resetTimeSpaced,
sessionTokens: params.sessionTokens,
});
}
Expand All @@ -130,12 +134,16 @@ export function formatQuotaRows(params: {

const separator = " ";
const percentCol = Math.max(
DISPLAYED_PERCENT_LABEL_WIDTH,
displayedPercentLabelWidth(params.percentLabelStyle),
...(params.entries ?? [])
.filter(isPercentEntry)
.map(
(entry) =>
formatDisplayedPercentLabel(entry.percentRemaining, params.percentDisplayMode).length,
formatDisplayedPercentLabel(
entry.percentRemaining,
params.percentDisplayMode,
params.percentLabelStyle,
).length,
),
);

Expand All @@ -157,7 +165,11 @@ export function formatQuotaRows(params: {
rightSummary?: string,
) => {
const displayedPercent = resolveDisplayedPercent(remaining, params.percentDisplayMode);
const percentLabel = formatDisplayedPercentLabel(remaining, params.percentDisplayMode);
const percentLabel = formatDisplayedPercentLabel(
remaining,
params.percentDisplayMode,
params.percentLabelStyle,
);
const visibleBarSuffix = percentLabel.slice(0, percentValueCol);
const summary = rightSummary?.trim() || "";
const leftText = summary ? `${name} ${summary}` : name;
Expand All @@ -174,7 +186,7 @@ export function formatQuotaRows(params: {
compactRounded: true,
decimals: params.resetTimeDecimals,
}
: { missing: "-" },
: { missing: "-", spaced: params.resetTimeSpaced },
)
: "";

Expand Down Expand Up @@ -239,7 +251,7 @@ export function formatQuotaRows(params: {
compactRounded: true,
decimals: params.resetTimeDecimals,
}
: { missing: "-" },
: { missing: "-", spaced: params.resetTimeSpaced },
);

if (atomicValue) {
Expand Down
Loading