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
12 changes: 8 additions & 4 deletions packages/opencode/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand All @@ -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'}**`,
'',
Expand Down
21 changes: 15 additions & 6 deletions packages/opencode/src/core/reset-credits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' ||
Expand Down
87 changes: 87 additions & 0 deletions packages/opencode/src/tests/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, unknown>>

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')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The not.toContain('does not currently count') assertion in this test is inert: '' args render the account-list view (renderResetAccountList), and that disclosure string exists only in renderResetConfirm, so the assertion passes regardless of the implementation. Drop it or move it to a select/confirm-view test to actually guard the disclosure behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/tests/commands.test.ts, line 2462:

<comment>The `not.toContain('does not currently count')` assertion in this test is inert: `''` args render the account-list view (renderResetAccountList), and that disclosure string exists only in renderResetConfirm, so the assertion passes regardless of the implementation. Drop it or move it to a select/confirm-view test to actually guard the disclosure behavior.</comment>

<file context>
@@ -2415,6 +2415,93 @@ describe('commands', () => {
+        }),
+      )
+      expect(payload.text).toContain('?/1 applicable/available')
+      expect(payload.text).not.toContain('does not currently count')
+    })
+
</file context>

})

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', {
Expand Down
6 changes: 0 additions & 6 deletions packages/opencode/src/tests/reset-credits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/tui/command-dialogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? '?'}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When availableCount is undefined, the main account list renders 'credits unavailable', but the dialog account option renders '?/?'. These two surfaces describe the same unknown state differently, contradicting the PR's stated consistency goal. Use the same wording in resetAccountOptions (e.g. fall back to 'credits unavailable' when availableCount is undefined).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/tui/command-dialogs.tsx, line 145:

<comment>When availableCount is undefined, the main account list renders 'credits unavailable', but the dialog account option renders '?/?'. These two surfaces describe the same unknown state differently, contradicting the PR's stated consistency goal. Use the same wording in resetAccountOptions (e.g. fall back to 'credits unavailable' when availableCount is undefined).</comment>

<file context>
@@ -142,7 +142,7 @@ function resetAccountOptions(payload: OpenDialogPayload): ResetDialogOption[] {
           ? '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'
</file context>
Suggested change
const counts = `${account.applicableAvailableCount === undefined ? '?' : account.applicableAvailableCount}/${account.availableCount ?? '?'}`
const counts =
account.availableCount === undefined
? 'credits unavailable'
: `${account.applicableAvailableCount === undefined ? '?' : account.applicableAvailableCount}/${account.availableCount}`

const status = account.eligible
? 'eligible'
: (account.reason ?? 'unavailable')
Expand Down Expand Up @@ -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(() => (
<DialogConfirm
Expand Down