From f171b2716fa9fe2e5173cba2cb03a7a197fa4224 Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:47:31 +0200 Subject: [PATCH 1/5] feat(quota): normalize the spend-control credit budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wham/usage carries a third exhaustion axis on its own clock: a credit budget with its own limit, usage and reset, independent of the two rate limit windows. normalizeWham read only rate_limit_reset_credits, so an account could sit healthy on both windows and still be refused with nothing on any surface to explain it. The numeric fields arrive as strings and both spend_control and its individual_limit are nullable, so every field is parsed defensively and the whole reading stays optional — an account without spend controls produces exactly the snapshot it did before. --- packages/core/src/accounts.ts | 73 ++++++++++++++++++ packages/core/src/quota-normalize.ts | 69 +++++++++++++++++ .../src/tests/quota-normalize.test.ts | 77 +++++++++++++++++++ 3 files changed, 219 insertions(+) diff --git a/packages/core/src/accounts.ts b/packages/core/src/accounts.ts index de0ee7f4..c9b3db4a 100644 --- a/packages/core/src/accounts.ts +++ b/packages/core/src/accounts.ts @@ -90,6 +90,27 @@ export interface OAuthQuotaSnapshot { secondary?: AccountQuotaWindow resetCreditsAvailable?: number resetCreditsApplicable?: number + spendControl?: OAuthSpendControlReading + credits?: OAuthCredits +} + +export interface OAuthSpendControlReading { + limit: number + used: number + remaining: number + usedPercent: number + remainingPercent: number + resetsAt?: string + unit?: string + source?: string + reached: boolean +} + +export interface OAuthCredits { + hasCredits: boolean + unlimited: boolean + overageLimitReached: boolean + balance?: number } // --------------------------------------------------------------------------- @@ -477,6 +498,58 @@ function normalizeQuota(value: unknown): OAuthAccount['quota'] { } } + const spendControl = value.spendControl + if (isRecord(spendControl)) { + const limit = Number(spendControl.limit) + const used = Number(spendControl.used) + const remaining = Number(spendControl.remaining) + const usedPercent = Number(spendControl.usedPercent) + const remainingPercent = Number(spendControl.remainingPercent) + if ( + Number.isFinite(limit) && + Number.isFinite(used) && + Number.isFinite(remaining) && + Number.isFinite(usedPercent) && + Number.isFinite(remainingPercent) && + typeof spendControl.reached === 'boolean' + ) { + quota.spendControl = { + limit, + used, + remaining, + usedPercent, + remainingPercent, + resetsAt: + typeof spendControl.resetsAt === 'string' + ? spendControl.resetsAt + : undefined, + unit: + typeof spendControl.unit === 'string' ? spendControl.unit : undefined, + source: + typeof spendControl.source === 'string' + ? spendControl.source + : undefined, + reached: spendControl.reached, + } + } + } + + const credits = value.credits + if ( + isRecord(credits) && + typeof credits.hasCredits === 'boolean' && + typeof credits.unlimited === 'boolean' && + typeof credits.overageLimitReached === 'boolean' + ) { + const balance = Number(credits.balance) + quota.credits = { + hasCredits: credits.hasCredits, + unlimited: credits.unlimited, + overageLimitReached: credits.overageLimitReached, + ...(Number.isFinite(balance) ? { balance } : {}), + } + } + return Object.keys(quota).length ? quota : undefined } diff --git a/packages/core/src/quota-normalize.ts b/packages/core/src/quota-normalize.ts index c773eb27..01ee5044 100644 --- a/packages/core/src/quota-normalize.ts +++ b/packages/core/src/quota-normalize.ts @@ -203,6 +203,31 @@ interface WhamUsageResponse { available_count?: number applicable_available_count?: number } | null + credits?: { + has_credits?: boolean + unlimited?: boolean + overage_limit_reached?: boolean + balance?: unknown + } | null + spend_control?: { + reached?: boolean + individual_limit?: { + source?: string + unit?: string + limit?: unknown + used?: unknown + remaining?: unknown + used_percent?: unknown + remaining_percent?: unknown + reset_at?: string | number + } | null + } | null +} + +function nonNegativeNumberish(value: unknown): number | undefined { + if (value == null || value === '') return undefined + const parsed = Number(value) + return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined } function windowFromWham( @@ -256,5 +281,49 @@ export function normalizeWham(json: WhamUsageResponse): OAuthQuotaSnapshot { if (resetCreditsApplicable !== undefined) { snapshot.resetCreditsApplicable = resetCreditsApplicable } + const individualLimit = json.spend_control?.individual_limit + if (individualLimit && typeof json.spend_control?.reached === 'boolean') { + const limit = nonNegativeNumberish(individualLimit.limit) + const used = nonNegativeNumberish(individualLimit.used) + const remaining = nonNegativeNumberish(individualLimit.remaining) + const usedPercent = nonNegativeNumberish(individualLimit.used_percent) + const remainingPercent = nonNegativeNumberish( + individualLimit.remaining_percent, + ) + if ( + limit !== undefined && + used !== undefined && + remaining !== undefined && + usedPercent !== undefined && + remainingPercent !== undefined + ) { + snapshot.spendControl = { + limit, + used, + remaining, + usedPercent, + remainingPercent, + resetsAt: toResetIso(individualLimit.reset_at), + unit: individualLimit.unit, + source: individualLimit.source, + reached: json.spend_control.reached, + } + } + } + const credits = json.credits + if ( + credits && + typeof credits.has_credits === 'boolean' && + typeof credits.unlimited === 'boolean' && + typeof credits.overage_limit_reached === 'boolean' + ) { + const balance = nonNegativeNumberish(credits.balance) + snapshot.credits = { + hasCredits: credits.has_credits, + unlimited: credits.unlimited, + overageLimitReached: credits.overage_limit_reached, + ...(balance !== undefined ? { balance } : {}), + } + } return snapshot } diff --git a/packages/opencode/src/tests/quota-normalize.test.ts b/packages/opencode/src/tests/quota-normalize.test.ts index 4fce310c..e671e275 100644 --- a/packages/opencode/src/tests/quota-normalize.test.ts +++ b/packages/opencode/src/tests/quota-normalize.test.ts @@ -197,6 +197,83 @@ describe('quota normalize → QuotaSnapshot', () => { expect(snapshot.resetCreditsApplicable).toBe(3) }) + it('wham keeps absent spend controls out of a normal snapshot', () => { + const withoutSpendControl = normalizeWham({ rate_limit: {} }) + const nullSpendControl = normalizeWham({ + rate_limit: {}, + spend_control: null, + } as Parameters[0]) + const nullIndividualLimit = normalizeWham({ + rate_limit: {}, + spend_control: { reached: false, individual_limit: null }, + } as Parameters[0]) + + expect(withoutSpendControl).toEqual({}) + expect(nullSpendControl.spendControl).toBeUndefined() + expect(nullIndividualLimit.spendControl).toBeUndefined() + }) + + it('wham normalizes a spend-control credit budget with string numerics', () => { + const snapshot = normalizeWham({ + rate_limit: {}, + credits: { + has_credits: true, + unlimited: false, + overage_limit_reached: false, + balance: null, + }, + spend_control: { + reached: false, + individual_limit: { + source: 'workspace_spend_controls', + unit: 'credit', + limit: '2500', + used: '501.7787666320801', + remaining: '1998.22123336792', + used_percent: '20', + remaining_percent: '80', + reset_at: '1790812800', + }, + }, + } as Parameters[0]) + + expect(snapshot.spendControl).toEqual({ + limit: 2500, + used: 501.7787666320801, + remaining: 1998.22123336792, + usedPercent: 20, + remainingPercent: 80, + resetsAt: new Date(1790812800 * 1000).toISOString(), + unit: 'credit', + source: 'workspace_spend_controls', + reached: false, + }) + expect(snapshot.credits).toEqual({ + hasCredits: true, + unlimited: false, + overageLimitReached: false, + }) + }) + + it('wham preserves a numeric credit balance when the provider supplies one', () => { + const snapshot = normalizeWham({ + rate_limit: {}, + credits: { + has_credits: true, + unlimited: false, + overage_limit_reached: false, + balance: '12.5', + }, + } as Parameters[0]) + + expect(snapshot.credits).toEqual({ + hasCredits: true, + unlimited: false, + overageLimitReached: false, + balance: 12.5, + }) + }) + it('omits invalid window lengths and reset-credit counts', () => { const headers = normalizeQuotaHeaders( new Headers({ From dde211ae3703c7adce370c88f25a4525e51d6ef1 Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:52:36 +0200 Subject: [PATCH 2/5] feat(quota): persist spend-control budgets --- packages/opencode/src/index.ts | 11 +++-- packages/opencode/src/sidebar-state.ts | 17 +++++++ .../opencode/src/tests/quota-push.test.ts | 34 ++++++++++++++ .../opencode/src/tests/sidebar-state.test.ts | 47 +++++++++++++++++++ 4 files changed, 104 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 408d3cd5..aa6a0087 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -763,11 +763,11 @@ export function findCachekeepFallbackAccount( ) } -// wham is the only source that reports reset-credit counts; header/WS pushes -// never carry the fields. An incoming push that omits them inherits the last -// known counts for the same account so the sidebar and the reset dialog do -// not lose them on every per-turn header/WS update — but an explicit incoming -// value (including 0) always wins over a stale cached one. +// wham is the only source that reports reset-credit counts and spend-control +// budgets; header/WS pushes never carry those fields. An incoming push that +// omits them inherits the last known reading for the same account so the +// sidebar and command output do not lose it on every per-turn update — but an +// explicit incoming value (including 0) always wins over a stale cached one. export function mergePushedQuotaMetadata( incoming: OAuthQuotaSnapshot, previous: OAuthQuotaSnapshot | undefined, @@ -777,6 +777,7 @@ export function mergePushedQuotaMetadata( for (const key of [ 'resetCreditsAvailable', 'resetCreditsApplicable', + 'spendControl', ] as const) { const carried = previous[key] if (merged[key] === undefined && carried !== undefined) { diff --git a/packages/opencode/src/sidebar-state.ts b/packages/opencode/src/sidebar-state.ts index c701324a..c8ba966c 100644 --- a/packages/opencode/src/sidebar-state.ts +++ b/packages/opencode/src/sidebar-state.ts @@ -6,10 +6,23 @@ export interface QuotaWindow { windowMinutes?: number } +export interface SpendControlReading { + limit: number + used: number + remaining: number + usedPercent: number + remainingPercent: number + resetsAt?: string + unit?: string + source?: string + reached: boolean +} + export interface AccountQuota { checkedAt?: number primary?: QuotaWindow secondary?: QuotaWindow + spendControl?: SpendControlReading resetCreditsAvailable?: number } @@ -1089,6 +1102,9 @@ function mergeQuotaByWindow( incoming.checkedAt, existing.checkedAt, ) + const spendControl = existingSnapshotIsFresher + ? (existing.spendControl ?? incoming.spendControl) + : (incoming.spendControl ?? existing.spendControl) let checkedAt: number | undefined for (const stamp of [ finiteWindowCheckedAt(primary), @@ -1102,6 +1118,7 @@ function mergeQuotaByWindow( ...incoming, primary, secondary, + ...(spendControl !== undefined ? { spendControl } : {}), checkedAt: checkedAt ?? incoming.checkedAt, } } diff --git a/packages/opencode/src/tests/quota-push.test.ts b/packages/opencode/src/tests/quota-push.test.ts index c62eef94..41649b93 100644 --- a/packages/opencode/src/tests/quota-push.test.ts +++ b/packages/opencode/src/tests/quota-push.test.ts @@ -507,6 +507,40 @@ describe('QuotaManager push', () => { expect(explicit.resetCreditsAvailable).toBe(4) }) + it('preserves the spend-control budget when a per-turn push omits it', () => { + const previous: OAuthQuotaSnapshot = { + primary: { + usedPercent: 10, + remainingPercent: 90, + checkedAt: 1, + windowMinutes: 300, + }, + spendControl: { + limit: 2500, + used: 501.7787666320801, + remaining: 1998.2212333679199, + usedPercent: 20.071150665283206, + remainingPercent: 79.9288493347168, + resetsAt: '2026-10-01T00:00:00.000Z', + unit: 'credits', + source: 'individual_limit', + reached: false, + }, + } + const incoming: OAuthQuotaSnapshot = { + primary: { + usedPercent: 20, + remainingPercent: 80, + checkedAt: 2, + windowMinutes: 300, + }, + } + + expect(mergePushedQuotaMetadata(incoming, previous).spendControl).toEqual( + previous.spendControl, + ) + }) + it('round-trips resetCreditsApplicable from wham normalization through the main quota cache', async () => { const { QuotaManager } = await import( '@cortexkit/openai-auth-core/internal' diff --git a/packages/opencode/src/tests/sidebar-state.test.ts b/packages/opencode/src/tests/sidebar-state.test.ts index dbfa319f..09268dd0 100644 --- a/packages/opencode/src/tests/sidebar-state.test.ts +++ b/packages/opencode/src/tests/sidebar-state.test.ts @@ -2938,6 +2938,53 @@ test('machine writes cannot clobber fresher main and fallback quota from disk', expect(written.activeRouting?.session?.activeId).toBe('fallback-1') }) +test('machine writes keep the fresher spend-control budget for the same account', async () => { + const tempDir = mkdtempSync(join(tmpdir(), 'oai-sb-spend-control-fresh-')) + const file = join(tempDir, 'sidebar-state.json') + const now = Date.now() + const stale = now - 10 * 60_000 + const currentBudget = { + limit: 2500, + used: 501.7787666320801, + remaining: 1998.2212333679199, + usedPercent: 20.071150665283206, + remainingPercent: 79.9288493347168, + resetsAt: '2026-10-01T00:00:00.000Z', + unit: 'credits', + source: 'individual_limit', + reached: false, + } + await setSidebarState( + make({ + main: { + ...main({ ...quota(10, now), spendControl: currentBudget }), + mainAccountId: 'acct-x', + }, + }), + file, + ) + + await setSidebarMachineState( + { + main: { + ...main({ + ...quota(90, stale), + spendControl: { ...currentBudget, used: 2400, remaining: 100 }, + }), + mainAccountId: 'acct-x', + }, + fallbacks: [], + route: 'main-first', + lastUpdated: now + 1, + }, + file, + ) + await drainSidebarWrites() + + const written = normalizeSidebarState(JSON.parse(readFileSync(file, 'utf8'))) + expect(written.main.quota?.spendControl).toEqual(currentBudget) +}) + test('machine write keeps the existing identity when the existing quota wins the merge (re-login race)', async () => { const tempDir = mkdtempSync(join(tmpdir(), 'oai-sb-identity-keep-')) const file = join(tempDir, 'sidebar-state.json') From cd61648e30b2db4d30c5de30625631fb54269408 Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:54:08 +0200 Subject: [PATCH 3/5] feat(tui): render spend-control quota bar --- .../src/tests/tui-quota-render.test.ts | 65 +++++++++++++++++++ packages/opencode/src/tui.tsx | 18 ++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/tests/tui-quota-render.test.ts b/packages/opencode/src/tests/tui-quota-render.test.ts index 212b08b5..bfd5cdb6 100644 --- a/packages/opencode/src/tests/tui-quota-render.test.ts +++ b/packages/opencode/src/tests/tui-quota-render.test.ts @@ -53,6 +53,71 @@ describe('dynamic quota TUI rows', () => { expect(buildQuotaRowsForDisplay({}, now, true)).toEqual([]) }) + test('renders a third credit-budget bar when spend control is present', () => { + const rows = buildQuotaRowsForDisplay( + { + primary: { + usedPercent: 3, + remainingPercent: 97, + windowMinutes: 300, + }, + secondary: { + usedPercent: 20, + remainingPercent: 80, + windowMinutes: 10_080, + }, + spendControl: { + limit: 2500, + used: 501.7787666320801, + remaining: 1998.2212333679199, + usedPercent: 20.071150665283206, + remainingPercent: 79.9288493347168, + resetsAt: '2026-10-01T00:00:00.000Z', + unit: 'credits', + source: 'individual_limit', + reached: false, + }, + }, + now, + false, + ) + + expect( + rows.map((row) => [row.key, row.label, row.window.usedPercent]), + ).toEqual([ + ['primary', '5h', 3], + ['secondary', '7d', 20], + ['spendControl', 'credits', 20.071150665283206], + ]) + }) + + test('renders the existing two-bar sidebar output without spend control', () => { + const rows = buildQuotaRowsForDisplay( + { + primary: { + usedPercent: 3, + remainingPercent: 97, + windowMinutes: 300, + }, + secondary: { + usedPercent: 20, + remainingPercent: 80, + windowMinutes: 10_080, + }, + }, + now, + false, + ) + + expect( + rows.map((row) => [row.key, row.label, row.window.usedPercent]), + ).toEqual([ + ['primary', '5h', 3], + ['secondary', '7d', 20], + ]) + expect(rows.some((row) => row.key === 'spendControl')).toBe(false) + }) + test('distinguishes an unloaded quota from a loaded snapshot with no windows', () => { expect(isQuotaLoaded(null)).toBe(false) expect(isQuotaLoaded({})).toBe(true) diff --git a/packages/opencode/src/tui.tsx b/packages/opencode/src/tui.tsx index 9328fd71..7f2895f8 100644 --- a/packages/opencode/src/tui.tsx +++ b/packages/opencode/src/tui.tsx @@ -241,7 +241,7 @@ function CollapsedRow(props: { } export interface QuotaDisplayRow { - key: 'primary' | 'secondary' + key: 'primary' | 'secondary' | 'spendControl' label: string window: QuotaWindow pacing: QuotaPacing | null @@ -254,7 +254,7 @@ export function buildQuotaRowsForDisplay( now: number, pacingEnabled: boolean, ): QuotaDisplayRow[] { - return getPresentQuotaWindows(quota).map((row) => ({ + const rows = getPresentQuotaWindows(quota).map((row) => ({ key: row.key, label: row.label, window: row.window, @@ -263,6 +263,20 @@ export function buildQuotaRowsForDisplay( ? computeQuotaPacing(row.window, row.windowMs, now) : null, })) + const spendControl = quota?.spendControl + if (spendControl) { + rows.push({ + key: 'spendControl', + label: 'credits', + window: { + usedPercent: spendControl.usedPercent, + remainingPercent: spendControl.remainingPercent, + resetsAt: spendControl.resetsAt, + }, + pacing: null, + }) + } + return rows } export function isQuotaLoaded(quota: AccountQuota | null): boolean { From f99e743f6d0f63750808ac14dbf31f3329b4694b Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:55:32 +0200 Subject: [PATCH 4/5] feat(quota): show spend-control budgets --- packages/core/src/commands.ts | 17 +++++++ packages/opencode/src/tests/commands.test.ts | 48 ++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/packages/core/src/commands.ts b/packages/core/src/commands.ts index e8013aba..d49866df 100644 --- a/packages/core/src/commands.ts +++ b/packages/core/src/commands.ts @@ -6,6 +6,7 @@ import { isSafeResetAccountKey, mutateAccounts, type OAuthAccount, + type OAuthSpendControlReading, type RoutingMode, readConfigRosterIds, } from './accounts' @@ -248,6 +249,16 @@ function quotaAge(checkedAt: number | undefined, now: number): string { return ` (${Math.floor(ageMs / 60_000)}m old)` } +function formatSpendControlLine( + spendControl: OAuthSpendControlReading, + indent = '', +): string { + const resets = spendControl.resetsAt + ? ` · resets ${spendControl.resetsAt}` + : '' + return `${indent}- credits: ${Math.round(spendControl.usedPercent)}% used (${Math.round(spendControl.used)} / ${Math.round(spendControl.limit)}, ${Math.round(spendControl.remaining)} remaining)${resets}` +} + async function executeQuotaCommand( ctx: CommandContext, ): Promise { @@ -274,6 +285,9 @@ async function executeQuotaCommand( if (q.resetCreditsAvailable !== undefined) { lines.push(`- resets: ${q.resetCreditsAvailable}`) } + if (q.spendControl) { + lines.push(formatSpendControlLine(q.spendControl)) + } } else { lines.push('No main quota snapshot available. Send a request first.') } @@ -298,6 +312,9 @@ async function executeQuotaCommand( if (entry.quota.resetCreditsAvailable !== undefined) { lines.push(` - resets: ${entry.quota.resetCreditsAvailable}`) } + if (entry.quota.spendControl) { + lines.push(formatSpendControlLine(entry.quota.spendControl, ' ')) + } } } diff --git a/packages/opencode/src/tests/commands.test.ts b/packages/opencode/src/tests/commands.test.ts index af5a6ddd..7436f71c 100644 --- a/packages/opencode/src/tests/commands.test.ts +++ b/packages/opencode/src/tests/commands.test.ts @@ -1853,6 +1853,54 @@ describe('commands', () => { expect(fb2Section).not.toContain('resets:') }) + test('quota command shows spend control only for accounts that report it', async () => { + const qm = new QuotaManager({ + configPath: getAccountStoragePath(), + storage: { version: 1 as const, accounts: [] }, + }) + qm.setMain('access-main', { + quota: { + ...makeQuotaSnapshot(15), + spendControl: { + limit: 2500, + used: 501.7787666320801, + remaining: 1998.2212333679199, + usedPercent: 20.071150665283206, + remainingPercent: 79.9288493347168, + resetsAt: '2026-10-01T00:00:00.000Z', + unit: 'credits', + source: 'individual_limit', + reached: false, + }, + }, + refreshAfter: Date.now() + 5 * 60 * 1000, + checkedAt: Date.now(), + }) + qm.setFallback('fb-1', { + quota: makeQuotaSnapshot(42), + refreshAfter: Date.now() + 5 * 60 * 1000, + checkedAt: Date.now(), + }) + const ctx: CommandContext = { + packageVersion: PackageVersion, + accountStoragePath: configPath, + accountStatePath: getAccountStatePath(configPath), + quotaManager: qm, + loadAccounts, + client: makeClient(), + } + + const payload = await buildDialogPayload('openai-quota', '', ctx) + const [mainSection, fallbackSection = ''] = payload.text.split( + '### Fallback accounts', + ) + + expect(mainSection).toContain( + '- credits: 20% used (502 / 2500, 1998 remaining) · resets 2026-10-01T00:00:00.000Z', + ) + expect(fallbackSection).not.toContain('credits:') + }) + test('refreshAllQuota with one failure → short retry state for failing account', async () => { const qm = new QuotaManager({ configPath: getAccountStoragePath(), From 87cdca917cd45e0cd069372ec2d296d45e66d237 Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:58:54 +0200 Subject: [PATCH 5/5] fix(quota): type spend-control metadata --- packages/opencode/src/index.ts | 7 ++++++- packages/opencode/src/tui.tsx | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index aa6a0087..5b306aa0 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -777,13 +777,18 @@ export function mergePushedQuotaMetadata( for (const key of [ 'resetCreditsAvailable', 'resetCreditsApplicable', - 'spendControl', ] as const) { const carried = previous[key] if (merged[key] === undefined && carried !== undefined) { merged[key] = carried } } + if ( + merged.spendControl === undefined && + previous.spendControl !== undefined + ) { + merged.spendControl = previous.spendControl + } return merged } diff --git a/packages/opencode/src/tui.tsx b/packages/opencode/src/tui.tsx index 7f2895f8..a9e40d86 100644 --- a/packages/opencode/src/tui.tsx +++ b/packages/opencode/src/tui.tsx @@ -254,7 +254,7 @@ export function buildQuotaRowsForDisplay( now: number, pacingEnabled: boolean, ): QuotaDisplayRow[] { - const rows = getPresentQuotaWindows(quota).map((row) => ({ + const rows: QuotaDisplayRow[] = getPresentQuotaWindows(quota).map((row) => ({ key: row.key, label: row.label, window: row.window,