show subagent usage and active duration

This commit is contained in:
kingwl
2026-08-02 23:31:24 +08:00
parent baf133d134
commit b30686d634
35 changed files with 438 additions and 88 deletions
@@ -173,15 +173,17 @@
}
.summary,
.time {
.metrics {
color: var(--dsw-alias-label-tertiary);
font-size: 11px;
line-height: 16px;
}
.time {
.metrics {
flex: none;
margin-top: 16px;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.children {
@@ -2,13 +2,16 @@ import {
useEffect, useRef, useState, type KeyboardEvent, type MouseEvent,
} from 'react'
import type {
SessionId, SessionListState, SessionSummary, SubagentAddress, SubagentCatalogSnapshot,
SessionId, SessionListState, SessionProjectionMap, SessionSummary, SubagentAddress,
SubagentCatalogSnapshot,
} from '@deepseek-ai/dsh-client-runtime/client'
import {
IconChevronDownOutline14, IconChevronRightOutline14, IconRefreshOutline14, StateDot,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type {} from '@deepseek-ai/dsh-subagent/client'
import type {} from '@deepseek-ai/dsh-token-meter/client'
import css from './SubagentCatalogAction.module.css'
type CatalogEntry = SubagentCatalogSnapshot['entries'][number]
@@ -53,19 +56,53 @@ function treeItems(root: HTMLDivElement | null): HTMLElement[] {
: Array.from(root.querySelectorAll<HTMLElement>('[role="treeitem"]:not([aria-disabled="true"])'))
}
/** Compact trailing activity time for a catalog row. */
function relativeTime(updatedAt: number | undefined, now: number): string | undefined {
if (updatedAt === undefined) return undefined
const minute = 60_000
const hour = 60 * minute
const day = 24 * hour
const diff = Math.max(0, now - updatedAt)
if (diff < minute) return '刚刚'
if (diff < hour) return `${Math.floor(diff / minute)}分钟`
if (diff < day) return `${Math.floor(diff / hour)}小时`
if (diff < 30 * day) return `${Math.floor(diff / day)}`
if (diff < 365 * day) return `${Math.floor(diff / (30 * day))}个月`
return `${Math.floor(diff / (365 * day))}`
/** Compact token count shared in shape with the conversation stats strip. */
function formatTokens(value: number): string {
const scaled = (next: number): string => next >= 100
? String(Math.round(next))
: String(Math.round(next * 10) / 10)
if (value < 1_000) return String(value)
if (value < 1_000_000) return `${scaled(value / 1_000)}K`
return `${scaled(value / 1_000_000)}M`
}
/** Sum the four disjoint durable provider-usage buckets. */
function tokenTotal(
usage: SessionProjectionMap['tokenUsage'] | undefined,
): number | undefined {
return usage === undefined
? undefined
: usage.uncachedInputTokens + usage.outputTokens
+ usage.cacheReadTokens + usage.cacheWriteTokens
}
/** Exact whole-second active-turn duration for one catalog row. */
function activityDuration(
timing: SessionProjectionMap['subagentTiming'] | undefined,
activity: 'running' | 'inactive',
updatedAt: number | undefined,
now: number,
): number | undefined {
if (timing === undefined) return undefined
if (timing.activeSince === undefined) return timing.settledMs
const end = activity === 'running' ? now : updatedAt ?? timing.activeSince
return timing.settledMs + Math.max(0, end - timing.activeSince)
}
/** Format a non-negative duration to seconds without dropping larger units. */
function formatDuration(ms: number): string {
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) {
return `${hours}小时${String(minutes).padStart(2, '0')}${String(seconds).padStart(2, '0')}`
}
if (totalMinutes > 0) {
return `${totalMinutes}${String(seconds).padStart(2, '0')}`
}
return `${seconds}`
}
/** Aggregate the complete subagent-only descendant subtree from flat summaries. */
@@ -190,7 +227,17 @@ function CatalogRows({
const secondary = [summary?.title, mode, activity]
.filter(value => value !== undefined)
.join(' · ')
const time = relativeTime(summary?.updatedAt, now)
const totalTokens = tokenTotal(summary?.projectionValues?.tokenUsage)
const durationMs = activityDuration(
summary?.projectionValues?.subagentTiming,
entry.activity,
summary?.updatedAt,
now,
)
const metrics = [
totalTokens === undefined ? undefined : `${formatTokens(totalTokens)} tok`,
durationMs === undefined ? undefined : formatDuration(durationMs),
].filter(value => value !== undefined).join(' · ')
const open = (): void => {
openChild({ parentSessionId, childSessionId: entry.id, mode: entry.mode })
@@ -222,7 +269,7 @@ function CatalogRows({
role="treeitem"
tabIndex={0}
aria-level={level}
aria-label={[label, secondary, time].filter(value => value !== undefined).join(' ')}
aria-label={[label, secondary, metrics].filter(value => value !== '').join(' ')}
{...knownLeaf ? {} : { 'aria-expanded': isExpanded }}
className={css.row}
onClick={open}
@@ -247,7 +294,7 @@ function CatalogRows({
<span className={css.label}>{label}</span>
<span className={css.summary}>{secondary}</span>
</span>
{time !== undefined && <span className={css.time}>{time}</span>}
{metrics !== '' && <span className={css.metrics}>{metrics}</span>}
</div>
</div>
{isExpanded && !knownLeaf && (
@@ -300,6 +347,7 @@ export function SubagentCatalogAction({
const summaries = useSessions(state => state.byId)
const catalog = catalogs[sessionId]
const [open, setOpen] = useState(false)
const [now, setNow] = useState(() => Date.now())
const [expanded, setExpanded] = useState<ReadonlySet<SessionId>>(() => new Set())
const rootRef = useRef<HTMLDivElement>(null)
const triggerRef = useRef<HTMLButtonElement>(null)
@@ -328,7 +376,10 @@ export function SubagentCatalogAction({
const changeOpen = (next: boolean, restoreFocus = false): void => {
setOpen(next)
if (next) observeCatalog(sessionId, true)
if (next) {
setNow(Date.now())
observeCatalog(sessionId, true)
}
else closeAllCatalogs()
if (restoreFocus) queueMicrotask(() => { triggerRef.current?.focus() })
}
@@ -368,6 +419,12 @@ export function SubagentCatalogAction({
return () => { document.removeEventListener('pointerdown', closeOutside) }
}, [open])
useEffect(() => {
if (!open || !descendants.running) return
const timer = setInterval(() => { setNow(Date.now()) }, 1_000)
return () => { clearInterval(timer) }
}, [open, descendants.running])
useEffect(() => () => {
for (const parentSessionId of observedCatalogs.current) {
setCatalogOpenRef.current(parentSessionId, false)
@@ -443,7 +500,7 @@ export function SubagentCatalogAction({
summaries={summaries}
expanded={expanded}
level={1}
now={Date.now()}
now={now}
openChild={openChild}
refresh={refresh}
toggleBranch={toggleBranch}