diff --git a/packages/cli/src/ui/components/QuotaStatsInfo.tsx b/packages/cli/src/ui/components/QuotaStatsInfo.tsx index 35877f2a73..e6bf31ab1b 100644 --- a/packages/cli/src/ui/components/QuotaStatsInfo.tsx +++ b/packages/cli/src/ui/components/QuotaStatsInfo.tsx @@ -85,13 +85,7 @@ export const QuotaStatsInfo: React.FC = ({ : `${displayPercentage.toFixed(0)}%`} {displayPercentage !== 0 && usage remaining} - {displayResetTime && - `, ${(function (t) { - const formatted = formatResetTime(t); - return formatted === 'Resetting...' || formatted === '< 1m' - ? formatted - : `resets in ${formatted}`; - })(displayResetTime)}`} + {displayResetTime && `, ${formatResetTime(displayResetTime)}`} )} {showDetails && ( diff --git a/packages/cli/src/ui/components/StatsDisplay.tsx b/packages/cli/src/ui/components/StatsDisplay.tsx index 38dbb4bcbc..5288f1ddeb 100644 --- a/packages/cli/src/ui/components/StatsDisplay.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.tsx @@ -278,7 +278,13 @@ const ModelUsageTable: React.FC<{ - {row.cachedTokens} + + {row.cachedTokens} + {row.bucket && row.bucket.remainingFraction != null && ( - + {(row.bucket.remainingFraction * 100).toFixed(1)}% {row.bucket.resetTime && ( {' '} ( - {(function (t) { - const formatted = formatResetTime(t); - return formatted === 'Resetting...' || - formatted === '< 1m' - ? formatted - : `Resets in ${formatted}`; - })(row.bucket.resetTime)} + {formatResetTime(row.bucket.resetTime, { + capitalize: true, + })} ) )} diff --git a/packages/cli/src/ui/utils/displayUtils.ts b/packages/cli/src/ui/utils/displayUtils.ts index 6fb6faec83..e311aa4974 100644 --- a/packages/cli/src/ui/utils/displayUtils.ts +++ b/packages/cli/src/ui/utils/displayUtils.ts @@ -16,8 +16,8 @@ export const USER_AGREEMENT_RATE_MEDIUM = 45; export const CACHE_EFFICIENCY_HIGH = 40; export const CACHE_EFFICIENCY_MEDIUM = 15; -export const QUOTA_THRESHOLD_HIGH = 50; -export const QUOTA_THRESHOLD_MEDIUM = 20; +export const QUOTA_THRESHOLD_HIGH = 20; +export const QUOTA_THRESHOLD_MEDIUM = 5; // --- Color Logic --- export const getStatusColor = ( diff --git a/packages/cli/src/ui/utils/formatters.ts b/packages/cli/src/ui/utils/formatters.ts index d68fccdf7b..0b2d6d138b 100644 --- a/packages/cli/src/ui/utils/formatters.ts +++ b/packages/cli/src/ui/utils/formatters.ts @@ -97,17 +97,24 @@ export function stripReferenceContent(text: string): string { return text.replace(pattern, '').trim(); } +const UNIX_SECONDS_THRESHOLD = 10_000_000_000; -export const formatResetTime = (resetTime: string): string => { +export const formatResetTime = ( + resetTime: string, + options: { capitalize?: boolean; includePrefix?: boolean } = {}, +): string => { + const { capitalize = false, includePrefix = true } = options; let date = new Date(resetTime); // If invalid, try parsing as a number (unix timestamp) if (isNaN(date.getTime())) { const timestamp = parseInt(resetTime, 10); if (!isNaN(timestamp)) { - // Heuristic: If the timestamp is less than 10^12, it is likely in seconds - // (as 10^12 ms is year 2001, while 10^12 s is far in the future). - date = new Date(timestamp < 10000000000 ? timestamp * 1000 : timestamp); + // Heuristic: If the timestamp is less than 10^10, it is likely in seconds + // (as 10^10 ms is year 1970, while 10^10 s is year 2286). + date = new Date( + timestamp < UNIX_SECONDS_THRESHOLD ? timestamp * 1000 : timestamp, + ); } } @@ -123,19 +130,34 @@ export const formatResetTime = (resetTime: string): string => { const minutes = totalMinutes % 60; const fmt = (val: number, unit: 'hour' | 'minute') => - new Intl.NumberFormat('en', { - style: 'unit', - unit, - unitDisplay: 'narrow', - }).format(val); + 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(); + let timeStr = ''; if (hours > 0 && minutes > 0) { - return `${fmt(hours, 'hour')} ${fmt(minutes, 'minute')}`; + timeStr = `${fmt(hours, 'hour')} ${fmt(minutes, 'minute')}`; } else if (hours > 0) { - return fmt(hours, 'hour'); + timeStr = fmt(hours, 'hour'); } else if (minutes > 0) { - return fmt(minutes, 'minute'); + timeStr = fmt(minutes, 'minute'); } else { - return '< 1m'; + timeStr = '< 1m'; } + + if (timeStr === '< 1m' || !includePrefix) { + return timeStr; + } + + const prefix = capitalize ? 'Resets in ' : 'resets in '; + return `${prefix}${timeStr}`; }; diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 2d9225b5d0..8697993e44 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -68,7 +68,10 @@ 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 } from '../telemetry/loggers.js'; +import { logRipgrepFallback, logFlashFallback , + logApprovalModeSwitch, + logApprovalModeDuration, +} from '../telemetry/loggers.js'; import { RipgrepFallbackEvent, FlashFallbackEvent, @@ -103,9 +106,7 @@ 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 } from '../code_assist/types.js'; -import type { RetrieveUserQuotaResponse } from '../code_assist/types.js'; -import type { 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'; @@ -119,10 +120,6 @@ import { debugLogger } from '../utils/debugLogger.js'; import { SkillManager, type SkillDefinition } from '../skills/skillManager.js'; import { startupProfiler } from '../telemetry/startupProfiler.js'; import type { AgentDefinition } from '../agents/types.js'; -import { - logApprovalModeSwitch, - logApprovalModeDuration, -} from '../telemetry/loggers.js'; import { fetchAdminControls } from '../code_assist/admin/admin_controls.js'; import { isSubpath } from '../utils/paths.js'; import { UserHintService } from './userHintService.js'; @@ -1335,12 +1332,11 @@ export class Config { const flashQuota = this.modelQuotas.get(flashModel); if (proQuota || flashQuota) { - // For reset time, take the one that is nearest in the future (soonest reset) + // 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) - .map((t) => new Date(t)) - .sort((a, b) => a.getTime() - b.getTime())[0] - ?.toISOString(); + .sort() + .reverse()[0]; return { remaining: (proQuota?.remaining ?? 0) + (flashQuota?.remaining ?? 0),