+
{stats.steps > 0 && (
& PropsRenderSlots<'conversation.chat.turnTail' | 'conversation.chat.assistant-actions'>
+ & InjectFace
/** Turn-local actions and feature tail over the Location index, independent of Assistant placement. */
export const TurnTailNodeView = memo(function TurnTailNodeView({
- node, openFile, forkAt, renderSlot, t, useChat,
+ node, openFile, forkAt, renderSlot, t, useChat, usePerformanceUsage,
}: TurnTailNodeViewProps) {
+ const detailed = usePerformanceUsage(mode => mode) === 'detailed'
const data = node.data
const hasLaterChatNode = useChat(snapshot =>
snapshot.locations.getTurn(data.turn).at(-1) !== node.key)
@@ -25,9 +27,6 @@ export const TurnTailNodeView = memo(function TurnTailNodeView({
const owner: TurnTailOwnerProps = { turn, seq: closing?.finalNode.seq ?? data.seq, openFile }
const tail = renderSlot('conversation.chat.turnTail', owner)
if (closing === null) return tail === null ? null : {tail}
- const runMs = turn.start === undefined || turn.end === undefined
- ? undefined
- : Math.max(0, turn.end.time - turn.start.time)
// Interruption-frozen partials carry no messageId, so they address no
// durable message and contribute no per-message actions.
const messageId = closing.finalNode.messageId
@@ -49,19 +48,9 @@ export const TurnTailNodeView = memo(function TurnTailNodeView({
branchUnavailable={data.branchUnavailable || hasLaterChatNode}
className={css.actions}
extraActions={assistantActions}
- usageAction={(
- <>
- {data.tokenUsage !== undefined && }
- {runMs !== undefined && (
-
- )}
- >
- )}
+ usageAction={detailed && data.tokenUsage !== undefined
+ ?
+ : null}
t={t}
/>
diff --git a/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx b/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx
index 765650b5ca..460d1092a2 100644
--- a/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx
+++ b/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx
@@ -1,13 +1,9 @@
-// Icon-row Turn-stat actions: a database pill labelled with the turn total
-// click-opens the per-Turn usage dialog, and a clock pill labelled with the
-// turn wall time click-opens the Turn-time dialog. Both sit right of the
-// branch action in the tail's IconActions row, ahead of the plain clock text.
+/** Completed-Turn token usage action and its accounting details dialog. */
import { createPortal } from 'react-dom'
-import { IconClockOutlineRegular, IconDatabaseOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
+import { IconDatabaseOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import type { TurnTokenUsage } from '../contract/chat-nodes.ts'
import type { ChatViewSlotProps } from '../contract/slots.ts'
-import { formatLatencySeconds, formatRunDuration, formatTokensPerSecond } from './message-chrome.ts'
import { formatCacheHitPercent, formatExactTokens, formatTokens } from './token-format.ts'
import { MEASURE_STYLE, useStatDialog } from './stat-dialog.ts'
import css from './TurnUsagePanel.module.css'
@@ -19,17 +15,6 @@ export interface TurnUsagePanelProps {
t: ChatViewSlotProps['t']
}
-export interface TurnTimePanelProps {
- /** Turn wall time in ms, the pill's label. */
- runMs: number
- /** Turn decode throughput, a dialog row when known. */
- tokensPerSecond?: number | undefined
- /** Turn first-step TTFT in ms, a dialog row when known. */
- ttftMs?: number | undefined
- /** The owning view's locale seat, passed down as a plain prop. */
- t: ChatViewSlotProps['t']
-}
-
function formatCompactCount(value: number, t: ChatViewSlotProps['t']): string {
return t('message.turnUsage.count', { count: formatTokens(value, t) })
}
@@ -123,60 +108,3 @@ export function TurnUsagePanel({ usage, t }: TurnUsagePanelProps) {
)
}
-
-/**
- * Turn-time IconActions pill with a click-open Turn-time details dialog.
- * @param props - Turn timing facts and locale seat.
- * @returns The clock-and-duration trigger and, while open, its portaled dialog anchored above the trigger.
- */
-export function TurnTimePanel({ runMs, tokensPerSecond, ttftMs, t }: TurnTimePanelProps) {
- const { open, setOpen, rootRef, panelRef, pos } = useStatDialog()
- return (
-
-
- {open && createPortal(
-
-
-
-
- {t('message.turnTime.title')}
-
-
-
-
- - {t('message.turnTime.duration')}
- - {formatRunDuration(runMs, t)}
- {tokensPerSecond !== undefined && (
- <>
- - {t('message.turnTime.speed')}
- - {t('message.tokensPerSecond', { tps: formatTokensPerSecond(tokensPerSecond) })}
- >
- )}
- {ttftMs !== undefined && (
- <>
- - {t('message.turnTime.ttft')}
- - {t('duration.seconds', { seconds: formatLatencySeconds(ttftMs) })}
- >
- )}
-
-
,
- document.body,
- )}
-
- )
-}
diff --git a/packages/client/ui-chat/src/client/chat/message-chrome.ts b/packages/client/ui-chat/src/client/chat/message-chrome.ts
index 4bd134a2a9..36c4a3f0bd 100644
--- a/packages/client/ui-chat/src/client/chat/message-chrome.ts
+++ b/packages/client/ui-chat/src/client/chat/message-chrome.ts
@@ -35,7 +35,7 @@ export function msUntilNextLocalMidnight(ms: number): number {
}
/**
- * Localized elapsed-time label shared by running and settled turn chrome.
+ * Localized elapsed-time label for the running conversation clock.
* @param ms - Elapsed duration in milliseconds (negatives clamp to zero).
* @param t - Translate seat supplying the duration templates.
* @returns Display string in whole seconds; minutes and seconds once the
@@ -55,17 +55,6 @@ export function formatRunDuration(ms: number, t: RunDurationTranslate): string {
: t('duration.seconds', { seconds })
}
-/**
- * Sub-turn latency figure: one decimal under ten seconds, whole seconds
- * beyond. Unit-less so the locale template owns the second suffix.
- * @param ms - Latency in milliseconds (negatives clamp to zero).
- * @returns Display number in seconds without unit.
- */
-export function formatLatencySeconds(ms: number): string {
- const s = Math.max(0, ms) / 1000
- return s < 10 ? String(Math.round(s * 10) / 10) : String(Math.round(s))
-}
-
/**
* Decode-throughput figure: whole tokens from ten up, one decimal below.
* @param tps - Tokens per second.
diff --git a/packages/client/ui-chat/src/client/chat/register-node-renderers.ts b/packages/client/ui-chat/src/client/chat/register-node-renderers.ts
index 79b35fd85a..f8f78000f1 100644
--- a/packages/client/ui-chat/src/client/chat/register-node-renderers.ts
+++ b/packages/client/ui-chat/src/client/chat/register-node-renderers.ts
@@ -1,4 +1,6 @@
import type { Context } from '@deepseek-ai/cordis'
+import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
+import type { PerformanceUsageMode } from '../../chat-settings.ts'
import { NS } from '../locale.ts'
import { AssistantNodeView } from './AssistantNodeView.tsx'
import { CommandNodeView, ManualCompactionNodeView } from './CommandNodeView.tsx'
@@ -13,8 +15,9 @@ import { TurnTailNodeView } from './TurnTailNodeView.tsx'
/**
* Register this package's business renderers behind the keyed Chat Node seat.
* @param ctx - owning UI Conversation context.
+ * @param performanceUsage - live statistics detail preference.
*/
-export function registerChatNodeRenderers(ctx: Context): void {
+export function registerChatNodeRenderers(ctx: Context, performanceUsage: ObservableSnapshot
): void {
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register(
{ name: 'conversation.chat.node', key: 'user', locale: NS }, UserMessageNodeView))
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register(
@@ -47,6 +50,7 @@ export function registerChatNodeRenderers(ctx: Context): void {
name: 'conversation.chat.node',
key: 'turn-tail',
locale: NS,
+ inject: () => ({ hooks: { performanceUsage } }),
children: {
'conversation.chat.turnTail': { kind: 'list', scope: 'session' },
'conversation.chat.assistant-actions': { kind: 'list', scope: 'session' },
diff --git a/packages/client/ui-chat/src/client/contract/chat-nodes.ts b/packages/client/ui-chat/src/client/contract/chat-nodes.ts
index d04bdfd90e..cade646b1c 100644
--- a/packages/client/ui-chat/src/client/contract/chat-nodes.ts
+++ b/packages/client/ui-chat/src/client/contract/chat-nodes.ts
@@ -91,8 +91,6 @@ export interface TurnTailChatData {
readonly closing: FinalAssistantChatData | null
/** Whether non-rendered later evidence makes the closing seq non-tail. */
readonly branchUnavailable: boolean
- readonly ttftMs?: number
- readonly tokensPerSecond?: number
/** Exact per-Turn accounting; absent when the loaded evidence is incomplete. */
readonly tokenUsage?: TurnTokenUsage
}
diff --git a/packages/client/ui-chat/src/client/contract/slots.ts b/packages/client/ui-chat/src/client/contract/slots.ts
index 91c3adad7b..d5b007b7cf 100644
--- a/packages/client/ui-chat/src/client/contract/slots.ts
+++ b/packages/client/ui-chat/src/client/contract/slots.ts
@@ -9,7 +9,7 @@ import type {
InjectFace, KeyedSnapshotSelectorHook, PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore,
SlotHookFactory, SnapshotSelectorHook,
} from '@deepseek-ai/dsh-client-ui-slots'
-import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
+import type { ObservableSnapshot, SnapshotStore } from '@deepseek-ai/dsh-client-store'
import type { MarkdownFileMentions } from '@deepseek-ai/dsh-client-ui-primitives'
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
import type { createChatStore } from '../stores.ts'
@@ -19,7 +19,7 @@ import type {
ChatNodeProcessSource, ChatNodeSource, ChatSnapshot, ChatTurnProcessPresentation,
} from './snapshot.ts'
import type { TurnProcessSpec } from './turn-process.ts'
-import type { TranscriptViewMode } from '../../chat-settings.ts'
+import type { PerformanceUsageMode, TranscriptViewMode } from '../../chat-settings.ts'
/** Selector hook over the current Conversation binding's Chat target. */
export type UseChat = SnapshotSelectorHook
@@ -128,6 +128,14 @@ export interface ChatScrollPosition {
readonly scrollTop: number
}
+/** Shared settings source for the performance row, composer, and turn tail. */
+export interface PerformanceUsageInjected {
+ hooks: {
+ /** Accepted performance and usage detail preference. */
+ performanceUsage: ObservableSnapshot
+ }
+}
+
/** Business callbacks injected into the Chat view. */
export interface ChatViewInjected {
hooks: {
diff --git a/packages/client/ui-chat/src/client/contract/turn-metrics.ts b/packages/client/ui-chat/src/client/contract/turn-metrics.ts
index a9f4dbc7c0..6cbd619beb 100644
--- a/packages/client/ui-chat/src/client/contract/turn-metrics.ts
+++ b/packages/client/ui-chat/src/client/contract/turn-metrics.ts
@@ -1,17 +1,9 @@
-// Latency/throughput folds shared by the settled turn footer and StatsPills.
+// Assistant timing readings used by session statistics.
import type {
- AssistantMessageNode, ConversationNode,
+ AssistantMessageNode,
} from '@deepseek-ai/dsh-client-ui-conversation/client'
-/** Latency and decode-throughput readings for one turn's footer. */
-export interface TurnMetrics {
- /** First-step TTFT in ms; absent when that step carries no recorded timing. */
- ttftMs?: number
- /** Decode throughput over steps carrying both timing and provider usage. */
- tokensPerSecond?: number
-}
-
/** One assistant step's derivable latency facts; null marks an unrecorded part. */
export interface StepReading {
/** step/start → first token delta, in ms. */
@@ -49,51 +41,3 @@ export function assistantStepReading(node: AssistantNode): StepReading {
: null
return { ttftMs, decodeMs, outputTokens: usageOutputTokens(node.usage) }
}
-
-interface TurnFold {
- firstStep: number
- firstStepTtftMs: number | null
- decodeMs: number
- outputTokens: number
- sampled: boolean
-}
-
-/**
- * Fold assistant nodes into per-turn footer metrics.
- *
- * TTFT is the turn's lowest-step request-dispatch-to-first-token reading, so
- * it is only meaningful when the turn's start is inside
- * the loaded window (the caller gates on `turnTimings`, which shares that
- * window). Throughput divides summed output tokens by summed decode wall time,
- * counting only steps that carry both.
- * @param nodes - Snapshot nodes of the loaded window.
- * @returns Turn number → available metrics; turns with none are absent.
- */
-export function deriveTurnMetrics(nodes: readonly ConversationNode[]): Map {
- const folds = new Map()
- for (const node of nodes) {
- if (node.kind !== 'assistant') continue
- const reading = assistantStepReading(node)
- let fold = folds.get(node.turn)
- if (fold === undefined) {
- fold = { firstStep: node.step, firstStepTtftMs: reading.ttftMs, decodeMs: 0, outputTokens: 0, sampled: false }
- folds.set(node.turn, fold)
- } else if (node.step < fold.firstStep) {
- fold.firstStep = node.step
- fold.firstStepTtftMs = reading.ttftMs
- }
- if (reading.decodeMs !== null && reading.outputTokens !== null) {
- fold.decodeMs += reading.decodeMs
- fold.outputTokens += reading.outputTokens
- fold.sampled = true
- }
- }
- const metrics = new Map()
- for (const [turn, fold] of folds) {
- const entry: TurnMetrics = {}
- if (fold.firstStepTtftMs !== null) entry.ttftMs = fold.firstStepTtftMs
- if (fold.sampled && fold.decodeMs > 0) entry.tokensPerSecond = fold.outputTokens / (fold.decodeMs / 1000)
- if (entry.ttftMs !== undefined || entry.tokensPerSecond !== undefined) metrics.set(turn, entry)
- }
- return metrics
-}
diff --git a/packages/client/ui-chat/src/client/conversation-nodes/turn-tail.ts b/packages/client/ui-chat/src/client/conversation-nodes/turn-tail.ts
index 3206b1cea3..1cccf210f3 100644
--- a/packages/client/ui-chat/src/client/conversation-nodes/turn-tail.ts
+++ b/packages/client/ui-chat/src/client/conversation-nodes/turn-tail.ts
@@ -9,7 +9,6 @@ import { deriveTurnTokenUsage } from '@deepseek-ai/dsh-token-meter/client'
import type {
AssistantChatData, FinalAssistantChatData, TurnTailChatData,
} from '../contract/chat-nodes.ts'
-import { deriveTurnMetrics } from '../contract/turn-metrics.ts'
import { CHAT_SYNTHETIC_SEQ_OFFSETS, chatNode } from './common.ts'
import { toAssistantBlocks } from './event-projection.ts'
@@ -146,7 +145,6 @@ function tailData(context: ConversationNodeContext): TurnTailChat
latestTranscriptSeq = candidate
}
}
- const metrics = deriveTurnMetrics(finalized.map(candidate => candidate.finalNode)).get(end.event.data.turn)
const tokenUsage = context.start?.event.type === 'turn/start'
? deriveTurnTokenUsage(context.matches.map(match => match.event).filter(isSessionEvent))
: undefined
@@ -156,8 +154,6 @@ function tailData(context: ConversationNodeContext): TurnTailChat
time: end.event.time,
closing,
branchUnavailable: closing === null || latestTranscriptSeq !== closing.finalNode.seq,
- ...metrics?.ttftMs === undefined ? {} : { ttftMs: metrics.ttftMs },
- ...metrics?.tokensPerSecond === undefined ? {} : { tokensPerSecond: metrics.tokensPerSecond },
...tokenUsage === undefined ? {} : { tokenUsage },
}
}
diff --git a/packages/client/ui-chat/src/client/locale.ts b/packages/client/ui-chat/src/client/locale.ts
index 5468ed9df4..344fef13d0 100644
--- a/packages/client/ui-chat/src/client/locale.ts
+++ b/packages/client/ui-chat/src/client/locale.ts
@@ -27,6 +27,10 @@ export const zh = {
'chat.turnNavigation.jump': '跳转到第 {turn} 轮',
'chat.turnNavigation.jumpLoad': '加载并跳转到第 {turn} 轮',
'chat.turnNavigation.turn': '第 {turn} 轮',
+ 'settings.performance.title': '性能与用量',
+ 'settings.performance.description': '选择性能与用量信息展示的详细程度',
+ 'settings.performance.compact': '简洁',
+ 'settings.performance.detailed': '详细',
'settings.transcript.title': '对话显示',
'settings.transcript.description': '控制已完成轮次的过程内容',
'settings.transcript.normal': '标准',
@@ -81,7 +85,6 @@ export const zh = {
'message.turnError': '本轮运行失败',
'message.maxTokens': '已达到输出 token 上限',
'message.maxTokens.hint': '回答被截断,已有输出保留在对话中。发送“继续”可让模型接着输出。',
- 'message.ranFor': '用时 {duration}',
'message.tokensPerSecond': '{tps} tok/s',
'message.turnUsage.title': '本轮用量',
'message.turnUsage.consumed': '用量 {total}',
@@ -93,10 +96,6 @@ export const zh = {
'message.turnUsage.output': '输出',
'message.turnUsage.reasoning': '(其中推理 {tokens})',
'message.turnUsage.count': '{count} tok',
- 'message.turnTime.title': '本轮用时和速度',
- 'message.turnTime.duration': '本轮总用时',
- 'message.turnTime.speed': '输出速度(TPS)',
- 'message.turnTime.ttft': '首 token 用时(TTFT)',
'duration.seconds': '{seconds}秒',
'duration.minutes': '{minutes}分{seconds}秒',
'duration.hours': '{hours}小时{minutes}分{seconds}秒',
@@ -138,6 +137,10 @@ export const en = {
'chat.turnNavigation.jump': 'Jump to turn {turn}',
'chat.turnNavigation.jumpLoad': 'Load and jump to turn {turn}',
'chat.turnNavigation.turn': 'Turn {turn}',
+ 'settings.performance.title': 'Performance & usage',
+ 'settings.performance.description': 'Choose how much performance and usage information to show',
+ 'settings.performance.compact': 'Compact',
+ 'settings.performance.detailed': 'Detailed',
'settings.transcript.title': 'Conversation display',
'settings.transcript.description': 'Controls process content in completed turns',
'settings.transcript.normal': 'Normal',
@@ -192,7 +195,6 @@ export const en = {
'message.turnError': 'This turn failed',
'message.maxTokens': 'Output token limit reached',
'message.maxTokens.hint': 'The reply was cut off; earlier output is preserved in the conversation. Send "continue" to let the model resume.',
- 'message.ranFor': 'Ran for {duration}',
'message.tokensPerSecond': '{tps} tok/s',
'message.turnUsage.title': 'Turn usage',
'message.turnUsage.consumed': 'Usage {total}',
@@ -204,10 +206,6 @@ export const en = {
'message.turnUsage.output': 'Output',
'message.turnUsage.reasoning': ' ({tokens} reasoning)',
'message.turnUsage.count': '{count} tok',
- 'message.turnTime.title': 'Turn time and speed',
- 'message.turnTime.duration': 'Total run time',
- 'message.turnTime.speed': 'Tokens per second (TPS)',
- 'message.turnTime.ttft': 'Time to first token (TTFT)',
'duration.seconds': '{seconds}s',
'duration.minutes': '{minutes}m {seconds}s',
'duration.hours': '{hours}h {minutes}m {seconds}s',
diff --git a/packages/client/ui-chat/src/client/performance-usage.ts b/packages/client/ui-chat/src/client/performance-usage.ts
new file mode 100644
index 0000000000..b724416c05
--- /dev/null
+++ b/packages/client/ui-chat/src/client/performance-usage.ts
@@ -0,0 +1,30 @@
+/** Performance detail preference with process-local choices on memory-only settings scopes. */
+import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
+import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client'
+import { DEFAULT_PERFORMANCE_USAGE, type ChatSettings, type PerformanceUsageMode } from '../chat-settings.ts'
+
+/** Shared live preference for the settings row and chat statistics. */
+export class PerformanceUsagePolicy {
+ /** Current choice, reconciled with accepted Host settings when available. */
+ readonly mode = createSnapshotStore(DEFAULT_PERFORMANCE_USAGE)
+
+ /** @param host - Chat settings scope, durable on loopback and memory-only elsewhere. */
+ constructor(private readonly host: SettingsScope) {
+ const adopt = (): void => {
+ const accepted = host.getSnapshot().value?.performanceUsage
+ if (accepted !== undefined) this.mode.set(accepted)
+ }
+ host.subscribe(adopt)
+ adopt()
+ }
+
+ /**
+ * Publish a choice immediately and persist it when the scope supports writes.
+ * @param mode - Statistics detail selected by the user.
+ */
+ setMode(mode: PerformanceUsageMode): void {
+ if (mode === this.mode.getSnapshot()) return
+ this.mode.set(mode)
+ void this.host.set('performanceUsage', mode)
+ }
+}
diff --git a/packages/client/ui-chat/src/client/settings/PerformanceUsageRow.tsx b/packages/client/ui-chat/src/client/settings/PerformanceUsageRow.tsx
new file mode 100644
index 0000000000..db05aaebb9
--- /dev/null
+++ b/packages/client/ui-chat/src/client/settings/PerformanceUsageRow.tsx
@@ -0,0 +1,46 @@
+/** General Settings row for performance and usage detail. */
+
+import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
+import type { PerformanceUsageMode } from '../../chat-settings.ts'
+import type { PerformanceUsageInjected } from '../contract/slots.ts'
+import type { ChatKey } from '../locale.ts'
+import { PreferenceRow } from './PreferenceRow.tsx'
+
+/** Registration-side performance and usage preference face. */
+export interface PerformanceUsageRowInjected extends PerformanceUsageInjected {
+ /** Change the performance and usage detail. */
+ setPerformanceUsage: (mode: PerformanceUsageMode) => void
+}
+
+/** Full Settings-row props. */
+export type PerformanceUsageRowProps =
+ PropsRuntime<'settings.general.item'>
+ & PropsLocale<'chat'>
+ & InjectFace
+
+const OPTIONS: readonly { id: PerformanceUsageMode; label: ChatKey }[] = [
+ { id: 'compact', label: 'settings.performance.compact' },
+ { id: 'detailed', label: 'settings.performance.detailed' },
+]
+
+/**
+ * Render the performance and usage detail selector.
+ * @param props - composed Settings slot props.
+ * @returns the preference row.
+ */
+export function PerformanceUsageRow({ usePerformanceUsage, setPerformanceUsage, t }: PerformanceUsageRowProps) {
+ const mode = usePerformanceUsage(value => value)
+ const selectedLabel = mode === 'detailed'
+ ? 'settings.performance.detailed'
+ : 'settings.performance.compact'
+ return (
+ ({ id: option.id, label: t(option.label) }))}
+ onSelect={(value) => { setPerformanceUsage(value as PerformanceUsageMode) }}
+ />
+ )
+}
diff --git a/packages/client/ui-chat/src/client/settings/TranscriptViewRow.module.css b/packages/client/ui-chat/src/client/settings/PreferenceRow.module.css
similarity index 92%
rename from packages/client/ui-chat/src/client/settings/TranscriptViewRow.module.css
rename to packages/client/ui-chat/src/client/settings/PreferenceRow.module.css
index 1ab93f1d52..27e3a05ab7 100644
--- a/packages/client/ui-chat/src/client/settings/TranscriptViewRow.module.css
+++ b/packages/client/ui-chat/src/client/settings/PreferenceRow.module.css
@@ -1,4 +1,4 @@
-/* Completed-Turn transcript preference row: label plus selector pill. */
+/* Chat preference row: label plus selector pill. */
.row {
display: flex;
diff --git a/packages/client/ui-chat/src/client/settings/PreferenceRow.tsx b/packages/client/ui-chat/src/client/settings/PreferenceRow.tsx
new file mode 100644
index 0000000000..c5b216b455
--- /dev/null
+++ b/packages/client/ui-chat/src/client/settings/PreferenceRow.tsx
@@ -0,0 +1,56 @@
+/** Localized two-column selector shared by Chat preference rows. */
+import { useState } from 'react'
+import { IconChevronDownOutlineRegular, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
+import css from './PreferenceRow.module.css'
+
+/**
+ * Render a preference label and its menu.
+ * @param props - localized copy, selected value, choices, and mutation callback.
+ * @returns the settings row.
+ */
+export function PreferenceRow({ title, description, value, selectedLabel, options, onSelect }: {
+ title: string
+ description: string
+ value: string
+ selectedLabel: string
+ options: readonly { id: string; label: string }[]
+ onSelect: (value: string) => void
+}) {
+ const [open, setOpen] = useState(false)
+ const closeMenu = () => { setOpen(false) }
+ const selectMode = (id: string) => {
+ closeMenu()
+ onSelect(id)
+ }
+ const selector = (
+
+ )
+
+ return (
+
+
+
{title}
+
{description}
+
+
+
+ )
+}
diff --git a/packages/client/ui-chat/src/client/settings/TranscriptViewRow.tsx b/packages/client/ui-chat/src/client/settings/TranscriptViewRow.tsx
index b3b188c69c..7dc7df7363 100644
--- a/packages/client/ui-chat/src/client/settings/TranscriptViewRow.tsx
+++ b/packages/client/ui-chat/src/client/settings/TranscriptViewRow.tsx
@@ -1,12 +1,10 @@
/** General Settings row for completed-Turn transcript presentation. */
-import { useState } from 'react'
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
-import { IconChevronDownOutlineRegular, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type { TranscriptViewMode } from '../../chat-settings.ts'
import type { ChatKey } from '../locale.ts'
-import css from './TranscriptViewRow.module.css'
+import { PreferenceRow } from './PreferenceRow.tsx'
/** Registration-side transcript preference face. */
export interface TranscriptViewRowInjected {
@@ -36,44 +34,17 @@ const OPTIONS: readonly { id: TranscriptViewMode; label: ChatKey }[] = [
*/
export function TranscriptViewRow({ useTranscriptView, setTranscriptView, t }: TranscriptViewRowProps) {
const mode = useTranscriptView(value => value)
- const [open, setOpen] = useState(false)
const selectedLabel = mode === 'normal'
? 'settings.transcript.normal'
: 'settings.transcript.compact'
- const closeMenu = () => { setOpen(false) }
- const selectMode = (id: string) => {
- closeMenu()
- setTranscriptView(id as TranscriptViewMode)
- }
- const selector = (
-
- )
-
return (
-
-
-
{t('settings.transcript.title')}
-
{t('settings.transcript.description')}
-
-
+ ({ id: option.id, label: t(option.label) }))}
+ onSelect={(value) => { setTranscriptView(value as TranscriptViewMode) }}
+ />
)
}
diff --git a/packages/client/ui-chat/tests/chat-apply.client.spec.tsx b/packages/client/ui-chat/tests/chat-apply.client.spec.tsx
index 20096e9f8c..92290ff074 100644
--- a/packages/client/ui-chat/tests/chat-apply.client.spec.tsx
+++ b/packages/client/ui-chat/tests/chat-apply.client.spec.tsx
@@ -23,6 +23,7 @@ import {
import type {
ChatNodeTurnDataInjected, ChatSnapshot, TranscriptViewRowInjected, UseChatNodeTurnData,
} from '@deepseek-ai/dsh-client-ui-chat/client'
+import type { PerformanceUsageRowInjected } from '../src/client/settings/PerformanceUsageRow.tsx'
import { CHAT_SETTINGS_NAMESPACE, type ChatSettings } from '../src/chat-settings.ts'
declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
@@ -93,7 +94,7 @@ describe('Chat apply wiring', () => {
expect(b.runtime.slots.entries('conversation.composer.dock').map(row => row.options.id))
.toEqual(['stats'])
expect(b.runtime.slots.entries('settings.general.item').map(row => row.options.id))
- .toEqual(['transcript-view', 'composer-enter'])
+ .toEqual(['transcript-view', 'performance-usage', 'composer-enter'])
await b.runtime.dispose()
})
@@ -109,12 +110,31 @@ describe('Chat apply wiring', () => {
expect(b.chatSettings.set).toHaveBeenCalledWith('transcriptView', 'normal')
b.chatSettings.publish({
- status: 'ready', value: { transcriptView: 'compact' }, revision: 1, writable: true,
+ status: 'ready', value: { transcriptView: 'compact', performanceUsage: 'detailed' }, revision: 1, writable: true,
})
expect(face.hooks.transcriptView.getSnapshot()).toBe('compact')
await b.runtime.dispose()
})
+ it('shares the accepted performance preference with settings, composer, and turn tails', async () => {
+ const b = await bench()
+ const row = b.runtime.slots.entries('settings.general.item').find(entry => entry.options.id === 'performance-usage')!
+ const face = (row.inject as unknown as () => PerformanceUsageRowInjected)()
+ expect(face.hooks.performanceUsage.getSnapshot()).toBe('detailed')
+ face.setPerformanceUsage('compact')
+ expect(b.chatSettings.set).toHaveBeenCalledWith('performanceUsage', 'compact')
+ b.chatSettings.publish({ value: { transcriptView: 'compact', performanceUsage: 'compact' } })
+ expect(face.hooks.performanceUsage.getSnapshot()).toBe('compact')
+ for (const entry of [
+ b.runtime.slots.entries('conversation.composer.dock').find(entry => entry.options.id === 'stats')!,
+ b.runtime.slots.entries('conversation.chat.node').find(entry => entry.options.key === 'turn-tail')!,
+ ]) {
+ const injected = (entry.inject as unknown as () => Pick)()
+ expect(injected.hooks.performanceUsage).toBe(face.hooks.performanceUsage)
+ }
+ await b.runtime.dispose()
+ })
+
it('shares one Chat store while keeping it distinct from Conversation state', async () => {
const b = await bench()
const conversationStore = storeOf(b.runtime, 'conversation.session')
diff --git a/packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx b/packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx
index cc447d8ff0..4e2493d17c 100644
--- a/packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx
+++ b/packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx
@@ -1064,6 +1064,7 @@ describe('small branch tails', () => {
const source = { getSnapshot: () => snap, subscribe: () => () => {} }
const view = render(
selector('detailed')}
t={t}
useChat={bindSnapshotSelector(source)}
useProjection={(key: string) => key === 'tokenUsage'
diff --git a/packages/client/ui-chat/tests/chat-settings.client.spec.ts b/packages/client/ui-chat/tests/chat-settings.client.spec.ts
index 54ecf4c63e..ad5ae6ab89 100644
--- a/packages/client/ui-chat/tests/chat-settings.client.spec.ts
+++ b/packages/client/ui-chat/tests/chat-settings.client.spec.ts
@@ -21,10 +21,13 @@ describe('ui-chat Host settings', () => {
await fiber.await()
const ns = CHAT_SETTINGS_NAMESPACE
- expect(ctx.settings.get(ns)).toEqual({ transcriptView: DEFAULT_TRANSCRIPT_VIEW_MODE })
+ expect(ctx.settings.get(ns)).toEqual({ transcriptView: DEFAULT_TRANSCRIPT_VIEW_MODE, performanceUsage: 'detailed' })
await ctx.settings.update(ns, { transcriptView: 'normal' })
- expect(ctx.settings.get(ns)).toEqual({ transcriptView: 'normal' })
+ expect(ctx.settings.get(ns)).toEqual({ transcriptView: 'normal', performanceUsage: 'detailed' })
await expect(ctx.settings.update(ns, { transcriptView: 'dense' })).rejects.toThrow()
+ await ctx.settings.update(ns, { performanceUsage: 'compact' })
+ expect(ctx.settings.get(ns)).toMatchObject({ performanceUsage: 'compact' })
+ await expect(ctx.settings.update(ns, { performanceUsage: 'hidden' })).rejects.toThrow()
await fiber.dispose()
expect(ctx.settings.describe().map(row => row.ns)).not.toContain(ns)
diff --git a/packages/client/ui-chat/tests/chat-snapshot-fixture.client.ts b/packages/client/ui-chat/tests/chat-snapshot-fixture.client.ts
index 6914089291..21b0d483ff 100644
--- a/packages/client/ui-chat/tests/chat-snapshot-fixture.client.ts
+++ b/packages/client/ui-chat/tests/chat-snapshot-fixture.client.ts
@@ -8,7 +8,6 @@ import type {
ConversationLocationDataSource, ConversationLocationDataStore, ConversationTurnDataMap, TurnLocation,
} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type { TurnTokenUsage } from '../src/client/contract/chat-nodes.ts'
-import { deriveTurnMetrics } from '../src/client/contract/turn-metrics.ts'
import {
sameTurnNavigationItem, turnNavigationItem,
} from '../src/client/conversation-nodes/turn-navigation.ts'
@@ -461,7 +460,6 @@ export function chatSnapshotFixture(input: {
return (location.kind === 'turn' || location.kind === 'step')
&& location.turn.turn === turnNumber
})
- const metrics = deriveTurnMetrics(legacy.nodes).get(turnNumber)
const tokenUsage = input.turnUsages?.get(turnNumber)
const tailData = {
turn: turnNumber,
@@ -471,8 +469,6 @@ export function chatSnapshotFixture(input: {
branchUnavailable: closing === null
|| preceding?.kind !== 'assistant-step'
|| (preceding.data as ReturnType).finalNode.seq !== closing.finalNode.seq,
- ...metrics?.ttftMs === undefined ? {} : { ttftMs: metrics.ttftMs },
- ...metrics?.tokensPerSecond === undefined ? {} : { tokensPerSecond: metrics.tokensPerSecond },
...tokenUsage === undefined ? {} : { tokenUsage },
}
dataStore.set('turn-tail', tailData)
diff --git a/packages/client/ui-chat/tests/chat-stats.client.spec.tsx b/packages/client/ui-chat/tests/chat-stats.client.spec.tsx
index af16ba4d43..fdf4556bce 100644
--- a/packages/client/ui-chat/tests/chat-stats.client.spec.tsx
+++ b/packages/client/ui-chat/tests/chat-stats.client.spec.tsx
@@ -145,7 +145,7 @@ describe('StatsPills', () => {
source: { getSnapshot(): ChatSnapshot; subscribe(fn: () => void): () => void },
values: Record = { tokenUsage: USAGE },
): StatsPillsProps {
- return { useChat: bindSnapshotSelector(source), useProjection: projections(values), t: tEn }
+ return { usePerformanceUsage: selector => selector('detailed'), useChat: bindSnapshotSelector(source), useProjection: projections(values), t: tEn }
}
function tokenUsage(cacheReadTokens: number, uncachedInputTokens: number) {
@@ -158,6 +158,27 @@ describe('StatsPills', () => {
timing: { stepStartTime: 1_000, firstTokenTime: 1_800, completedTime: 4_800 },
})
+ it('compact keeps only speed and cache hit, with no interactive statistics', () => {
+ const { source } = makeSource({ nodes: [timedStep()] })
+ const view = render( selector('compact')} />)
+ expect(view.container.textContent).toBe('20 tok/sCache hit 90%')
+ expect(view.queryByRole('button')).toBeNull()
+ fireEvent.mouseOver(view.getByText('20 tok/s'))
+ expect(view.queryByRole('dialog')).toBeNull()
+ view.rerender()
+ expect(view.getAllByRole('button')).toHaveLength(2)
+ fireEvent.click(view.getAllByRole('button')[0]!)
+ expect(view.getByRole('dialog')).toBeTruthy()
+ view.rerender( selector('compact')} />)
+ expect(view.queryByRole('dialog')).toBeNull()
+ })
+
+ it('compact omits unavailable metrics instead of showing counts', () => {
+ const { source } = makeSource({ nodes: [assistant(1, 1)] })
+ const view = render( selector('compact')} />)
+ expect(view.container.textContent).toBe('')
+ })
+
it('renders the counts reading and usage pill and hides a brand-new empty session', () => {
const { source } = makeSource({ nodes: [assistant(1, 1)] })
const view = render()
diff --git a/packages/client/ui-chat/tests/chat-view.client.spec.tsx b/packages/client/ui-chat/tests/chat-view.client.spec.tsx
index 7913f00b7c..a25692a2d5 100644
--- a/packages/client/ui-chat/tests/chat-view.client.spec.tsx
+++ b/packages/client/ui-chat/tests/chat-view.client.spec.tsx
@@ -275,6 +275,7 @@ function makeHarness(
// Rows and the harness must observe the same chat-store instance.
const chat = createChatStore().create()
const transcriptView = createSnapshotStore('compact')
+ const performanceUsage = createSnapshotStore<'compact' | 'detailed'>('detailed')
const t = makeTranslate(zh, commonZh)
const toolOwners: Array<{
callId: string
@@ -334,6 +335,7 @@ function makeHarness(
case 'turn-tail':
return (
()}
renderSlot={renderTurnTailSlot}
SessionProvider={props.SessionProvider}
@@ -442,6 +444,7 @@ function makeHarness(
openFile, openSkill, loadOlder, loadThrough, openView,
setOutline: (value: unknown) => { outlineValue = value },
chatScroll, forkAt, toolOwners,
+ setPerformanceUsage: (mode: 'compact' | 'detailed') => { performanceUsage.set(mode) },
setTranscriptView: (mode: TranscriptViewMode) => { transcriptView.set(mode) },
setNodeRenderer: (renderer: React.ComponentProps['renderSlot']) => {
nodeSlotOverride = renderer
@@ -1896,7 +1899,7 @@ describe('ChatView', () => {
expect(view.getAllByRole('button', { name: '复制' })).toHaveLength(4)
})
- it('the actions-owning assistant footer shows the turn run time', () => {
+ it('the assistant footer omits turn run time', () => {
const h = makeHarness({
nodes: [
user(1, 'hi'), // time 1_000
@@ -1909,10 +1912,10 @@ describe('ChatView', () => {
})
const view = render()
// The exact turn/end includes trailing tool activity after the final text.
- expect(view.container.querySelector('[data-turn-tail="1"]')?.textContent).toContain('用时 19秒')
+ expect(view.container.querySelector('[data-turn-tail="1"]')?.textContent).not.toContain('用时 19秒')
})
- it('the actions-owning assistant footer shows an hour-scale run time', () => {
+ it('the assistant footer omits hour-scale run time', () => {
const h = makeHarness({
nodes: [
user(1, 'hi'),
@@ -1925,10 +1928,10 @@ describe('ChatView', () => {
})
const view = render()
expect(view.container.querySelector('[data-turn-tail="1"]')?.textContent)
- .toContain('用时 1小时05分03秒')
+ .not.toContain('用时 1小时05分03秒')
})
- it('the settled footer exposes ttft, decode throughput, and usage as the details trigger', () => {
+ it('the settled footer shows usage only in Detailed mode', () => {
const first: AssistantMessageNode = {
kind: 'assistant', seq: 2, time: 2_000, turn: 1, step: 1, blocks: [{ kind: 'text', text: 'mid' }],
timing: { stepStartTime: 1_000, firstTokenTime: 2_200, completedTime: 5_200 },
@@ -1962,17 +1965,11 @@ describe('ChatView', () => {
expect(dialog.textContent).toContain('缓存命中49.4%')
expect(dialog.textContent).toContain('未缓存输入5,060 tok')
fireEvent.keyDown(document, { key: 'Escape' })
- // The time pill carries the run time; first-step ttft (1.2s) and 100
- // tokens over 5s of decode move into its dialog.
- const timeTrigger = view.getByRole('button', { name: /用时 19秒/ })
- expect(timeTrigger.textContent).toBe('用时 19秒')
- expect(view.queryByText(/速度 20 tok\/s|首 token/)).toBeNull()
- fireEvent.click(timeTrigger)
- const timeDialog = view.getByRole('dialog')
- expect(timeDialog.getAttribute('aria-label')).toBe('本轮用时和速度')
- expect(timeDialog.textContent).toContain('本轮总用时19秒')
- expect(timeDialog.textContent).toContain('输出速度(TPS)20 tok/s')
- expect(timeDialog.textContent).toContain('首 token 用时(TTFT)1.2秒')
+ expect(view.queryByRole('button', { name: /用时/ })).toBeNull()
+ act(() => { h.setPerformanceUsage('compact') })
+ expect(view.queryByRole('button', { name: /用量/ })).toBeNull()
+ act(() => { h.setPerformanceUsage('detailed') })
+ expect(view.getByRole('button', { name: /用量/ })).toBeTruthy()
})
it('withholds the usage-details trigger when turn usage is outside the window', () => {
@@ -1987,9 +1984,7 @@ describe('ChatView', () => {
turnEnds: new Map([[1, 20]]),
})
const view = render()
- // Timing facts keep their pill, but with no usage in the window there is
- // no usage pill to click.
- expect(view.getByRole('button', { name: /用时/ })).toBeTruthy()
+ expect(view.queryByRole('button', { name: /用时/ })).toBeNull()
expect(view.queryByRole('button', { name: /用量/ })).toBeNull()
})
diff --git a/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts b/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts
index 7caa2f8fd9..f393a952bf 100644
--- a/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts
+++ b/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts
@@ -987,8 +987,6 @@ describe('built-in conversation node Definitions', () => {
})
const tail = node(view, 'turn-tail')?.data as TurnTailChatData
expect(tail.turn).toBe(1)
- expect(tail.ttftMs).toBeUndefined()
- expect(tail.tokensPerSecond).toBeUndefined()
}
})
diff --git a/packages/client/ui-chat/tests/gate-branch-tails.client.spec.tsx b/packages/client/ui-chat/tests/gate-branch-tails.client.spec.tsx
index a778ba7bfb..8bfb4014b6 100644
--- a/packages/client/ui-chat/tests/gate-branch-tails.client.spec.tsx
+++ b/packages/client/ui-chat/tests/gate-branch-tails.client.spec.tsx
@@ -46,6 +46,7 @@ describe('render branch tails', () => {
const source = { getSnapshot: () => snap, subscribe: () => () => {} }
const view = render(
selector('detailed')}
t={t}
useChat={bindSnapshotSelector(source)}
useProjection={() => undefined}
diff --git a/packages/client/ui-chat/tests/performance-usage.client.spec.ts b/packages/client/ui-chat/tests/performance-usage.client.spec.ts
new file mode 100644
index 0000000000..00e862cfe6
--- /dev/null
+++ b/packages/client/ui-chat/tests/performance-usage.client.spec.ts
@@ -0,0 +1,30 @@
+// @vitest-environment jsdom
+import { describe, expect, it } from 'vitest'
+import { stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime'
+import type { ChatSettings } from '../src/chat-settings.ts'
+import { PerformanceUsagePolicy } from '../src/client/performance-usage.ts'
+
+describe('PerformanceUsagePolicy', () => {
+ it('keeps explicit choices live when a memory-only scope cannot persist them', () => {
+ const host = stubSettingsScope()
+ host.publish({ mode: 'memory', status: 'ready' })
+ const policy = new PerformanceUsagePolicy(host.scope)
+ expect(policy.mode.getSnapshot()).toBe('detailed')
+ policy.setMode('compact')
+ expect(policy.mode.getSnapshot()).toBe('compact')
+ expect(host.set).toHaveBeenCalledWith('performanceUsage', 'compact')
+ host.publish({ value: undefined, writable: false })
+ expect(policy.mode.getSnapshot()).toBe('compact')
+ policy.setMode('compact')
+ expect(host.set).toHaveBeenCalledTimes(1)
+ })
+
+ it('adopts accepted Host settings at construction and after updates', () => {
+ const host = stubSettingsScope()
+ host.publish({ value: { transcriptView: 'normal', performanceUsage: 'compact' } })
+ const policy = new PerformanceUsagePolicy(host.scope)
+ expect(policy.mode.getSnapshot()).toBe('compact')
+ host.publish({ value: { transcriptView: 'normal', performanceUsage: 'detailed' } })
+ expect(policy.mode.getSnapshot()).toBe('detailed')
+ })
+})
diff --git a/packages/client/ui-chat/tests/transcript-view-policy.client.spec.ts b/packages/client/ui-chat/tests/transcript-view-policy.client.spec.ts
index b0781e1edf..84717bb5c0 100644
--- a/packages/client/ui-chat/tests/transcript-view-policy.client.spec.ts
+++ b/packages/client/ui-chat/tests/transcript-view-policy.client.spec.ts
@@ -30,18 +30,18 @@ describe('TranscriptViewPolicy', () => {
const host = stubSettingsScope()
const policy = new TranscriptViewPolicy(host.scope)
- host.publish({ status: 'ready', value: { transcriptView: 'normal' }, revision: 1, writable: true })
+ host.publish({ status: 'ready', value: { transcriptView: 'normal', performanceUsage: 'detailed' }, revision: 1, writable: true })
expect(policy.mode.getSnapshot()).toBe('normal')
policy.setMode('normal')
expect(host.set).not.toHaveBeenCalled()
- host.publish({ value: { transcriptView: 'compact' }, revision: 2 })
+ host.publish({ value: { transcriptView: 'compact', performanceUsage: 'detailed' }, revision: 2 })
expect(policy.mode.getSnapshot()).toBe('compact')
})
it('adopts an accepted section standing at construction', () => {
const host = stubSettingsScope()
- host.publish({ status: 'ready', value: { transcriptView: 'normal' }, revision: 1, writable: true })
+ host.publish({ status: 'ready', value: { transcriptView: 'normal', performanceUsage: 'detailed' }, revision: 1, writable: true })
expect(new TranscriptViewPolicy(host.scope).mode.getSnapshot()).toBe('normal')
})
})
diff --git a/packages/client/ui-chat/tests/transcript-view-row.client.spec.tsx b/packages/client/ui-chat/tests/transcript-view-row.client.spec.tsx
index 0c544c1211..f3ac40e704 100644
--- a/packages/client/ui-chat/tests/transcript-view-row.client.spec.tsx
+++ b/packages/client/ui-chat/tests/transcript-view-row.client.spec.tsx
@@ -8,6 +8,8 @@ import type { GlobalStandardProps } from '@deepseek-ai/dsh-client-ui-slots'
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
import { bindSnapshotSelector, makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import { TranscriptViewRow, type TranscriptViewRowProps } from '../src/client/settings/TranscriptViewRow.tsx'
+import { PerformanceUsageRow } from '../src/client/settings/PerformanceUsageRow.tsx'
+import type { PerformanceUsageMode } from '../src/chat-settings.ts'
import { en, zh } from '../src/client/locale.ts'
afterEach(cleanup)
@@ -46,7 +48,7 @@ function mount(mode: 'normal' | 'compact' = 'compact', dictionary: typeof en | t
t: makeTranslate(dictionary),
}
render()
- return { setTranscriptView }
+ return { setTranscriptView, props }
}
describe('TranscriptViewRow', () => {
@@ -76,3 +78,23 @@ describe('TranscriptViewRow', () => {
expect(screen.getByRole('button', { name: '标准' })).toBeDefined()
})
})
+
+
+describe('PerformanceUsageRow', () => {
+ it('selects compact statistics independently of conversation display', () => {
+ const b = mount()
+ const source = createSnapshotStore('detailed')
+ const setPerformanceUsage = vi.fn((mode: PerformanceUsageMode) => { source.set(mode) })
+ render()
+ expect(screen.getByText('Performance & usage')).toBeDefined()
+ fireEvent.click(screen.getByRole('button', { name: 'Detailed' }))
+ fireEvent.click(screen.getByRole('menuitem', { name: 'Compact' }))
+ expect(setPerformanceUsage).toHaveBeenCalledWith('compact')
+ expect(screen.getAllByRole('button', { name: 'Compact' })).toHaveLength(2)
+ expect(b.setTranscriptView).not.toHaveBeenCalled()
+ })
+})
diff --git a/packages/client/ui-chat/tests/turn-metrics.client.spec.ts b/packages/client/ui-chat/tests/turn-metrics.client.spec.ts
index 1d92c61335..7d0ffed6e4 100644
--- a/packages/client/ui-chat/tests/turn-metrics.client.spec.ts
+++ b/packages/client/ui-chat/tests/turn-metrics.client.spec.ts
@@ -1,11 +1,11 @@
-// Per-turn latency/throughput fold and the footer figure formatters.
+// Assistant timing readings and statistics formatting.
import { describe, expect, it } from 'vitest'
import type {
- AssistantMessageNode, ConversationNode, UserMessageNode,
+ AssistantMessageNode,
} from '@deepseek-ai/dsh-client-ui-chat/client'
-import { assistantStepReading, deriveTurnMetrics } from '../src/client/contract/turn-metrics.ts'
-import { formatLatencySeconds, formatTokensPerSecond } from '../src/client/chat/message-chrome.ts'
+import { assistantStepReading } from '../src/client/contract/turn-metrics.ts'
+import { formatTokensPerSecond } from '../src/client/chat/message-chrome.ts'
import { formatCacheHitPercent } from '../src/client/chat/token-format.ts'
interface StepSpec {
@@ -22,10 +22,6 @@ const assistant = ({ seq, turn, step, timing, usage }: StepSpec): AssistantMessa
...(usage === undefined ? {} : { usage }),
})
-const user = (seq: number): UserMessageNode => ({
- kind: 'user', seq, time: seq * 1_000, content: [{ type: 'text', text: 'hi' }] as never, source: null,
-})
-
describe('assistantStepReading', () => {
it('derives ttft, decode time, and output tokens from a fully recorded step', () => {
const reading = assistantStepReading(assistant({
@@ -66,92 +62,11 @@ describe('assistantStepReading', () => {
})
})
-describe('deriveTurnMetrics', () => {
- it('takes ttft from the lowest step and throughput over all sampled steps', () => {
- const nodes: ConversationNode[] = [
- user(1),
- // Out of step order on purpose: the lowest step owns the ttft slot.
- assistant({
- seq: 4, turn: 1, step: 2,
- timing: { stepStartTime: 10_000, firstTokenTime: 10_200, completedTime: 12_200 },
- usage: { outputTokens: 60 },
- }),
- assistant({
- seq: 2, turn: 1, step: 1,
- timing: { stepStartTime: 1_000, firstTokenTime: 2_200, completedTime: 5_200 },
- usage: { outputTokens: 40 },
- }),
- ]
- // 100 tokens over 5s of decode.
- expect(deriveTurnMetrics(nodes).get(1)).toEqual({ ttftMs: 1_200, tokensPerSecond: 20 })
- })
-
- it('emits ttft without throughput when no step carries usage', () => {
- const nodes = [assistant({
- seq: 2, turn: 1, step: 1,
- timing: { stepStartTime: 1_000, firstTokenTime: 1_900, completedTime: 3_000 },
- })]
- expect(deriveTurnMetrics(nodes).get(1)).toEqual({ ttftMs: 900 })
- })
-
- it('emits throughput without ttft when only a later step is recorded', () => {
- const nodes = [
- assistant({ seq: 2, turn: 1, step: 1 }),
- assistant({
- seq: 4, turn: 1, step: 2,
- timing: { stepStartTime: 10_000, firstTokenTime: 10_500, completedTime: 12_500 },
- usage: { outputTokens: 30 },
- }),
- ]
- expect(deriveTurnMetrics(nodes).get(1)).toEqual({ tokensPerSecond: 15 })
- })
-
- it('omits turns with no readings and zero-decode throughput', () => {
- const nodes = [
- assistant({ seq: 2, turn: 1, step: 1 }),
- assistant({
- seq: 4, turn: 2, step: 1,
- timing: { stepStartTime: null, firstTokenTime: 5_000, completedTime: 5_000 },
- usage: { outputTokens: 10 },
- }),
- ]
- expect(deriveTurnMetrics(nodes).size).toBe(0)
- })
-
- it('keeps turns independent and ignores non-assistant nodes', () => {
- const nodes: ConversationNode[] = [
- user(1),
- assistant({
- seq: 2, turn: 1, step: 1,
- timing: { stepStartTime: 1_000, firstTokenTime: 1_400, completedTime: 2_400 },
- usage: { outputTokens: 10 },
- }),
- user(3),
- assistant({
- seq: 4, turn: 2, step: 1,
- timing: { stepStartTime: 4_000, firstTokenTime: 4_100, completedTime: 6_100 },
- usage: { outputTokens: 100 },
- }),
- ]
- const metrics = deriveTurnMetrics(nodes)
- expect(metrics.get(1)).toEqual({ ttftMs: 400, tokensPerSecond: 10 })
- expect(metrics.get(2)).toEqual({ ttftMs: 100, tokensPerSecond: 50 })
- })
-})
-
describe('footer figure formatters', () => {
it('omits a redundant decimal zero in cache-hit percentages', () => {
expect(formatCacheHitPercent(1, 2, 1)).toBe('50')
})
- it('formats latency with one decimal under ten seconds and whole seconds beyond', () => {
- expect(formatLatencySeconds(840)).toBe('0.8')
- expect(formatLatencySeconds(1_000)).toBe('1')
- expect(formatLatencySeconds(9_949)).toBe('9.9')
- expect(formatLatencySeconds(12_400)).toBe('12')
- expect(formatLatencySeconds(-5)).toBe('0')
- })
-
it('formats throughput with whole tokens from ten up and one decimal below', () => {
expect(formatTokensPerSecond(34.4)).toBe('34')
expect(formatTokensPerSecond(9.96)).toBe('10')
diff --git a/packages/client/ui-chat/tests/turn-usage-panel.client.spec.tsx b/packages/client/ui-chat/tests/turn-usage-panel.client.spec.tsx
index cd1704eb61..107ca55f9e 100644
--- a/packages/client/ui-chat/tests/turn-usage-panel.client.spec.tsx
+++ b/packages/client/ui-chat/tests/turn-usage-panel.client.spec.tsx
@@ -4,7 +4,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { cleanup, fireEvent, render } from '@testing-library/react'
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts'
-import { TurnTimePanel, TurnUsagePanel } from '../src/client/chat/TurnUsagePanel.tsx'
+import { TurnUsagePanel } from '../src/client/chat/TurnUsagePanel.tsx'
import type { TurnTokenUsage } from '../src/client/contract/chat-nodes.ts'
import { en } from '../src/client/locale.ts'
@@ -96,38 +96,3 @@ describe('TurnUsagePanel', () => {
expect(view.queryByRole('dialog')).toBeNull()
})
})
-
-describe('TurnTimePanel', () => {
- it('shows a clock-and-duration pill and opens the time dialog on click', () => {
- const view = render(
- ,
- )
- const trigger = view.getByRole('button')
- expect(trigger.textContent).toBe('Ran for 19s')
- expect(trigger.querySelector('svg')).not.toBeNull()
- expect(trigger.getAttribute('aria-haspopup')).toBe('dialog')
- expect(view.queryByRole('dialog')).toBeNull()
-
- fireEvent.click(trigger)
- expect(trigger.getAttribute('aria-expanded')).toBe('true')
- const dialog = view.getByRole('dialog')
- expect(dialog.getAttribute('aria-label')).toBe('Turn time and speed')
- expect(dialog.parentElement).toBe(document.body)
- const details = dialog.querySelector('[data-turn-time-details]') as HTMLElement
- expect(details.textContent).toContain('Total run time19s')
- expect(details.textContent).toContain('Tokens per second (TPS)20 tok/s')
- expect(details.textContent).toContain('Time to first token (TTFT)1.2s')
-
- fireEvent.keyDown(document, { key: 'Escape' })
- expect(view.queryByRole('dialog')).toBeNull()
- })
-
- it('omits unrecorded speed and TTFT rows', () => {
- const view = render()
- fireEvent.click(view.getByRole('button'))
- const dialog = view.getByRole('dialog')
- expect(dialog.textContent).toContain('Total run time3s')
- expect(dialog.textContent).not.toContain('Tokens per second')
- expect(dialog.textContent).not.toContain('Time to first token')
- })
-})
diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
index 690e574c27..927527cd5c 100644
--- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
+++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
@@ -176,7 +176,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.assistant-actions\', () => ctx.slots.register(\n { name: \'conversation.chat.assistant-actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-chat/src/client/contract/slots.ts:219',
+ source: 'packages/client/ui-chat/src/client/contract/slots.ts:227',
},
{
key: 'conversation.chat.commandview',
@@ -224,7 +224,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.commandview\', () => ctx.slots.register(\n { name: \'conversation.chat.commandview\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-chat/src/client/contract/slots.ts:207',
+ source: 'packages/client/ui-chat/src/client/contract/slots.ts:215',
},
{
key: 'conversation.chat.node',
@@ -293,7 +293,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.node\', () => ctx.slots.register(\n { name: \'conversation.chat.node\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-chat/src/client/contract/slots.ts:188',
+ source: 'packages/client/ui-chat/src/client/contract/slots.ts:196',
},
{
key: 'conversation.chat.turnTail',
@@ -354,7 +354,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.turnTail\', () => ctx.slots.register(\n { name: \'conversation.chat.turnTail\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-chat/src/client/contract/slots.ts:213',
+ source: 'packages/client/ui-chat/src/client/contract/slots.ts:221',
},
{
key: 'conversation.composer',
@@ -1128,7 +1128,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.message.images\', () => ctx.slots.register(\n { name: \'conversation.message.images\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-chat/src/client/contract/slots.ts:201',
+ source: 'packages/client/ui-chat/src/client/contract/slots.ts:209',
},
{
key: 'conversation.plan-review.actions',
@@ -1991,6 +1991,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
declaredBy: 'an entry in \'settings.section\' (client-ui-settings-general), so it exists while that entry is mounted',
occupants: [
'client-locale LanguageRow id \'language\'',
+ 'client-ui-chat PerformanceUsageRow id \'performance-usage\'',
'client-ui-chat TranscriptViewRow id \'transcript-view\'',
'client-ui-conversation EnterBehaviorRow id \'composer-enter\'',
'client-ui-permission-presets PermissionRow id \'permission\'',
diff --git a/snapshots/web/changed-files-turn/ui.expected.md b/snapshots/web/changed-files-turn/ui.expected.md
index e6d546d851..35a1a4d004 100644
--- a/snapshots/web/changed-files-turn/ui.expected.md
+++ b/snapshots/web/changed-files-turn/ui.expected.md
@@ -99,5 +99,4 @@
- button "有问题的回答"
- button "在新对话中分支"
- button "用量 36.2K tok"
-- button "用时 {{duration}}"
- text: {{clock}}
diff --git a/snapshots/web/deepseek-messages-chat/ui.expected.md b/snapshots/web/deepseek-messages-chat/ui.expected.md
index 3822fac786..df658c2f8f 100644
--- a/snapshots/web/deepseek-messages-chat/ui.expected.md
+++ b/snapshots/web/deepseek-messages-chat/ui.expected.md
@@ -18,7 +18,6 @@
- button "有问题的回答"
- button "在新对话中分支"
- button "用量 8.2K tok"
-- button "用时 {{duration}}"
- text: {{clock}}
- textbox "发消息或创建任务, / 调用指令, @ 文件或对话"
- button "添加文件或调用指令"
diff --git a/snapshots/web/feedback-command/ack-expanded.expected.md b/snapshots/web/feedback-command/ack-expanded.expected.md
index 679a7f2ecc..193f1e1cff 100644
--- a/snapshots/web/feedback-command/ack-expanded.expected.md
+++ b/snapshots/web/feedback-command/ack-expanded.expected.md
@@ -18,7 +18,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}."'
- textbox "Message or run a task, / commands, @ files or sessions"
diff --git a/snapshots/web/feedback-command/ack.expected.md b/snapshots/web/feedback-command/ack.expected.md
index 8de2c855f3..c9e9bad335 100644
--- a/snapshots/web/feedback-command/ack.expected.md
+++ b/snapshots/web/feedback-command/ack.expected.md
@@ -16,7 +16,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}."'
- textbox "Message or run a task, / commands, @ files or sessions"
diff --git a/snapshots/web/feedback-release/ack-expanded.expected.md b/snapshots/web/feedback-release/ack-expanded.expected.md
index 679a7f2ecc..193f1e1cff 100644
--- a/snapshots/web/feedback-release/ack-expanded.expected.md
+++ b/snapshots/web/feedback-release/ack-expanded.expected.md
@@ -18,7 +18,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}."'
- textbox "Message or run a task, / commands, @ files or sessions"
diff --git a/snapshots/web/feedback-release/ack.expected.md b/snapshots/web/feedback-release/ack.expected.md
index 8de2c855f3..c9e9bad335 100644
--- a/snapshots/web/feedback-release/ack.expected.md
+++ b/snapshots/web/feedback-release/ack.expected.md
@@ -16,7 +16,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}."'
- textbox "Message or run a task, / commands, @ files or sessions"
diff --git a/snapshots/web/file-upload-round/ui.expected.md b/snapshots/web/file-upload-round/ui.expected.md
index dd833a9103..373474e2e9 100644
--- a/snapshots/web/file-upload-round/ui.expected.md
+++ b/snapshots/web/file-upload-round/ui.expected.md
@@ -20,7 +20,6 @@
- button "Bad response"
- button "Branch into a new conversation"
- button "Usage 17.3K tok"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/fresh-round-trip/ui-expanded.expected.md b/snapshots/web/fresh-round-trip/ui-expanded.expected.md
index 59857b3a36..aaa7617775 100644
--- a/snapshots/web/fresh-round-trip/ui-expanded.expected.md
+++ b/snapshots/web/fresh-round-trip/ui-expanded.expected.md
@@ -20,7 +20,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/fresh-round-trip/ui.expected.md b/snapshots/web/fresh-round-trip/ui.expected.md
index 8fcd0762e2..517580bf06 100644
--- a/snapshots/web/fresh-round-trip/ui.expected.md
+++ b/snapshots/web/fresh-round-trip/ui.expected.md
@@ -16,7 +16,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md
index 9dda6dd66b..439bfe0fed 100644
--- a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md
+++ b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md
@@ -42,7 +42,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- button "System prompt"
- button "5 tool calls · 2 messages" [expanded]
@@ -124,7 +123,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- button "Back to bottom"
- textbox "Message or run a task, / commands, @ files or sessions"
diff --git a/snapshots/web/goal-multi-turn-actions/ui.expected.md b/snapshots/web/goal-multi-turn-actions/ui.expected.md
index 22d915bc3e..de78d8224b 100644
--- a/snapshots/web/goal-multi-turn-actions/ui.expected.md
+++ b/snapshots/web/goal-multi-turn-actions/ui.expected.md
@@ -28,7 +28,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- button "System prompt"
- button "5 tool calls · 2 messages"
@@ -85,7 +84,6 @@
- button "Bad response"
- button "Branch into a new conversation"
- tooltip "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md
index 3b481f8cca..bb2569da3d 100644
--- a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md
+++ b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md
@@ -18,7 +18,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/lifecycle-chrome/reloaded.expected.md b/snapshots/web/lifecycle-chrome/reloaded.expected.md
index 7c17dfc106..1cef83a500 100644
--- a/snapshots/web/lifecycle-chrome/reloaded.expected.md
+++ b/snapshots/web/lifecycle-chrome/reloaded.expected.md
@@ -16,7 +16,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/live-interactions/cancel-expanded.expected.md b/snapshots/web/live-interactions/cancel-expanded.expected.md
index 94a19be9f9..38df9a295c 100644
--- a/snapshots/web/live-interactions/cancel-expanded.expected.md
+++ b/snapshots/web/live-interactions/cancel-expanded.expected.md
@@ -18,7 +18,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/live-interactions/cancel.expected.md b/snapshots/web/live-interactions/cancel.expected.md
index b5f6d26fd0..fb25f9e5d1 100644
--- a/snapshots/web/live-interactions/cancel.expected.md
+++ b/snapshots/web/live-interactions/cancel.expected.md
@@ -17,7 +17,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/live-interactions/retry-expanded.expected.md b/snapshots/web/live-interactions/retry-expanded.expected.md
index 0e6187e973..72429a5f1d 100644
--- a/snapshots/web/live-interactions/retry-expanded.expected.md
+++ b/snapshots/web/live-interactions/retry-expanded.expected.md
@@ -20,7 +20,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/live-interactions/retry.expected.md b/snapshots/web/live-interactions/retry.expected.md
index af9acdfd3a..e02699f7b7 100644
--- a/snapshots/web/live-interactions/retry.expected.md
+++ b/snapshots/web/live-interactions/retry.expected.md
@@ -16,7 +16,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/message-actions/compact.expected.md b/snapshots/web/message-actions/compact.expected.md
new file mode 100644
index 0000000000..fe95152fa5
--- /dev/null
+++ b/snapshots/web/message-actions/compact.expected.md
@@ -0,0 +1 @@
+- text: {{throughput}} tok/s Cache hit 98%
diff --git a/snapshots/web/message-actions/ui.expected.md b/snapshots/web/message-actions/ui.expected.md
index 98a48bb121..4f94675057 100644
--- a/snapshots/web/message-actions/ui.expected.md
+++ b/snapshots/web/message-actions/ui.expected.md
@@ -20,9 +20,7 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation" [disabled]
-- text: Available only on the last message of a completed turn
-- button "Ran for {{duration}}"
-- text: {{clock}}
+- text: Available only on the last message of a completed turn {{clock}}
- button "Read a.txt":
- text: Read
- button "a.txt"
@@ -37,7 +35,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}} Keep this later input in the original conversation. {{clock}}
- button "Copy"
- paragraph: ORIGINAL ONLY
@@ -45,7 +42,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/minimal-preset/ui.expected.md b/snapshots/web/minimal-preset/ui.expected.md
index 7ad491e0cf..747da8bbea 100644
--- a/snapshots/web/minimal-preset/ui.expected.md
+++ b/snapshots/web/minimal-preset/ui.expected.md
@@ -19,7 +19,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/plan-review/approved-expanded.expected.md b/snapshots/web/plan-review/approved-expanded.expected.md
index 1c7fce2311..742e32348d 100644
--- a/snapshots/web/plan-review/approved-expanded.expected.md
+++ b/snapshots/web/plan-review/approved-expanded.expected.md
@@ -26,7 +26,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/plan-review/approved.expected.md b/snapshots/web/plan-review/approved.expected.md
index da51f924a1..17cb2ec82d 100644
--- a/snapshots/web/plan-review/approved.expected.md
+++ b/snapshots/web/plan-review/approved.expected.md
@@ -18,7 +18,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/present-svg/ui.expected.md b/snapshots/web/present-svg/ui.expected.md
index 8d1982ff68..3a6f85583c 100644
--- a/snapshots/web/present-svg/ui.expected.md
+++ b/snapshots/web/present-svg/ui.expected.md
@@ -44,5 +44,4 @@
- button "有问题的回答"
- button "在新对话中分支"
- button "用量 32K tok"
-- button "用时 {{duration}}"
- text: {{clock}}
diff --git a/snapshots/web/present/ui.expected.md b/snapshots/web/present/ui.expected.md
index e4c3ec81e6..20ec6c8afe 100644
--- a/snapshots/web/present/ui.expected.md
+++ b/snapshots/web/present/ui.expected.md
@@ -51,7 +51,6 @@
- button "Bad response"
- button "Branch into a new conversation"
- button "Usage 19K tok"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/ptc-round/ui.expected.md b/snapshots/web/ptc-round/ui.expected.md
index e9212ee443..77daf41f67 100644
--- a/snapshots/web/ptc-round/ui.expected.md
+++ b/snapshots/web/ptc-round/ui.expected.md
@@ -27,7 +27,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/question-composer/answered-expanded.expected.md b/snapshots/web/question-composer/answered-expanded.expected.md
index 1da8b2f2cd..58b3ed28b3 100644
--- a/snapshots/web/question-composer/answered-expanded.expected.md
+++ b/snapshots/web/question-composer/answered-expanded.expected.md
@@ -23,7 +23,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/question-composer/answered.expected.md b/snapshots/web/question-composer/answered.expected.md
index d865c5f8e6..6e7738f321 100644
--- a/snapshots/web/question-composer/answered.expected.md
+++ b/snapshots/web/question-composer/answered.expected.md
@@ -16,7 +16,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/queue-actions/preserved-expanded.expected.md b/snapshots/web/queue-actions/preserved-expanded.expected.md
index 22d8b78588..4efa97d7c0 100644
--- a/snapshots/web/queue-actions/preserved-expanded.expected.md
+++ b/snapshots/web/queue-actions/preserved-expanded.expected.md
@@ -18,7 +18,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- button "2 queued messages" [expanded]
- list:
diff --git a/snapshots/web/queue-actions/preserved.expected.md b/snapshots/web/queue-actions/preserved.expected.md
index 6864b1fc3d..099622b124 100644
--- a/snapshots/web/queue-actions/preserved.expected.md
+++ b/snapshots/web/queue-actions/preserved.expected.md
@@ -17,7 +17,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- button "2 queued messages" [expanded]
- list:
diff --git a/snapshots/web/queued-image/delivered.expected.md b/snapshots/web/queued-image/delivered.expected.md
index d2ebeeb0bb..4e3aaa548f 100644
--- a/snapshots/web/queued-image/delivered.expected.md
+++ b/snapshots/web/queued-image/delivered.expected.md
@@ -21,7 +21,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- button "queued.png, click to view original":
- img "queued.png"
@@ -33,7 +32,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}} Continue with the queued comparison {{clock}}
- button "Copy"
- button "Thought for a while"
@@ -42,7 +40,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/seeded-history/command-row.expected.md b/snapshots/web/seeded-history/command-row.expected.md
index 63052fbda9..72449c0f85 100644
--- a/snapshots/web/seeded-history/command-row.expected.md
+++ b/snapshots/web/seeded-history/command-row.expected.md
@@ -26,7 +26,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- button "compact Compacted 5 history items (~{{tokens}} tokens)"
- button "Context injection AGENTS.md"
diff --git a/snapshots/web/seeded-history/feedback-row.expected.md b/snapshots/web/seeded-history/feedback-row.expected.md
index 9238eb5070..ca827139a0 100644
--- a/snapshots/web/seeded-history/feedback-row.expected.md
+++ b/snapshots/web/seeded-history/feedback-row.expected.md
@@ -26,7 +26,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- button "compact Compacted 5 history items (~{{tokens}} tokens)"
- button "Context injection AGENTS.md"
diff --git a/snapshots/web/seeded-history/ui-expanded.expected.md b/snapshots/web/seeded-history/ui-expanded.expected.md
index b7606e8776..cedbc667dd 100644
--- a/snapshots/web/seeded-history/ui-expanded.expected.md
+++ b/snapshots/web/seeded-history/ui-expanded.expected.md
@@ -26,7 +26,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- button "compact Compacted 5 history items (~{{tokens}} tokens)"
- button "Context injection AGENTS.md"
diff --git a/snapshots/web/seeded-history/ui.expected.md b/snapshots/web/seeded-history/ui.expected.md
index 9a40a350d9..57b3f91a06 100644
--- a/snapshots/web/seeded-history/ui.expected.md
+++ b/snapshots/web/seeded-history/ui.expected.md
@@ -18,7 +18,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- button "compact Compacted 5 history items (~{{tokens}} tokens)"
- button "Context injection AGENTS.md"
diff --git a/snapshots/web/skill-tool-row/ui.expected.md b/snapshots/web/skill-tool-row/ui.expected.md
index d3dfcb56f3..d0d16efd83 100644
--- a/snapshots/web/skill-tool-row/ui.expected.md
+++ b/snapshots/web/skill-tool-row/ui.expected.md
@@ -22,7 +22,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{date}} {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/steering/settled-expanded.expected.md b/snapshots/web/steering/settled-expanded.expected.md
index dab18a04b1..a1427d9404 100644
--- a/snapshots/web/steering/settled-expanded.expected.md
+++ b/snapshots/web/steering/settled-expanded.expected.md
@@ -20,7 +20,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/steering/settled.expected.md b/snapshots/web/steering/settled.expected.md
index d327666fc1..04ad8d224d 100644
--- a/snapshots/web/steering/settled.expected.md
+++ b/snapshots/web/steering/settled.expected.md
@@ -18,7 +18,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/subagent-conversation/sidebar-chat.expected.md b/snapshots/web/subagent-conversation/sidebar-chat.expected.md
index 63ccfba712..588d7bc778 100644
--- a/snapshots/web/subagent-conversation/sidebar-chat.expected.md
+++ b/snapshots/web/subagent-conversation/sidebar-chat.expected.md
@@ -7,7 +7,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/subagent-conversation/ui-expanded.expected.md b/snapshots/web/subagent-conversation/ui-expanded.expected.md
index c32ce44656..5e7253e2fe 100644
--- a/snapshots/web/subagent-conversation/ui-expanded.expected.md
+++ b/snapshots/web/subagent-conversation/ui-expanded.expected.md
@@ -24,7 +24,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- button "System prompt"
- text: Now give the same explanation to a human reader. {{clock}}
@@ -36,7 +35,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/subagent-conversation/ui.expected.md b/snapshots/web/subagent-conversation/ui.expected.md
index 63534c81fb..62af856938 100644
--- a/snapshots/web/subagent-conversation/ui.expected.md
+++ b/snapshots/web/subagent-conversation/ui.expected.md
@@ -22,7 +22,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- button "System prompt"
- text: Now give the same explanation to a human reader. {{clock}}
@@ -33,7 +32,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/turn-tail-actions/completed.expected.md b/snapshots/web/turn-tail-actions/completed.expected.md
index 8f3053942f..7b3c3e481e 100644
--- a/snapshots/web/turn-tail-actions/completed.expected.md
+++ b/snapshots/web/turn-tail-actions/completed.expected.md
@@ -17,7 +17,6 @@
- button "Bad response"
- button "Branch into a new conversation"
- button "Usage 15.8K tok"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/turn-tail-actions/focused.expected.md b/snapshots/web/turn-tail-actions/focused.expected.md
index d79d578b20..860191368f 100644
--- a/snapshots/web/turn-tail-actions/focused.expected.md
+++ b/snapshots/web/turn-tail-actions/focused.expected.md
@@ -21,7 +21,6 @@
- button "Bad response"
- button "Branch into a new conversation"
- button "Usage 15.8K tok"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/turn-tail-actions/settled.expected.md b/snapshots/web/turn-tail-actions/settled.expected.md
index 09c57d760e..4312288efa 100644
--- a/snapshots/web/turn-tail-actions/settled.expected.md
+++ b/snapshots/web/turn-tail-actions/settled.expected.md
@@ -18,7 +18,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/turn-tail-actions/usage-expanded.expected.md b/snapshots/web/turn-tail-actions/usage-expanded.expected.md
index f725ddc0c2..64a4b1b0ef 100644
--- a/snapshots/web/turn-tail-actions/usage-expanded.expected.md
+++ b/snapshots/web/turn-tail-actions/usage-expanded.expected.md
@@ -17,7 +17,6 @@
- button "Bad response"
- button "Branch into a new conversation"
- button "Usage 15.8K tok" [expanded]
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/web-search-round/ui.expected.md b/snapshots/web/web-search-round/ui.expected.md
index 32429834ab..27d0cce32c 100644
--- a/snapshots/web/web-search-round/ui.expected.md
+++ b/snapshots/web/web-search-round/ui.expected.md
@@ -18,7 +18,6 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}
- textbox "Message or run a task, / commands, @ files or sessions"
- button "Add files or run commands"
diff --git a/snapshots/web/workflow-run/ui.expected.md b/snapshots/web/workflow-run/ui.expected.md
index f8023a0622..40d39e64cf 100644
--- a/snapshots/web/workflow-run/ui.expected.md
+++ b/snapshots/web/workflow-run/ui.expected.md
@@ -12,5 +12,4 @@
- button "Good response"
- button "Bad response"
- button "Branch into a new conversation"
-- button "Ran for {{duration}}"
- text: {{clock}}