π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes formatFileSize, formatSecondsShort, formatDuration, formatNumber, and formatTokens (and more) β mainly functions, hooks, or classes. It composes internal code from intl and truncate (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
// Pure display formatters β leaf-safe (no Ink). Width-aware truncation lives in ./truncate.ts. import { getRelativeTimeFormat, getTimeZone } from './intl.js' /**
π€ Exports (heuristic)
formatFileSizeformatSecondsShortformatDurationformatNumberformatTokensformatRelativeTimeformatRelativeTimeAgoformatLogMetadataformatResetTimeformatResetTexttruncatetruncatePathMiddletruncateStartToWidthtruncateToWidthtruncateToWidthNoEllipsiswrapText
π₯οΈ Source preview
// Pure display formatters β leaf-safe (no Ink). Width-aware truncation lives in ./truncate.ts.
import { getRelativeTimeFormat, getTimeZone } from './intl.js'
/**
* Formats a byte count to a human-readable string (KB, MB, GB).
* @example formatFileSize(1536) β "1.5KB"
*/
export function formatFileSize(sizeInBytes: number): string {
const kb = sizeInBytes / 1024
if (kb < 1) {
return `${sizeInBytes} bytes`
}
if (kb < 1024) {
return `${kb.toFixed(1).replace(/\.0$/, '')}KB`
}
const mb = kb / 1024
if (mb < 1024) {
return `${mb.toFixed(1).replace(/\.0$/, '')}MB`
}
const gb = mb / 1024
return `${gb.toFixed(1).replace(/\.0$/, '')}GB`
}
/**
* Formats milliseconds as seconds with 1 decimal place (e.g. `1234` β `"1.2s"`).
* Unlike formatDuration, always keeps the decimal β use for sub-minute timings
* where the fractional second is meaningful (TTFT, hook durations, etc.).
*/
export function formatSecondsShort(ms: number): string {
return `${(ms / 1000).toFixed(1)}s`
}
export function formatDuration(
ms: number,
options?: { hideTrailingZeros?: boolean; mostSignificantOnly?: boolean },
): string {
if (ms < 60000) {
// Special case for 0
if (ms === 0) {
return '0s'
}
// For durations < 1s, show 1 decimal place (e.g., 0.5s)
if (ms < 1) {
const s = (ms / 1000).toFixed(1)
return `${s}s`
}
const s = Math.floor(ms / 1000).toString()
return `${s}s`
}
let days = Math.floor(ms / 86400000)
let hours = Math.floor((ms % 86400000) / 3600000)
let minutes = Math.floor((ms % 3600000) / 60000)
let seconds = Math.round((ms % 60000) / 1000)
// Handle rounding carry-over (e.g., 59.5s rounds to 60s)
if (seconds === 60) {
seconds = 0
minutes++
}
if (minutes === 60) {
minutes = 0
hours++
}
if (hours === 24) {
hours = 0
days++
}
const hide = options?.hideTrailingZeros
if (options?.mostSignificantOnly) {
if (days > 0) return `${days}d`
if (hours > 0) return `${hours}h`
if (minutes > 0) return `${minutes}m`
return `${seconds}s`
}
if (days > 0) {
if (hide && hours === 0 && minutes === 0) return `${days}d`
if (hide && minutes === 0) return `${days}d ${hours}h`
return `${days}d ${hours}h ${minutes}m`
}
if (hours > 0) {
if (hide && minutes === 0 && seconds === 0) return `${hours}h`
if (hide && seconds === 0) return `${hours}h ${minutes}m`
return `${hours}h ${minutes}m ${seconds}s`
}
if (minutes > 0) {
if (hide && seconds === 0) return `${minutes}m`
return `${minutes}m ${seconds}s`
}
return `${seconds}s`
}
// `new Intl.NumberFormat` is expensive, so cache formatters for reuse
let numberFormatterForConsistentDecimals: Intl.NumberFormat | null = null
let numberFormatterForInconsistentDecimals: Intl.NumberFormat | null = null
const getNumberFormatter = (
useConsistentDecimals: boolean,
): Intl.NumberFormat => {
if (useConsistentDecimals) {
if (!numberFormatterForConsistentDecimals) {
numberFormatterForConsistentDecimals = new Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 1,
minimumFractionDigits: 1,
})
}
return numberFormatterForConsistentDecimals
} else {
if (!numberFormatterForInconsistentDecimals) {
numberFormatterForInconsistentDecimals = new Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 1,
minimumFractionDigits: 0,
})
}
return numberFormatterForInconsistentDecimals
}
}
export function formatNumber(number: number): string {
// Only use minimumFractionDigits for numbers that will be shown in compact notation
const shouldUseConsistentDecimals = number >= 1000
return getNumberFormatter(shouldUseConsistentDecimals)
.format(number) // eg. "1321" => "1.3K", "900" => "900"
.toLowerCase() // eg. "1.3K" => "1.3k", "1.0K" => "1.0k"
}
export function formatTokens(count: number): string {
return formatNumber(count).replace('.0', '')
}
type RelativeTimeStyle = 'long' | 'short' | 'narrow'
type RelativeTimeOptions = {
style?: RelativeTimeStyle
numeric?: 'always' | 'auto'
}
export function formatRelativeTime(
date: Date,
options: RelativeTimeOptions & { now?: Date } = {},
): string {
const { style = 'narrow', numeric = 'always', now = new Date() } = options
const diffInMs = date.getTime() - now.getTime()
// Use Math.trunc to truncate towards zero for both positive and negative values
const diffInSeconds = Math.trunc(diffInMs / 1000)
// Define time intervals with custom short units
const intervals = [
{ unit: 'year', seconds: 31536000, shortUnit: 'y' },
{ unit: 'month', seconds: 2592000, shortUnit: 'mo' },
{ unit: 'week', seconds: 604800, shortUnit: 'w' },
{ unit: 'day', seconds: 86400, shortUnit: 'd' },
{ unit: 'hour', seconds: 3600, shortUnit: 'h' },
{ unit: 'minute', seconds: 60, shortUnit: 'm' },
{ unit: 'second', seconds: 1, shortUnit: 's' },
] as const
// Find the appropriate unit
for (const { unit, seconds: intervalSeconds, shortUnit } of intervals) {
if (Math.abs(diffInSeconds) >= intervalSeconds) {
const value = Math.trunc(diffInSeconds / intervalSeconds)
// For short style, use custom format
if (style === 'narrow') {
return diffInSeconds < 0
? `${Math.abs(value)}${shortUnit} ago`
: `in ${value}${shortUnit}`
}
// For days and longer, use long style regardless of the style parameter
return getRelativeTimeFormat('long', numeric).format(value, unit)
}
}
// For values less than 1 second
if (style === 'narrow') {
return diffInSeconds <= 0 ? '0s ago' : 'in 0s'
}
return getRelativeTimeFormat(style, numeric).format(0, 'second')
}
export function formatRelativeTimeAgo(
date: Date,
options: RelativeTimeOptions & { now?: Date } = {},
): string {
const { now = new Date(), ...restOptions } = options
if (date > now) {
// For future dates, just return the relative time without "ago"
return formatRelativeTime(date, { ...restOptions, now })
}
// For past dates, force numeric: 'always' to ensure we get "X units ago"
return formatRelativeTime(date, { ...restOptions, numeric: 'always', now })
}
/**
* Formats log metadata for display (time, size or message count, branch, tag, PR)
*/
export function formatLogMetadata(log: {
modified: Date
messageCount: number
fileSize?: number
gitBranch?: string
tag?: string
agentSetting?: string
prNumber?: number
prRepository?: string
}): string {
const sizeOrCount =
log.fileSize !== undefined
? formatFileSize(log.fileSize)
: `${log.messageCount} messages`
const parts = [
formatRelativeTimeAgo(log.modified, { style: 'short' }),
...(log.gitBranch ? [log.gitBranch] : []),
sizeOrCount,
]
if (log.tag) {
parts.push(`#${log.tag}`)
}
if (log.agentSetting) {
parts.push(`@${log.agentSetting}`)
}
if (log.prNumber) {
parts.push(
log.prRepository
? `${log.prRepository}#${log.prNumber}`
: `#${log.prNumber}`,
)
}
return parts.join(' Β· ')
}
export function formatResetTime(
timestampInSeconds: number | undefined,
showTimezone: boolean = false,
showTime: boolean = true,
): string | undefined {
if (!timestampInSeconds) return undefined
const date = new Date(timestampInSeconds * 1000)
const now = new Date()
const minutes = date.getMinutes()
// Calculate hours until reset
const hoursUntilReset = (date.getTime() - now.getTime()) / (1000 * 60 * 60)
// If reset is more than 24 hours away, show the date as well
if (hoursUntilReset > 24) {
// Show date and time for resets more than a day away
const dateOptions: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
hour: showTime ? 'numeric' : undefined,
minute: !showTime || minutes === 0 ? undefined : '2-digit',
hour12: showTime ? true : undefined,
}
// Add year if it's not the current year
if (date.getFullYear() !== now.getFullYear()) {
dateOptions.year = 'numeric'
}
const dateString = date.toLocaleString('en-US', dateOptions)
// Remove the space before AM/PM and make it lowercase
return (
dateString.replace(/ ([AP]M)/i, (_match, ampm) => ampm.toLowerCase()) +
(showTimezone ? ` (${getTimeZone()})` : '')
)
}
// For resets within 24 hours, show just the time (existing behavior)
const timeString = date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: minutes === 0 ? undefined : '2-digit',
hour12: true,
})
// Remove the space before AM/PM and make it lowercase, then add timezone
return (
timeString.replace(/ ([AP]M)/i, (_match, ampm) => ampm.toLowerCase()) +
(showTimezone ? ` (${getTimeZone()})` : '')
)
}
export function formatResetText(
resetsAt: string,
showTimezone: boolean = false,
showTime: boolean = true,
): string {
const dt = new Date(resetsAt)
return `${formatResetTime(Math.floor(dt.getTime() / 1000), showTimezone, showTime)}`
}
// Back-compat: truncate helpers moved to ./truncate.ts (needs ink/stringWidth)
export {
truncate,
truncatePathMiddle,
truncateStartToWidth,
truncateToWidth,
truncateToWidthNoEllipsis,
wrapText,
} from './truncate.js'