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
6 changes: 5 additions & 1 deletion packages/core/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,11 @@ function formatSpendControlLine(
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}`
const amount = (value: number) => Math.round(value).toLocaleString('en-US')
const unit = spendControl.unit ?? 'unit'
const plural =
spendControl.limit === 1 || unit.endsWith('s') ? unit : `${unit}s`
return `${indent}- credits: ${Math.round(spendControl.usedPercent)}% used (${amount(spendControl.used)} / ${amount(spendControl.limit)} ${plural}, ${amount(spendControl.remaining)} remaining)${resets}`
}

async function executeQuotaCommand(
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/tests/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1868,7 +1868,7 @@ describe('commands', () => {
usedPercent: 20.071150665283206,
remainingPercent: 79.9288493347168,
resetsAt: '2026-10-01T00:00:00.000Z',
unit: 'credits',
unit: 'credit',
source: 'individual_limit',
reached: false,
},
Expand Down Expand Up @@ -1896,7 +1896,7 @@ describe('commands', () => {
)

expect(mainSection).toContain(
'- credits: 20% used (502 / 2500, 1998 remaining) · resets 2026-10-01T00:00:00.000Z',
'- credits: 20% used (502 / 2,500 credits, 1,998 remaining) · resets 2026-10-01T00:00:00.000Z',
)
expect(fallbackSection).not.toContain('credits:')
})
Expand Down
118 changes: 118 additions & 0 deletions packages/opencode/src/tests/tui-quota-render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,45 @@ import {
buildApplyRequest,
buildQuotaRowsForDisplay,
buildRoutingRowsForDisplay,
computeQuotaLabelWidth,
getAccountMetadataRows,
getQuotaMetadataRows,
isQuotaLoaded,
} from '../tui.tsx'

describe('dynamic quota TUI rows', () => {
const now = Date.UTC(2026, 6, 16, 12, 0, 0)

function projectQuotaRow(row: {
label: string
labelWidth: number
window: { usedPercent: number }
}): string {
return `${row.label.padEnd(row.labelWidth)}▓▓▓▓▓▓▓▓ ${String(Math.round(row.window.usedPercent)).padStart(3)}%`
}

const twoWindows = {
primary: { usedPercent: 0, remainingPercent: 100, windowMinutes: 300 },
secondary: {
usedPercent: 51,
remainingPercent: 49,
windowMinutes: 10_080,
},
}
const withSpendControl = {
...twoWindows,
spendControl: {
limit: 2500,
used: 501.7787666320801,
remaining: 1998.2212333679199,
usedPercent: 20.071150665283206,
remainingPercent: 79.9288493347168,
unit: 'credit',
source: 'individual_limit',
reached: false,
},
}

test('one 7-day primary window produces one 7d row paced over seven days', () => {
const rows = buildQuotaRowsForDisplay(
{
Expand Down Expand Up @@ -118,6 +150,57 @@ describe('dynamic quota TUI rows', () => {
expect(rows.some((row) => row.key === 'spendControl')).toBe(false)
})

test('a sidebar with no spend control anywhere keeps the three-column label width', () => {
const fallback = {
primary: { usedPercent: 12, remainingPercent: 88, windowMinutes: 300 },
}
const labelWidth = computeQuotaLabelWidth([twoWindows, fallback])

expect(labelWidth).toBe(3)
expect(
buildQuotaRowsForDisplay(twoWindows, now, false, labelWidth).map(
projectQuotaRow,
),
).toEqual(['5h ▓▓▓▓▓▓▓▓ 0%', '7d ▓▓▓▓▓▓▓▓ 51%'])
expect(
buildQuotaRowsForDisplay(fallback, now, false, labelWidth).map(
projectQuotaRow,
),
).toEqual(['5h ▓▓▓▓▓▓▓▓ 12%'])
})

test('one account with spend control widens every account label column', () => {
const labelWidth = computeQuotaLabelWidth([twoWindows, withSpendControl])

expect(labelWidth).toBe(8)
expect(
buildQuotaRowsForDisplay(twoWindows, now, false, labelWidth).map(
projectQuotaRow,
),
).toEqual(['5h ▓▓▓▓▓▓▓▓ 0%', '7d ▓▓▓▓▓▓▓▓ 51%'])
expect(
buildQuotaRowsForDisplay(withSpendControl, now, false, labelWidth).map(
projectQuotaRow,
),
).toEqual([
'5h ▓▓▓▓▓▓▓▓ 0%',
'7d ▓▓▓▓▓▓▓▓ 51%',
'credits ▓▓▓▓▓▓▓▓ 20%',
])
})

test('the longest label keeps a separator before its bar', () => {
const labelWidth = computeQuotaLabelWidth([withSpendControl])
const creditsRow = buildQuotaRowsForDisplay(
withSpendControl,
now,
false,
labelWidth,
).find((row) => row.key === 'spendControl')

expect(creditsRow?.label.padEnd(creditsRow.labelWidth)).toBe('credits ')
})

test('distinguishes an unloaded quota from a loaded snapshot with no windows', () => {
expect(isQuotaLoaded(null)).toBe(false)
expect(isQuotaLoaded({})).toBe(true)
Expand Down Expand Up @@ -171,6 +254,41 @@ describe('dynamic quota TUI rows', () => {
expect(tui.getAccountMetadataRows?.()).toEqual([])
})

test('renders grouped credit amounts with a pluralised unit', () => {
const rows = getAccountMetadataRows(undefined, {
limit: 2500,
used: 501.7787666320801,
remaining: 1998.22123336792,
usedPercent: 20.071150665283206,
remainingPercent: 79.9288493347168,
unit: 'credit',
source: 'individual_limit',
reached: false,
})
const rendered = JSON.stringify(rows)

expect(rows).toContainEqual({
label: 'credits',
value: '502 / 2,500 credits',
})
expect(rendered).not.toContain('501.7787666320801')
expect(rendered).not.toContain('$')
})

test('a single-credit budget keeps the unit singular', () => {
expect(
getAccountMetadataRows(undefined, {
limit: 1,
used: 1,
remaining: 0,
usedPercent: 100,
remainingPercent: 0,
unit: 'credit',
reached: true,
}),
).toContainEqual({ label: 'credits', value: '1 / 1 credit' })
})

test('modal routing apply sends sessionId on its RPC request', () => {
expect(buildApplyRequest('openai-routing', 'reset', 'session-a')).toEqual({
command: 'openai-routing',
Expand Down
Loading
Loading