|
|
@@ -97,16 +97,54 @@ function activityDuration(
|
|
|
return timing.settledMs + Math.max(0, end - timing.active.since)
|
|
|
}
|
|
|
|
|
|
-/** Format a non-negative duration to seconds without dropping larger units. */
|
|
|
-function formatDuration(ms: number, t: TranslateNS<typeof NS>): string {
|
|
|
+interface DurationParts {
|
|
|
+ seconds: number
|
|
|
+ minutes: number
|
|
|
+ hours: number
|
|
|
+ days: number
|
|
|
+ totalMinutes: number
|
|
|
+ totalHours: number
|
|
|
+}
|
|
|
+
|
|
|
+function splitDuration(ms: number): DurationParts {
|
|
|
const totalSeconds = Math.floor(Math.max(0, ms) / 1_000)
|
|
|
- const seconds = totalSeconds % 60
|
|
|
const totalMinutes = Math.floor(totalSeconds / 60)
|
|
|
- const minutes = totalMinutes % 60
|
|
|
- const hours = Math.floor(totalMinutes / 60)
|
|
|
- if (hours > 0) {
|
|
|
+ const totalHours = Math.floor(totalMinutes / 60)
|
|
|
+ return {
|
|
|
+ seconds: totalSeconds % 60,
|
|
|
+ minutes: totalMinutes % 60,
|
|
|
+ hours: totalHours % 24,
|
|
|
+ days: Math.floor(totalHours / 24),
|
|
|
+ totalMinutes,
|
|
|
+ totalHours,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** Format a duration with decreasing visual precision at larger scales. */
|
|
|
+function formatDuration(ms: number, t: TranslateNS<typeof NS>): string {
|
|
|
+ const { seconds, minutes, hours, days, totalMinutes, totalHours } = splitDuration(ms)
|
|
|
+ if (days >= 365) {
|
|
|
+ const years = Math.floor(days / 365)
|
|
|
+ const months = Math.floor((days % 365) / 30)
|
|
|
+ return months === 0
|
|
|
+ ? t('duration.years', { years })
|
|
|
+ : t('duration.yearsMonths', { years, months })
|
|
|
+ }
|
|
|
+ if (days >= 30) {
|
|
|
+ const months = Math.floor(days / 30)
|
|
|
+ const remainingDays = days % 30
|
|
|
+ return remainingDays === 0
|
|
|
+ ? t('duration.months', { months })
|
|
|
+ : t('duration.monthsDays', { months, days: remainingDays })
|
|
|
+ }
|
|
|
+ if (days > 0) {
|
|
|
+ return hours === 0
|
|
|
+ ? t('duration.days', { days })
|
|
|
+ : t('duration.daysHours', { days, hours })
|
|
|
+ }
|
|
|
+ if (totalHours > 0) {
|
|
|
return t('duration.hours', {
|
|
|
- hours,
|
|
|
+ hours: totalHours,
|
|
|
minutes: String(minutes).padStart(2, '0'),
|
|
|
seconds: String(seconds).padStart(2, '0'),
|
|
|
})
|
|
|
@@ -120,6 +158,19 @@ function formatDuration(ms: number, t: TranslateNS<typeof NS>): string {
|
|
|
return t('duration.seconds', { seconds })
|
|
|
}
|
|
|
|
|
|
+/** Preserve exact whole seconds for hover and accessible naming. */
|
|
|
+function formatExactDuration(ms: number, t: TranslateNS<typeof NS>): string {
|
|
|
+ const { seconds, minutes, hours, days } = splitDuration(ms)
|
|
|
+ return days === 0
|
|
|
+ ? formatDuration(ms, t)
|
|
|
+ : t('duration.exactDays', {
|
|
|
+ days,
|
|
|
+ hours: String(hours).padStart(2, '0'),
|
|
|
+ minutes: String(minutes).padStart(2, '0'),
|
|
|
+ seconds: String(seconds).padStart(2, '0'),
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
/** Aggregate the complete subagent-only descendant subtree from flat summaries. */
|
|
|
function summarizeDescendants(
|
|
|
sessionId: SessionId,
|
|
|
@@ -256,8 +307,11 @@ function CatalogRows({
|
|
|
: `${formatTokens(totalTokens)} tok`
|
|
|
const durationMetric = durationMs === undefined
|
|
|
? undefined
|
|
|
- : formatDuration(durationMs, t)
|
|
|
- const metrics = [tokenMetric, durationMetric]
|
|
|
+ : {
|
|
|
+ compact: formatDuration(durationMs, t),
|
|
|
+ exact: formatExactDuration(durationMs, t),
|
|
|
+ }
|
|
|
+ const metrics = [tokenMetric, durationMetric?.exact]
|
|
|
.filter(value => value !== undefined)
|
|
|
.join(' · ')
|
|
|
|
|
|
@@ -319,7 +373,14 @@ function CatalogRows({
|
|
|
{metrics !== '' && (
|
|
|
<span className={css.metrics}>
|
|
|
{tokenMetric !== undefined && <span className={css.metricToken}>{tokenMetric}</span>}
|
|
|
- {durationMetric !== undefined && <span className={css.metricDuration}>{durationMetric}</span>}
|
|
|
+ {durationMetric !== undefined && (
|
|
|
+ <span
|
|
|
+ className={css.metricDuration}
|
|
|
+ title={t('duration.exactTitle', { duration: durationMetric.exact })}
|
|
|
+ >
|
|
|
+ {durationMetric.compact}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
</span>
|
|
|
)}
|
|
|
</div>
|