{tail}
-
{ forkAt(closing.finalNode.seq) }}
- branchUnavailable={data.branchUnavailable || hasLaterChatNode}
- className={css.actions}
- extraActions={assistantActions}
- t={t}
- />
+
+ {data.tokenUsage === undefined ? null : }
+ { forkAt(closing.finalNode.seq) }}
+ branchUnavailable={data.branchUnavailable || hasLaterChatNode}
+ className={css.actions}
+ extraActions={assistantActions}
+ t={t}
+ />
+
)
})
diff --git a/packages/client/ui-chat/src/client/chat/TurnUsageDisclosure.module.css b/packages/client/ui-chat/src/client/chat/TurnUsageDisclosure.module.css
new file mode 100644
index 0000000000..46da05fb99
--- /dev/null
+++ b/packages/client/ui-chat/src/client/chat/TurnUsageDisclosure.module.css
@@ -0,0 +1,87 @@
+.root {
+ min-width: 0;
+}
+
+.root[data-open] {
+ padding-bottom: 4px;
+}
+
+.root [data-disclosure-row]:focus-visible {
+ border-radius: 6px;
+ outline: 2px solid var(--dsw-alias-label-tertiary);
+ outline-offset: -2px;
+}
+
+.chevron {
+ color: var(--dsw-alias-label-secondary);
+}
+
+.separator {
+ flex: none;
+ width: 2px;
+ height: 2px;
+ margin: 0 8px;
+ border-radius: 1px;
+ background: var(--dsw-alias-label-caption);
+}
+
+.summary {
+ min-width: 0;
+ overflow: hidden;
+ color: var(--dsw-alias-label-tertiary);
+ font-size: 14px;
+ font-variant-numeric: tabular-nums;
+ line-height: 24px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.details {
+ display: grid;
+ grid-template-columns: minmax(76px, auto) minmax(0, 1fr);
+ gap: 6px 16px;
+ box-sizing: border-box;
+ width: calc(100% - 22px);
+ margin: 4px 0 0 22px;
+ padding: 10px 16px 12px 12px;
+ border-radius: 8px;
+ background: var(--dsw-alias-markdown-code-block);
+ color: var(--dsw-alias-label-tertiary);
+ font-size: 12px;
+ line-height: 18px;
+}
+
+.details dt,
+.details dd {
+ min-width: 0;
+ margin: 0;
+}
+
+.details dd {
+ color: var(--dsw-alias-label-secondary);
+ font-variant-numeric: tabular-nums;
+ text-align: right;
+}
+
+.details .route {
+ overflow-wrap: anywhere;
+}
+
+.reasoning {
+ color: var(--dsw-alias-label-tertiary);
+ white-space: nowrap;
+}
+
+.totalLabel,
+.details .totalValue {
+ padding-top: 6px;
+ border-top: 1px solid var(--dsw-alias-separator-primary);
+ color: var(--dsw-alias-label-primary);
+}
+
+@media (max-width: 480px) {
+ .details {
+ grid-template-columns: minmax(72px, auto) minmax(0, 1fr);
+ gap-inline: 10px;
+ }
+}
diff --git a/packages/client/ui-chat/src/client/chat/TurnUsageDisclosure.tsx b/packages/client/ui-chat/src/client/chat/TurnUsageDisclosure.tsx
new file mode 100644
index 0000000000..8d79b44f4d
--- /dev/null
+++ b/packages/client/ui-chat/src/client/chat/TurnUsageDisclosure.tsx
@@ -0,0 +1,86 @@
+import { useState } from 'react'
+import { DisclosureRow, IconDataOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
+import type { TurnTokenUsage } from '../contract/chat-nodes.ts'
+import type { ChatViewSlotProps } from '../contract/slots.ts'
+import { formatCacheHitPercent, formatExactTokens, formatTokens } from './token-format.ts'
+import css from './TurnUsageDisclosure.module.css'
+
+export interface TurnUsageDisclosureProps {
+ usage: TurnTokenUsage
+ t: ChatViewSlotProps['t']
+}
+
+function formatCompactCount(value: number, t: ChatViewSlotProps['t']): string {
+ return t('message.turnUsage.count', { count: formatTokens(value, t) })
+}
+
+function formatExactCount(value: number, t: ChatViewSlotProps['t']): string {
+ return t('message.turnUsage.count', { count: formatExactTokens(value, t) })
+}
+
+/** Compact per-Turn usage summary with an opt-in bucket breakdown. */
+export function TurnUsageDisclosure({ usage, t }: TurnUsageDisclosureProps) {
+ const [open, setOpen] = useState(false)
+ const cacheHit = usage.cacheReadTokens === undefined
+ ? null
+ : formatCacheHitPercent(usage.cacheReadTokens, usage.totalTokens - usage.outputTokens, 1)
+ const total = formatCompactCount(usage.totalTokens, t)
+ const summary = cacheHit === null
+ ? total
+ : t('message.turnUsage.summaryWithCache', { total, percent: cacheHit })
+ const routes = usage.routes?.map(route => `${route.provider}/${route.model}`).join(', ') ?? ''
+
+ return (
+