diff --git a/packages/opencode/src/commands.ts b/packages/opencode/src/commands.ts index 1c84140..0609f15 100644 --- a/packages/opencode/src/commands.ts +++ b/packages/opencode/src/commands.ts @@ -16,6 +16,7 @@ import { whamUsageFn } from './core/provider' import type { QuotaManager } from './core/quota-manager' import type { RefreshAllQuotaResult } from './core/refresh-all-quota' import { + countEligibleResetCredits, evaluateResetPrecondition, listResetCredits, ResetCreditError, @@ -1235,9 +1236,12 @@ async function buildResetPreviewRow( listResetCredits(ctx.fetchImpl, target.accessToken, wireAccountId), ]) const selectedCredit = selectCreditToSpend(credits.credits) + const eligibleCreditCount = countEligibleResetCredits(credits.credits) const availableCount = - credits.availableCount ?? quota.resetCreditsAvailable ?? 0 - const applicableAvailableCount = quota.resetCreditsApplicable ?? 0 + credits.availableCount ?? + quota.resetCreditsAvailable ?? + (eligibleCreditCount > 0 ? eligibleCreditCount : undefined) + const applicableAvailableCount = quota.resetCreditsApplicable const precondition = evaluateResetPrecondition( quota, ctx.quotaManager.isRateLimited(accountKey), @@ -1293,7 +1297,7 @@ function renderResetAccountList(rows: readonly ResetPreviewRow[]): string { const credits = row.availableCount === undefined ? 'credits unavailable' - : `${row.applicableAvailableCount ?? 0}/${row.availableCount} applicable/available` + : `${row.applicableAvailableCount === undefined ? '?' : row.applicableAvailableCount}/${row.availableCount} applicable/available` const status = row.eligible ? `eligible ยท credit ${row.selectedCreditId} expires ${row.selectedCreditExpiresAt}` : row.reason @@ -1312,7 +1316,7 @@ function renderResetConfirm(row: ResetPreviewRow): string { '', `Account: **${row.label}** (\`${row.accountKey}\`)`, `Current quota: **${row.usedPercent ?? 'unknown'}% used**`, - `Credit: **Spend 1 of ${row.availableCount ?? 0}**`, + `Credit: **Spend 1 of ${row.availableCount ?? 'unknown'}**`, `Credit expires: **${row.selectedCreditExpiresAt ?? 'unavailable'}**`, `Quota resets: **${row.resetTime ?? 'unavailable'}**`, '', diff --git a/packages/opencode/src/core/reset-credits.ts b/packages/opencode/src/core/reset-credits.ts index 4de92dc..623a20d 100644 --- a/packages/opencode/src/core/reset-credits.ts +++ b/packages/opencode/src/core/reset-credits.ts @@ -571,17 +571,26 @@ export function selectCreditToSpend( credits: readonly ResetCredit[], ): ResetCredit | undefined { return [...credits] - .filter( - (credit) => - credit.status === 'available' && - credit.isSupportedByPlan && - credit.resetType === 'codex_rate_limits', - ) + .filter(isResetCreditEligible) .sort( (left, right) => Date.parse(left.expiresAt) - Date.parse(right.expiresAt), )[0] } +export function isResetCreditEligible(credit: ResetCredit): boolean { + return ( + credit.status === 'available' && + credit.isSupportedByPlan && + credit.resetType === 'codex_rate_limits' + ) +} + +export function countEligibleResetCredits( + credits: readonly ResetCredit[], +): number { + return credits.filter(isResetCreditEligible).length +} + function isTerminalConsumeKind(value: unknown): value is ResetConsumeKind { return ( value === 'reset' || diff --git a/packages/opencode/src/tests/commands.test.ts b/packages/opencode/src/tests/commands.test.ts index a69089f..48195c9 100644 --- a/packages/opencode/src/tests/commands.test.ts +++ b/packages/opencode/src/tests/commands.test.ts @@ -2415,6 +2415,93 @@ describe('commands', () => { ) }) + test('omitted applicable count stays unknown in the account preview', async () => { + await saveResetAccounts([]) + const { ctx } = await makeResetCommandHarness( + configPath, + now, + resetFixture(), + ) + ctx.fetchImpl = fetchStub(async (input) => { + if (input.toString().endsWith('/wham/usage')) { + return Response.json({ + rate_limit: { + primary_window: { + used_percent: 100, + reset_at: '2026-07-18T00:00:00.000Z', + }, + }, + rate_limit_reset_credits: { available_count: 1 }, + }) + } + return Response.json({ + credits: [ + { + id: 'credit-omitted-applicable', + status: 'available', + expires_at: '2026-08-01T00:00:00.000Z', + reset_type: 'codex_rate_limits', + is_supported_by_plan: true, + }, + ], + }) + }) + + const payload = await buildDialogPayload('openai-reset', '', ctx) + const rows = payload.knobs.accounts as Array> + + expect(rows).toContainEqual( + expect.objectContaining({ + accountKey: 'main', + availableCount: 1, + applicableAvailableCount: undefined, + eligible: true, + }), + ) + expect(payload.text).toContain('?/1 applicable/available') + expect(payload.text).not.toContain('does not currently count') + }) + + test('omitted credit counts fall back to the eligible credit total', async () => { + await saveResetAccounts([]) + const { ctx } = await makeResetCommandHarness( + configPath, + now, + resetFixture(), + ) + ctx.fetchImpl = fetchStub(async (input) => { + if (input.toString().endsWith('/wham/usage')) { + return Response.json({ + rate_limit: { + primary_window: { + used_percent: 100, + reset_at: '2026-07-18T00:00:00.000Z', + }, + }, + }) + } + return Response.json({ + credits: [ + { + id: 'credit-omitted-counts', + status: 'available', + expires_at: '2026-08-01T00:00:00.000Z', + reset_type: 'codex_rate_limits', + is_supported_by_plan: true, + }, + ], + }) + }) + + const payload = await buildDialogPayload( + 'openai-reset', + 'select main', + ctx, + ) + + expect(payload.text).toContain('Spend 1 of 1') + }) + test('account preview keeps per-account failures visible and requires a stable identity for action', async () => { await saveResetAccounts([ makeAccount('broken', { diff --git a/packages/opencode/src/tests/reset-credits.test.ts b/packages/opencode/src/tests/reset-credits.test.ts index 180f5f2..dfdb61b 100644 --- a/packages/opencode/src/tests/reset-credits.test.ts +++ b/packages/opencode/src/tests/reset-credits.test.ts @@ -458,12 +458,6 @@ describe('reset redemption precondition', () => { ).toEqual({ ok: false, reason: 'not exhausted' }) }) - it('accepts exhausted quota when the server reports no applicable credits', () => { - expect( - evaluateResetPrecondition({ primary: quotaWindow(100) }, false, now), - ).toEqual({ ok: true }) - }) - it('treats a 100%-used expired window as stale rather than exhausted', () => { expect( evaluateResetPrecondition( diff --git a/packages/opencode/src/tui/command-dialogs.tsx b/packages/opencode/src/tui/command-dialogs.tsx index 06b2276..6a7a015 100644 --- a/packages/opencode/src/tui/command-dialogs.tsx +++ b/packages/opencode/src/tui/command-dialogs.tsx @@ -142,7 +142,7 @@ function resetAccountOptions(payload: OpenDialogPayload): ResetDialogOption[] { account.usedPercent === undefined ? 'quota unavailable' : `${account.usedPercent}%` - const counts = `${account.applicableAvailableCount ?? 0}/${account.availableCount ?? 0}` + const counts = `${account.applicableAvailableCount === undefined ? '?' : account.applicableAvailableCount}/${account.availableCount ?? '?'}` const status = account.eligible ? 'eligible' : (account.reason ?? 'unavailable') @@ -278,7 +278,7 @@ function openResetDialog( const preview = state.knobs.preview as ResetPreviewKnob | undefined const accountKey = preview?.accountKey const chatgptAccountId = preview?.chatgptAccountId - const availableCount = preview?.availableCount ?? 0 + const availableCount = preview?.availableCount ?? 'unknown' const DialogConfirm = api.ui.DialogConfirm api.ui.dialog.replace(() => (