merge compact duration display

# Conflicts:
#	.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md
#	.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md
#	apps/web/tests/snapshots/subagent-conversation/tree.expected.md
#	packages/client/ui-subagent/README.i18n.yaml
#	packages/client/ui-subagent/README.md
#	packages/client/ui-subagent/README.zh.md
#	packages/client/ui-subagent/src/client/SubagentCatalogAction.tsx
#	packages/client/ui-subagent/tests/conversation-ui.spec.tsx
This commit is contained in:
kingwl
2026-08-03 13:31:19 +08:00
12 changed files with 141 additions and 34 deletions
@@ -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>
@@ -11,6 +11,14 @@ export const zh = {
'duration.seconds': '{seconds}秒',
'duration.minutes': '{minutes}分{seconds}秒',
'duration.hours': '{hours}小时{minutes}分{seconds}秒',
'duration.days': '{days}天',
'duration.daysHours': '{days}天{hours}小时',
'duration.months': '约{months}个月',
'duration.monthsDays': '约{months}个月{days}天',
'duration.years': '约{years}年',
'duration.yearsMonths': '约{years}年{months}个月',
'duration.exactDays': '{days}天{hours}小时{minutes}分{seconds}秒',
'duration.exactTitle': '总活跃耗时:{duration}',
'loading.label': '正在加载子代理…',
'loading.aria': '正在加载子代理',
'load.error': '无法加载子代理',
@@ -40,6 +48,14 @@ export const en: Record<SubagentKey, string> = {
'duration.seconds': '{seconds}s',
'duration.minutes': '{minutes}m {seconds}s',
'duration.hours': '{hours}h {minutes}m {seconds}s',
'duration.days': '{days}d',
'duration.daysHours': '{days}d {hours}h',
'duration.months': '~{months}mo',
'duration.monthsDays': '~{months}mo {days}d',
'duration.years': '~{years}y',
'duration.yearsMonths': '~{years}y {months}mo',
'duration.exactDays': '{days}d {hours}h {minutes}m {seconds}s',
'duration.exactTitle': 'Total active duration: {duration}',
'loading.label': 'Loading subagents…',
'loading.aria': 'Loading subagents',
'load.error': 'Unable to load subagents',