diff --git a/src/tui/dashboard.ts b/src/tui/dashboard.ts index 375795f..509d325 100644 --- a/src/tui/dashboard.ts +++ b/src/tui/dashboard.ts @@ -42,6 +42,7 @@ import { themes, throughputColumns } from './format.ts' +import { isFiveHourUsageWindow, visibleUsageWindows } from './usage-windows.ts' type Tab = 'accounts' | 'analytics' | 'settings' const TABS: readonly Tab[] = ['accounts', 'analytics', 'settings'] @@ -186,7 +187,7 @@ function accountsWidth(ctx: Ctx, snapshot: DashboardSnapshot): number { const state = snapshot.providers.find(s => s.provider === account.provider) const hidden = state?.policy.hiddenWindowIds ?? [] const usage = snapshot.usage.find(u => u.accountId === account.id) - const visible = visibleWindows(usage?.windows ?? [], hidden) + const visible = visibleUsageWindows(usage?.windows ?? [], hidden) const tag = ctx.tier === 'compact' ? null : planTag(account.plan) const base = 3 + @@ -209,10 +210,6 @@ function accountsWidth(ctx: Ctx, snapshot: DashboardSnapshot): number { return Math.min(CONTENT_MAX, ctx.columns - 2, widest + 2) } -function isFiveHour(window: UsageWindow): boolean { - return /5 ?h/i.test(window.label) || window.id === 'session' || window.id === 'five-hour' -} - function recentSwitch(ctx: Ctx, state: ProviderState | undefined): boolean { if (state?.switchedAt == null) { return false @@ -237,7 +234,10 @@ function orderedRows(snapshot: DashboardSnapshot): Row[] { const hidden = state?.policy.hiddenWindowIds ?? [] const pressure = (accountId: string): number => { const windows = snapshot.usage.find(u => u.accountId === accountId)?.windows ?? [] - return visibleWindows(windows, hidden).reduce((max, w) => Math.max(max, w.usedPercent), -1) + return visibleUsageWindows(windows, hidden).reduce( + (max, window) => Math.max(max, window.usedPercent), + -1 + ) } const accounts = snapshot.accounts .filter(account => account.provider === provider) @@ -253,17 +253,6 @@ function orderedRows(snapshot: DashboardSnapshot): Row[] { return rows } -function visibleWindows( - windows: readonly UsageWindow[], - hiddenIds: readonly string[] -): UsageWindow[] { - const rank = (window: UsageWindow): number => - isFiveHour(window) ? 0 : /scoped|fable|opus|sonnet|spark/i.test(window.id) ? 2 : 1 - return hardWindows(windows) - .filter(window => !hiddenIds.includes(window.id)) - .sort((left, right) => rank(left) - rank(right)) -} - function windowCell(ctx: Ctx, window: UsageWindow, barWidth: number, withReset: boolean) { const reset = withReset ? shortReset(window.resetAt, ctx.now) : null return [ @@ -337,7 +326,7 @@ function accountLine( fg: rgb(badge?.color ?? ctx.theme.dim) }) ] - const visible = visibleWindows(usage?.windows ?? [], hiddenIds) + const visible = visibleUsageWindows(usage?.windows ?? [], hiddenIds) const spend = spendCell(ctx.tier, account, usage) if (spend !== null) { children.push( @@ -496,7 +485,7 @@ function sessionResets(ctx: Ctx, snapshot: DashboardSnapshot): string | null { } const account = snapshot.accounts.find(a => a.id === state.activeAccountId) const windows = snapshot.usage.find(u => u.accountId === state.activeAccountId)?.windows ?? [] - const reset = shortReset(windows.find(isFiveHour)?.resetAt ?? null, ctx.now) + const reset = shortReset(windows.find(isFiveHourUsageWindow)?.resetAt ?? null, ctx.now) if (account === undefined || reset === null) { return [] } @@ -780,7 +769,7 @@ function providerWindows(snapshot: DashboardSnapshot, provider: ProviderId): Usa } } return [...seen.values()].sort( - (left, right) => (isFiveHour(left) ? 0 : 1) - (isFiveHour(right) ? 0 : 1) + (left, right) => (isFiveHourUsageWindow(left) ? 0 : 1) - (isFiveHourUsageWindow(right) ? 0 : 1) ) } diff --git a/src/tui/usage-windows.test.ts b/src/tui/usage-windows.test.ts new file mode 100644 index 0000000..201bd7f --- /dev/null +++ b/src/tui/usage-windows.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from 'bun:test' +import type { UsageWindow } from '../domain.ts' +import { visibleUsageWindows } from './usage-windows.ts' + +function usageWindow(id: string, label: string): UsageWindow { + return { id, kind: 'hard', label, resetAt: null, usedPercent: 0 } +} + +describe('visibleUsageWindows', () => { + test('keeps recognized model limits ahead of unknown limits', () => { + const windows = [ + usageWindow('session', '5h session'), + usageWindow('weekly_all', '7 day · all models'), + usageWindow('weekly_scoped:fable', '7 day · Fable'), + usageWindow('nimbus_quill', 'Nimbus Quill') + ] + + expect(visibleUsageWindows(windows, []).map(window => window.id)).toEqual([ + 'session', + 'weekly_all', + 'weekly_scoped:fable', + 'nimbus_quill' + ]) + }) + + test('shows Fable in the compact pair when the all-model limit is hidden', () => { + const windows = [ + usageWindow('session', '5h session'), + usageWindow('weekly_all', '7 day · all models'), + usageWindow('weekly_scoped:fable', '7 day · Fable'), + usageWindow('nimbus_quill', 'Nimbus Quill') + ] + + expect( + visibleUsageWindows(windows, ['weekly_all']) + .slice(0, 2) + .map(window => window.label) + ).toEqual(['5h session', '7 day · Fable']) + }) +}) diff --git a/src/tui/usage-windows.ts b/src/tui/usage-windows.ts new file mode 100644 index 0000000..3c1327b --- /dev/null +++ b/src/tui/usage-windows.ts @@ -0,0 +1,29 @@ +import type { UsageWindow } from '../domain.ts' + +export function isFiveHourUsageWindow(window: UsageWindow): boolean { + return /5 ?h/i.test(window.label) || window.id === 'session' || window.id === 'five-hour' +} + +function usageWindowPriority(window: UsageWindow): number { + const identity = `${window.id} ${window.label}` + switch (true) { + case isFiveHourUsageWindow(window): + return 0 + case /^(7 day(?: · all models)?)$/i.test(window.label): + case /^(weekly_all|seven_day|weekly|codex:primary)$/.test(window.id): + return 1 + case /scoped|fable|opus|sonnet|spark/i.test(identity): + return 2 + default: + return 3 + } +} + +export function visibleUsageWindows( + windows: readonly UsageWindow[], + hiddenWindowIds: readonly string[] +): UsageWindow[] { + return windows + .filter(window => window.kind === 'hard' && !hiddenWindowIds.includes(window.id)) + .sort((left, right) => usageWindowPriority(left) - usageWindowPriority(right)) +}