| 1 | /** |
| 2 | * Shared formatting utilities for token counts and numeric displays. |
| 3 | * |
| 4 | * Centralises the previously duplicated fmtTokens / fmtFullTokens helpers |
| 5 | * scattered across ContextPanel and Composer so every surface renders |
| 6 | * identical compact token strings (e.g. "1.5K", "2.0M"). |
| 7 | */ |
| 8 | |
| 9 | /** Options controlling how {@link formatTokens} renders a count. */ |
| 10 | export interface TokenFormatOptions { |
| 11 | /** |
| 12 | * When `true` (the default) large values are abbreviated with K / M suffixes. |
| 13 | * When `false` the raw locale-formatted number is returned instead. |
| 14 | */ |
| 15 | compact?: boolean; |
| 16 | |
| 17 | /** Maximum fractional digits kept in the abbreviated form (default `1`). */ |
| 18 | decimals?: number; |
| 19 | |
| 20 | /** |
| 21 | * Minimum value before the K-suffix kicks in (default `1000`). |
| 22 | * Useful for contexts that want to show plain numbers up to a higher bound. |
| 23 | */ |
| 24 | threshold?: number; |
| 25 | } |
| 26 | |
| 27 | /** Strip trailing zeros after the decimal point: "1.50" → "1.5", "1.00" → "1". */ |
| 28 | function stripTrailingZeros(s: string): string { |
| 29 | return s.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, ""); |
| 30 | } |
| 31 | |
| 32 | /** |
| 33 | * Format a token count for human-readable display. |
| 34 | * |
| 35 | * Behaviour: |
| 36 | * - Returns `"-"` for missing, zero, or negative inputs. |
| 37 | * - In compact mode (default): values >= 1 000 000 use the `M` suffix, |
| 38 | * values >= `threshold` (default 1 000) use the `K` suffix, and smaller |
| 39 | * values are rendered as-is. |
| 40 | * - Trailing `.0` is stripped so `1000` becomes `"1K"` rather than `"1.0K"`. |
| 41 | * - In non-compact mode `toLocaleString()` is used for locale-aware grouping. |
| 42 | * |
| 43 | * @example |
| 44 | * formatTokens(1500) // "1.5K" |
| 45 | * formatTokens(1000) // "1K" |
| 46 | * formatTokens(142000) // "142K" |
| 47 | * formatTokens(1_500_000) // "1.5M" |
| 48 | * formatTokens(999) // "999" |
| 49 | * formatTokens(undefined) // "-" |
| 50 | */ |
| 51 | export function formatTokens(tokens: number | undefined, options?: TokenFormatOptions): string { |
| 52 | if (typeof tokens !== "number" || tokens <= 0) return "-"; |
| 53 | |
| 54 | const { compact = true, decimals = 1, threshold = 1000 } = options ?? {}; |
| 55 | |
| 56 | if (!compact) { |
| 57 | return tokens.toLocaleString(); |
| 58 | } |
| 59 | |
| 60 | if (tokens >= 1_000_000) { |
| 61 | return `${stripTrailingZeros((tokens / 1_000_000).toFixed(decimals))}M`; |
| 62 | } |
| 63 | if (tokens >= threshold) { |
| 64 | return `${stripTrailingZeros((tokens / 1000).toFixed(decimals))}K`; |
| 65 | } |
| 66 | |
| 67 | return String(tokens); |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Convenience wrapper for optional token counts (session / turn totals). |
| 72 | * |
| 73 | * Delegates to {@link formatTokens} and returns `"-"` when the value is |
| 74 | * absent, zero, or negative — matching the old `fmtOptionalTokens` helper |
| 75 | * it replaces. |
| 76 | */ |
| 77 | export function formatOptionalTokens(tokens?: number | null, options?: TokenFormatOptions): string { |
| 78 | if (typeof tokens !== "number" || tokens <= 0) return "-"; |
| 79 | return formatTokens(tokens, options); |
| 80 | } |
| 81 |