diff --git a/packages/cli/src/ui/utils/formatters.test.ts b/packages/cli/src/ui/utils/formatters.test.ts index bafc04b555..675ac8cfb8 100644 --- a/packages/cli/src/ui/utils/formatters.test.ts +++ b/packages/cli/src/ui/utils/formatters.test.ts @@ -10,9 +10,60 @@ import { formatBytes, formatTimeAgo, stripReferenceContent, + formatResetTime, } from './formatters.js'; describe('formatters', () => { + describe('formatResetTime', () => { + const NOW = new Date('2026-02-20T12:00:00Z'); + + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(NOW); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('should format ISO date strings', () => { + const resetTime = '2026-02-20T13:30:00Z'; // 1h 30m from now + expect(formatResetTime(resetTime)).toBe('resets in 1h 30m'); + }); + + it('should format unix timestamps (milliseconds)', () => { + const resetTime = (NOW.getTime() + 65 * 60 * 1000).toString(); // 1h 5m + expect(formatResetTime(resetTime)).toBe('resets in 1h 5m'); + }); + + it('should format unix timestamps (seconds)', () => { + const resetTime = (Math.floor(NOW.getTime() / 1000) + 120).toString(); // 2m + expect(formatResetTime(resetTime)).toBe('resets in 2m'); + }); + + it('should return "Resetting..." for past times', () => { + const resetTime = '2026-02-20T11:59:59Z'; + expect(formatResetTime(resetTime)).toBe('Resetting...'); + }); + + it('should return "< 1m" for imminent resets', () => { + const resetTime = new Date(NOW.getTime() + 30 * 1000).toISOString(); + expect(formatResetTime(resetTime)).toBe('< 1m'); + }); + + it('should support capitalization and prefix options', () => { + const resetTime = '2026-02-20T14:00:00Z'; // 2h + expect( + formatResetTime(resetTime, { capitalize: true, includePrefix: true }), + ).toBe('Resets in 2h'); + expect(formatResetTime(resetTime, { includePrefix: false })).toBe('2h'); + }); + + it('should return empty string for invalid dates', () => { + expect(formatResetTime('not-a-date')).toBe(''); + }); + }); + describe('formatBytes', () => { it('should format bytes into KB', () => { expect(formatBytes(12345)).toBe('12.1 KB'); diff --git a/packages/cli/src/ui/utils/formatters.ts b/packages/cli/src/ui/utils/formatters.ts index 0b2d6d138b..81802bba0e 100644 --- a/packages/cli/src/ui/utils/formatters.ts +++ b/packages/cli/src/ui/utils/formatters.ts @@ -125,23 +125,16 @@ export const formatResetTime = ( const diff = date.getTime() - Date.now(); if (diff <= 0) return 'Resetting...'; - const totalMinutes = Math.ceil(diff / (1000 * 60)); + const totalMinutes = Math.floor(diff / (1000 * 60)); const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; const fmt = (val: number, unit: 'hour' | 'minute') => - new Intl.RelativeTimeFormat('en', { - style: 'narrow', - numeric: 'always', - }) - .formatToParts(val, unit) - .filter( - (p) => - p.type !== 'literal' || (p.value !== 'in ' && p.value !== ' ago'), - ) - .map((p) => p.value) - .join('') - .trim(); + new Intl.NumberFormat('en', { + style: 'unit', + unit, + unitDisplay: 'narrow', + }).format(val); let timeStr = ''; if (hours > 0 && minutes > 0) { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 8697993e44..0283e64d62 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -68,7 +68,9 @@ import { ideContextStore } from '../ide/ideContext.js'; import { WriteTodosTool } from '../tools/write-todos.js'; import type { FileSystemService } from '../services/fileSystemService.js'; import { StandardFileSystemService } from '../services/fileSystemService.js'; -import { logRipgrepFallback, logFlashFallback , +import { + logRipgrepFallback, + logFlashFallback, logApprovalModeSwitch, logApprovalModeDuration, } from '../telemetry/loggers.js'; @@ -106,7 +108,11 @@ import type { EventEmitter } from 'node:events'; import { PolicyEngine } from '../policy/policy-engine.js'; import { ApprovalMode, type PolicyEngineConfig } from '../policy/types.js'; import { HookSystem } from '../hooks/index.js'; -import type { UserTierId , RetrieveUserQuotaResponse , AdminControlsSettings } from '../code_assist/types.js'; +import type { + UserTierId, + RetrieveUserQuotaResponse, + AdminControlsSettings, +} from '../code_assist/types.js'; import type { HierarchicalMemory } from './memory.js'; import { getCodeAssistServer } from '../code_assist/codeAssist.js'; import type { Experiments } from '../code_assist/experiments/experiments.js'; @@ -1335,8 +1341,8 @@ export class Config { // For reset time, take the one that is furthest in the future (most conservative) const resetTime = [proQuota?.resetTime, flashQuota?.resetTime] .filter((t): t is string => !!t) - .sort() - .reverse()[0]; + .map((t) => ({ original: t, date: new Date(t) })) + .sort((a, b) => b.date.getTime() - a.date.getTime())[0]?.original; return { remaining: (proQuota?.remaining ?? 0) + (flashQuota?.remaining ?? 0),