From 9effa0c6b3ab5682b634df98092dd179cfa95ff3 Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Fri, 28 Aug 2026 01:34:18 +0800 Subject: [PATCH] feat(ui-chat): split turn stats into usage and time pills with dialogs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The turn tail's exposed meta line collapses into two icon pills — Usage (database glyph, turn total) and Ran-for (clock glyph, wall time) — each click-opening a details dialog; the calendar clock trails as plain text. Cache hit, TPS, and TTFT move dialog-only, and narrow viewports collapse the pills to bare icons. The TEMPORARY flat variant keeps the whole-line trigger for the A/B test. --- .../conversation-expanded.expected.md | 5 +- .../conversation.expected.md | 5 +- .../markdown-cjk-strong/ui.expected.md | 5 +- .../expected/markdown-images/ui.expected.md | 5 +- .../markdown-inline-code-links/ui.expected.md | 5 +- .../expected/math-rendering/ui.expected.md | 5 +- .../skill-user-invoke/ui-expanded.expected.md | 5 +- .../expected/skill-user-invoke/ui.expected.md | 5 +- .../stats-paged-history/ui.expected.md | 140 ++++++++++--- .../steer-all/settled-expanded.expected.md | 5 +- .../expected/steer-all/settled.expected.md | 5 +- apps/web/tests/turn-tail-actions.e2e.ts | 20 +- .../client/chat/MessageIconActions.module.css | 26 +-- .../src/client/chat/MessageIconActions.tsx | 42 +--- .../src/client/chat/TurnTailNodeView.tsx | 43 +++- .../src/client/chat/TurnUsagePanel.module.css | 95 +++++++-- .../src/client/chat/TurnUsagePanel.tsx | 198 ++++++++++++++++-- packages/client/ui-chat/src/client/locale.ts | 10 +- .../ui-chat/tests/chat-view.client.spec.tsx | 70 ++++++- .../tests/turn-usage-panel.client.spec.tsx | 98 ++++++++- .../client/ui-primitives/src/icons/index.tsx | 17 ++ snapshots/web/code-mode-round/ui.expected.md | 5 +- .../web/cordis-tool-round/ui.expected.md | 15 +- .../feedback-command/ack-expanded.expected.md | 5 +- .../web/feedback-command/ack.expected.md | 5 +- .../feedback-release/ack-expanded.expected.md | 5 +- .../web/feedback-release/ack.expected.md | 5 +- .../fresh-round-trip/ui-expanded.expected.md | 5 +- snapshots/web/fresh-round-trip/ui.expected.md | 5 +- .../ui-expanded.expected.md | 10 +- .../goal-multi-turn-actions/ui.expected.md | 10 +- .../reloaded-expanded.expected.md | 5 +- .../web/lifecycle-chrome/reloaded.expected.md | 5 +- .../cancel-expanded.expected.md | 5 +- .../web/live-interactions/cancel.expected.md | 5 +- .../retry-expanded.expected.md | 5 +- .../web/live-interactions/retry.expected.md | 5 +- snapshots/web/message-actions/ui.expected.md | 11 +- snapshots/web/minimal-preset/ui.expected.md | 5 +- .../plan-review/approved-expanded.expected.md | 5 +- .../web/plan-review/approved.expected.md | 5 +- .../answered-expanded.expected.md | 5 +- .../question-composer/answered.expected.md | 5 +- .../preserved-expanded.expected.md | 5 +- .../web/queue-actions/preserved.expected.md | 5 +- .../seeded-history/command-row.expected.md | 5 +- .../seeded-history/feedback-row.expected.md | 5 +- .../seeded-history/ui-expanded.expected.md | 5 +- snapshots/web/seeded-history/ui.expected.md | 5 +- snapshots/web/skill-tool-row/ui.expected.md | 5 +- .../web/steering/settled-expanded.expected.md | 5 +- snapshots/web/steering/settled.expected.md | 5 +- .../ui-expanded.expected.md | 10 +- .../web/subagent-conversation/ui.expected.md | 10 +- .../turn-tail-actions/completed.expected.md | 9 +- .../web/turn-tail-actions/focused.expected.md | 9 +- .../web/turn-tail-actions/settled.expected.md | 5 +- .../usage-expanded.expected.md | 9 +- snapshots/web/web-search-round/ui.expected.md | 5 +- snapshots/web/workflow-run/ui.expected.md | 5 +- 60 files changed, 834 insertions(+), 218 deletions(-) diff --git a/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md b/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md index 1459a2aa8a..3a29a14b51 100644 --- a/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md +++ b/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md @@ -44,7 +44,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/apps/web/tests/expected/github-ready-review/conversation.expected.md b/apps/web/tests/expected/github-ready-review/conversation.expected.md index c4f9670888..0a5fe67b90 100644 --- a/apps/web/tests/expected/github-ready-review/conversation.expected.md +++ b/apps/web/tests/expected/github-ready-review/conversation.expected.md @@ -36,7 +36,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md b/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md index 65c73793b2..2fba524c5d 100644 --- a/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md +++ b/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md @@ -44,7 +44,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/apps/web/tests/expected/markdown-images/ui.expected.md b/apps/web/tests/expected/markdown-images/ui.expected.md index 3c6a9475f8..3843143d1f 100644 --- a/apps/web/tests/expected/markdown-images/ui.expected.md +++ b/apps/web/tests/expected/markdown-images/ui.expected.md @@ -23,7 +23,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md b/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md index 228443ee1e..2d2127cbb2 100644 --- a/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md +++ b/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md @@ -35,7 +35,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/apps/web/tests/expected/math-rendering/ui.expected.md b/apps/web/tests/expected/math-rendering/ui.expected.md index e063215b77..fba05ffe71 100644 --- a/apps/web/tests/expected/math-rendering/ui.expected.md +++ b/apps/web/tests/expected/math-rendering/ui.expected.md @@ -39,7 +39,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md b/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md index f06169c311..07be7102ff 100644 --- a/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md +++ b/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md @@ -36,7 +36,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/apps/web/tests/expected/skill-user-invoke/ui.expected.md b/apps/web/tests/expected/skill-user-invoke/ui.expected.md index 3281b27094..7ddf071159 100644 --- a/apps/web/tests/expected/skill-user-invoke/ui.expected.md +++ b/apps/web/tests/expected/skill-user-invoke/ui.expected.md @@ -28,7 +28,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/apps/web/tests/expected/stats-paged-history/ui.expected.md b/apps/web/tests/expected/stats-paged-history/ui.expected.md index dc2200a0b3..fc8039e282 100644 --- a/apps/web/tests/expected/stats-paged-history/ui.expected.md +++ b/apps/web/tests/expected/stats-paged-history/ui.expected.md @@ -48,7 +48,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m2 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m2 7/25 {{clock}} - button "Copy": - img - paragraph: r2 @@ -60,7 +63,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m3 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m3 7/25 {{clock}} - button "Copy": - img - paragraph: r3 @@ -72,7 +78,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m4 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m4 7/25 {{clock}} - button "Copy": - img - paragraph: r4 @@ -84,7 +93,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m5 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m5 7/25 {{clock}} - button "Copy": - img - paragraph: r5 @@ -96,7 +108,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m6 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m6 7/25 {{clock}} - button "Copy": - img - paragraph: r6 @@ -108,7 +123,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m7 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m7 7/25 {{clock}} - button "Copy": - img - paragraph: r7 @@ -120,7 +138,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m8 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m8 7/25 {{clock}} - button "Copy": - img - paragraph: r8 @@ -132,7 +153,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m9 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m9 7/25 {{clock}} - button "Copy": - img - paragraph: r9 @@ -144,7 +168,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m10 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m10 7/25 {{clock}} - button "Copy": - img - paragraph: r10 @@ -156,7 +183,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m11 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m11 7/25 {{clock}} - button "Copy": - img - paragraph: r11 @@ -168,7 +198,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m12 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m12 7/25 {{clock}} - button "Copy": - img - paragraph: r12 @@ -180,7 +213,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m13 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m13 7/25 {{clock}} - button "Copy": - img - paragraph: r13 @@ -192,7 +228,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m14 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m14 7/25 {{clock}} - button "Copy": - img - paragraph: r14 @@ -204,7 +243,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m15 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m15 7/25 {{clock}} - button "Copy": - img - paragraph: r15 @@ -216,7 +258,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m16 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m16 7/25 {{clock}} - button "Copy": - img - paragraph: r16 @@ -228,7 +273,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m17 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m17 7/25 {{clock}} - button "Copy": - img - paragraph: r17 @@ -240,7 +288,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m18 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m18 7/25 {{clock}} - button "Copy": - img - paragraph: r18 @@ -252,7 +303,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m19 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m19 7/25 {{clock}} - button "Copy": - img - paragraph: r19 @@ -264,7 +318,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m20 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m20 7/25 {{clock}} - button "Copy": - img - paragraph: r20 @@ -276,7 +333,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m21 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m21 7/25 {{clock}} - button "Copy": - img - paragraph: r21 @@ -288,7 +348,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m22 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m22 7/25 {{clock}} - button "Copy": - img - paragraph: r22 @@ -300,7 +363,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m23 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m23 7/25 {{clock}} - button "Copy": - img - paragraph: r23 @@ -312,7 +378,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m24 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m24 7/25 {{clock}} - button "Copy": - img - paragraph: r24 @@ -324,7 +393,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m25 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m25 7/25 {{clock}} - button "Copy": - img - paragraph: r25 @@ -336,7 +408,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m26 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m26 7/25 {{clock}} - button "Copy": - img - paragraph: r26 @@ -348,7 +423,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m27 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m27 7/25 {{clock}} - button "Copy": - img - paragraph: r27 @@ -360,7 +438,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} m28 7/25 {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} m28 7/25 {{clock}} - button "Copy": - img - paragraph: r28 @@ -372,7 +453,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} - button "Back to bottom": - img - textbox "Message or run a task... / commands, @ files or sessions" diff --git a/apps/web/tests/expected/steer-all/settled-expanded.expected.md b/apps/web/tests/expected/steer-all/settled-expanded.expected.md index c220ca0d5d..73e987dfa2 100644 --- a/apps/web/tests/expected/steer-all/settled-expanded.expected.md +++ b/apps/web/tests/expected/steer-all/settled-expanded.expected.md @@ -46,7 +46,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/apps/web/tests/expected/steer-all/settled.expected.md b/apps/web/tests/expected/steer-all/settled.expected.md index 7a9146a5b5..cd48c29075 100644 --- a/apps/web/tests/expected/steer-all/settled.expected.md +++ b/apps/web/tests/expected/steer-all/settled.expected.md @@ -34,7 +34,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/apps/web/tests/turn-tail-actions.e2e.ts b/apps/web/tests/turn-tail-actions.e2e.ts index 99b2572d60..40f5f0e017 100644 --- a/apps/web/tests/turn-tail-actions.e2e.ts +++ b/apps/web/tests/turn-tail-actions.e2e.ts @@ -173,10 +173,12 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => { const trigger = page.getByRole('button', { name: /Usage 15\.8K tok/ }) await expect.poll(() => trigger.count(), { timeout: 10_000 }).toBe(1) expect(await trigger.getAttribute('aria-expanded')).toBe('false') - // The pill carries the icon, the turn total, and the cache-hit rate; - // timing facts stay in the plain meta line beside it. - expect(await trigger.textContent()).toBe('Usage 15.8K tok · Cache hit 49.7%') - expect(await page.getByText(/^.+ · Ran for .+ · \d+ tok\/s · TTFT \S+$/).count()).toBe(1) + // The usage pill carries the icon and the turn total; the time pill beside + // it carries the run time, and both keep their details dialog-only. + expect(await trigger.textContent()).toBe('Usage 15.8K tok') + const timeTrigger = page.getByRole('button', { name: /^Ran for \S+$/ }) + expect(await timeTrigger.count()).toBe(1) + expect(await page.locator('[data-turn-tail]').getByText(/tok\/s|TTFT/).count()).toBe(0) expect(await page.getByRole('dialog').count()).toBe(0) await trigger.click() @@ -189,6 +191,16 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => { expect(await dialog.getByText('7,808 tok', { exact: true }).count()).toBe(1) expect(await dialog.getByText('112 tok (42 tok reasoning)', { exact: true }).count()).toBe(1) expect(await dialog.getByText('15,811 tok', { exact: true }).count()).toBe(1) + await page.keyboard.press('Escape') + expect(await page.getByRole('dialog').count()).toBe(0) + + await timeTrigger.click() + const timeDialog = page.getByRole('dialog', { name: 'Turn time and speed' }) + expect(await timeDialog.count()).toBe(1) + expect(await timeDialog.getByText(/tok\/s/).count()).toBe(1) + expect(await timeDialog.getByText('Average time to first token (TTFT)', { exact: true }).count()).toBe(1) + await page.keyboard.press('Escape') + await trigger.click() const expanded = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd) await compareOrRefreshGolden(USAGE_EXPANDED_EXPECTED, expanded, MODE) diff --git a/packages/client/ui-chat/src/client/chat/MessageIconActions.module.css b/packages/client/ui-chat/src/client/chat/MessageIconActions.module.css index 37b48b77dc..2f6ec71c85 100644 --- a/packages/client/ui-chat/src/client/chat/MessageIconActions.module.css +++ b/packages/client/ui-chat/src/client/chat/MessageIconActions.module.css @@ -20,36 +20,16 @@ white-space: nowrap; } -/* The assistant tail's clock reads the secondary tier: its row carries the - detailed-usage cluster, so the whole meta line sits one step under body. */ +/* The assistant tail's clock sits at the row end after the stat pills; the + row's 8px flex gap spaces it, and it reads the secondary tier so it matches + the pill labels beside it. */ .timeEnd { - padding-left: 12px; font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(24px + var(--dsh-content-font-delta, 0px)); color: var(--dsw-alias-label-tertiary); white-space: nowrap; } -/* Separator between the clock and the run-time label (time · Ran for 15s). */ -.runTimeDot { - margin: 0 5px; -} - -/* Separator between the usage pill and the plain meta text; the row's 8px - flex gap spaces it, so the meta line drops its own 12px inset. The negative - margin rebates most of the pill's internal 8px padding so the label-to-dot - distance reads like the dot's other side, while keeping 2px clear of the - pill's hover background. */ -.actionsDot { - margin-left: -6px; - font-size: var(--dsh-content-font-size-secondary, 13px); - color: var(--dsw-alias-label-tertiary); -} - -.actionsDot + .timeEnd { - padding-left: 0; -} - /* Row containers (user rows and turn tails) opt in with data-actions-reveal='hover': every row but the latest of its kind reveals the entire actions row (icons and text) on hover or focus-within, while the diff --git a/packages/client/ui-chat/src/client/chat/MessageIconActions.tsx b/packages/client/ui-chat/src/client/chat/MessageIconActions.tsx index b53ff20ae6..5476f1a5c0 100644 --- a/packages/client/ui-chat/src/client/chat/MessageIconActions.tsx +++ b/packages/client/ui-chat/src/client/chat/MessageIconActions.tsx @@ -1,14 +1,12 @@ // Shared IconActions chrome for user and assistant messages: copy // live, optional branch wiring, and an optional date-aware clock. -import { Fragment, useCallback, useEffect, useId, useRef, useState, type ReactNode } from 'react' +import { useCallback, useEffect, useId, useRef, useState, type ReactNode } from 'react' import { IconBranchOutline16, IconCheckOutline16, IconCopyOutline16, Tooltip, writeClipboard, } from '@deepseek-ai/dsh-client-ui-primitives' import type { ChatViewSlotProps } from '../contract/slots.ts' -import { - formatLatencySeconds, formatMessageClock, formatRunDuration, formatTokensPerSecond, -} from './message-chrome.ts' +import { formatMessageClock } from './message-chrome.ts' import { useCalendarDay } from './use-calendar-day.ts' import css from './MessageIconActions.module.css' @@ -17,12 +15,6 @@ export interface MessageIconActionsProps { text: string /** Unix epoch ms for the clock label; omitted for transient messages. */ time?: number | undefined - /** Turn wall time in ms, appended to the clock as `· Ran for 15s`; omitted when the turn's start is unknown. */ - runMs?: number | undefined - /** Turn decode throughput, appended as `· 34 tok/s`; omitted when unrecorded. */ - tokensPerSecond?: number | undefined - /** Turn first-step TTFT in ms, appended as `· TTFT 1.2s`; omitted when unrecorded. */ - ttftMs?: number | undefined /** Clock before icons (user) or after (assistant). */ clock: 'start' | 'end' /** Fork the session at this message; omission hides the branch action. */ @@ -51,7 +43,7 @@ export interface MessageIconActionsProps { * @returns The actions row element. */ export function MessageIconActions({ - text, time, runMs, tokensPerSecond, ttftMs, clock, onBranch, branchUnavailable = false, className, + text, time, clock, onBranch, branchUnavailable = false, className, extraActions, usageAction, t, }: MessageIconActionsProps) { const day = useCalendarDay() @@ -82,30 +74,9 @@ export function MessageIconActions({ }, 1000) }) }, [copied, text]) - // The dots are decorative and stay hidden, but their margins separate the - // readings only on screen: without the flanking spaces a reader hears one - // run-on string ("8/26 22:08Ran for 13s") instead of separate facts. - const meta: string[] = [] - if (time !== undefined) meta.push(formatMessageClock(time, t, day)) - if (runMs !== undefined) meta.push(t('message.ranFor', { duration: formatRunDuration(runMs, t) })) - if (tokensPerSecond !== undefined) { - meta.push(t('message.turnUsage.speed', { tps: formatTokensPerSecond(tokensPerSecond) })) - } - if (ttftMs !== undefined) meta.push(t('message.ttft', { seconds: formatLatencySeconds(ttftMs) })) - const clockEl = meta.length === 0 ? null : ( + const clockEl = time === undefined ? null : ( - {meta.map((segment, index) => ( - - {index > 0 && ( - <> - {' '} - · - {' '} - - )} - {segment} - - ))} + {formatMessageClock(time, t, day)} ) return ( @@ -137,9 +108,6 @@ export function MessageIconActions({ {t('message.branchUnavailable')} )} {usageAction} - {usageAction != null && clock === 'end' && clockEl !== null && ( - · - )} {clock === 'end' ? clockEl : null} ) diff --git a/packages/client/ui-chat/src/client/chat/TurnTailNodeView.tsx b/packages/client/ui-chat/src/client/chat/TurnTailNodeView.tsx index 874eff5712..c54a093c36 100644 --- a/packages/client/ui-chat/src/client/chat/TurnTailNodeView.tsx +++ b/packages/client/ui-chat/src/client/chat/TurnTailNodeView.tsx @@ -2,7 +2,7 @@ import { memo } from 'react' import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' import type { ChatNodeViewProps, TurnTailOwnerProps } from '../contract/slots.ts' import { MessageIconActions } from './MessageIconActions.tsx' -import { TurnUsagePanel } from './TurnUsagePanel.tsx' +import { TurnTimePanel, TurnUsagePanel } from './TurnUsagePanel.tsx' import { assistantText } from './turn-assistant.ts' import css from './TurnTailNodeView.module.css' @@ -28,6 +28,14 @@ export const TurnTailNodeView = memo(function TurnTailNodeView({ const runMs = turn.start === undefined || turn.end === undefined ? undefined : Math.max(0, turn.end.time - turn.start.time) + // TEMPORARY usage-variant debug switch for user testing: `?usage-variant=flat` + // or `#usage-variant=flat` (the hash form survives the login token's 303 + // redirect to a clean `/`) hands the whole meta line to TurnUsagePanel as the + // dialog trigger; the default keeps the icon pill beside plain meta text. + // Delete after the test. + const flatVariant = data.tokenUsage !== undefined + && [window.location.search, window.location.hash.replace(/^#/, '')] + .some(query => new URLSearchParams(query).get('usage-variant') === 'flat') // Interruption-frozen partials carry no messageId, so they address no // durable message and contribute no per-message actions. const messageId = closing.finalNode.messageId @@ -43,18 +51,37 @@ export const TurnTailNodeView = memo(function TurnTailNodeView({ {tail} { forkAt(closing.finalNode.seq) }} branchUnavailable={data.branchUnavailable || hasLaterChatNode} className={css.actions} extraActions={assistantActions} - usageAction={data.tokenUsage === undefined ? null : ( - - )} + usageAction={flatVariant + ? data.tokenUsage !== undefined && ( + + ) + : ( + <> + {data.tokenUsage !== undefined && } + {runMs !== undefined && ( + + )} + + )} t={t} /> diff --git a/packages/client/ui-chat/src/client/chat/TurnUsagePanel.module.css b/packages/client/ui-chat/src/client/chat/TurnUsagePanel.module.css index f95534d7c0..d0af40f41f 100644 --- a/packages/client/ui-chat/src/client/chat/TurnUsagePanel.module.css +++ b/packages/client/ui-chat/src/client/chat/TurnUsagePanel.module.css @@ -7,6 +7,13 @@ min-width: 0; } +/* Adjacent stat pills (usage then time) rebate part of their combined internal + padding so the pair reads as one cluster inside the row's 8px flex gap, + while keeping 2px clear between their hover backgrounds. */ +.root + .root { + margin-left: -6px; +} + /* Same pill rules as the sibling `.action` icon buttons (28px hit height, 15px glyph, tertiary → secondary on hover with the interactive hover background), widened to carry the turn-total label. */ @@ -36,6 +43,36 @@ text-overflow: ellipsis; } +/* TEMPORARY (usage-variant debug): the flat whole-line trigger reads like the + plain meta text, brightening on hover to hint clickability. The margin + widens the row's 8px flex gap off the branch icon beside it. */ +.flatTrigger { + min-width: 0; + margin-left: 6px; + padding: 0; + border: none; + background: transparent; + color: var(--dsw-alias-label-tertiary); + font-size: var(--dsh-content-font-size-secondary, 13px); + font-variant-numeric: tabular-nums; + line-height: calc(24px + var(--dsh-content-font-delta, 0px)); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; +} + +.flatTrigger:hover, +.flatTrigger[aria-expanded='true'] { + color: var(--dsw-alias-label-secondary); +} + +/* TEMPORARY (usage-variant debug): flat-line separators breathe wider than the + plain meta line's 5px runTimeDot rhythm. */ +.flatDot { + margin: 0 8px; +} + .trigger svg { width: calc(15px + var(--dsh-content-font-delta, 0px)); height: calc(15px + var(--dsh-content-font-delta, 0px)); @@ -48,6 +85,13 @@ color: var(--dsw-alias-label-secondary); } +/* Narrow viewport: the pills collapse to bare icons; the dialogs keep the words. */ +@media (max-width: 480px) { + .trigger .label { + display: none; + } +} + /* Portal surface: fixed in the viewport, left/top supplied inline from the anchored-position clamp so the panel keeps its 12px viewport margin instead of hanging off the trigger and clipping at the window edge. Portaled panels @@ -73,13 +117,46 @@ cursor: default; } -/* Dialog heading, same size and weight as the total row it concludes with. */ +/* Dialog heading row: the section name left, the section's headline value + (the usage total) right, both at the primary weight. */ .title { - margin-bottom: 12px; + display: flex; + justify-content: space-between; + gap: 16px; + margin-bottom: 8px; color: var(--dsw-alias-label-primary); font-weight: 500; } +/* Second-section heading (Turn time), spaced well off the usage rows above. */ +.timeTitle { + margin-top: 20px; +} + +/* Rule under each section heading, above its rows. */ +.titleRule { + margin-bottom: 10px; + border-top: 1px solid var(--dsw-alias-border-l2); +} + +.titleValue { + font-variant-numeric: tabular-nums; +} + +/* Section-heading glyph seated left of the section name. */ +.titleLabel { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.titleLabel svg { + width: 14px; + height: 14px; + flex: none; +} + .details { display: grid; grid-template-columns: minmax(76px, auto) minmax(0, 1fr); @@ -108,17 +185,3 @@ color: var(--dsw-alias-label-tertiary); white-space: nowrap; } - -/* Full-bleed rule above the total row: the grid's column gap would break a - per-cell border-top, so the line is its own row spanning both columns. */ -.separator { - grid-column: 1 / -1; - margin: 4px 0; - border-top: 1px solid var(--dsw-alias-border-l2); -} - -.totalLabel, -.details .totalValue { - color: var(--dsw-alias-label-primary); - font-weight: 500; -} diff --git a/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx b/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx index 79cbb6617b..7ef5d5489a 100644 --- a/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx +++ b/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx @@ -1,17 +1,48 @@ -// Icon-row Turn-usage action: a data-icon pill labelled with the turn total -// (and the cache-hit rate when known) that click-opens the per-Turn details -// dialog. Sits right of the branch action in the tail's IconActions row. +// 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. +// TEMPORARY: the `flat` variant (usage-variant debug switch) renders the +// whole meta line as one trigger whose dialog stacks both sections; one of +// the two variants will be deleted after user testing. -import { useEffect, useRef, useState, type CSSProperties } from 'react' +import { Fragment, useEffect, useRef, useState, type CSSProperties, type MutableRefObject } from 'react' import { createPortal } from 'react-dom' -import { IconDataOutline16, useAnchoredPosition } from '@deepseek-ai/dsh-client-ui-primitives' +import { + IconClockOutline16, IconDatabaseOutline16, useAnchoredPosition, +} from '@deepseek-ai/dsh-client-ui-primitives' import type { TurnTokenUsage } from '../contract/chat-nodes.ts' import type { ChatViewSlotProps } from '../contract/slots.ts' +import { + formatLatencySeconds, formatMessageClock, formatRunDuration, formatTokensPerSecond, +} from './message-chrome.ts' import { formatCacheHitPercent, formatExactTokens, formatTokens } from './token-format.ts' +import { useCalendarDay } from './use-calendar-day.ts' import css from './TurnUsagePanel.module.css' export interface TurnUsagePanelProps { usage: TurnTokenUsage + /** Trigger form: the default icon pill or the temporary flat whole-line text. */ + variant?: 'pill' | 'flat' + /** Unix epoch ms for the flat trigger's clock segment. */ + time?: number | undefined + /** Turn wall time in ms; the flat trigger and its dialog's Turn-time section show it. */ + runMs?: number | undefined + /** Turn decode throughput; the flat trigger and its dialog's Turn-time section show it. */ + tokensPerSecond?: number | undefined + /** Turn first-step TTFT in ms; the flat trigger and its dialog's Turn-time section show it. */ + ttftMs?: number | undefined + /** The owning view's locale seat, passed down as a plain prop. */ + 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'] } @@ -36,12 +67,16 @@ const PANEL_GAP = 8 */ const MEASURE_STYLE: CSSProperties = { visibility: 'hidden', left: 0, top: 0 } -/** - * Turn-usage IconActions pill with a click-open Turn-details dialog. - * @param props - Turn usage buckets and locale seat. - * @returns The icon-and-total trigger and, while open, its portaled dialog anchored above the trigger. - */ -export function TurnUsagePanel({ usage, t }: TurnUsagePanelProps) { +interface StatDialogSeat { + open: boolean + setOpen: (open: boolean) => void + rootRef: MutableRefObject + panelRef: MutableRefObject + pos: CSSProperties | null +} + +/** One trigger-anchored dialog seat: open state, viewport-clamped placement, outside-close. */ +function useStatDialog(): StatDialogSeat { const [open, setOpen] = useState(false) const rootRef = useRef(null) const panelRef = useRef(null) @@ -77,27 +112,88 @@ export function TurnUsagePanel({ usage, t }: TurnUsagePanelProps) { } }, [open]) + return { open, setOpen, rootRef, panelRef, pos } +} + +/** Turn-time dialog rows: run time plus throughput and TTFT when known. */ +function TimeDetails({ runMs, tokensPerSecond, ttftMs, t }: { + runMs: number | undefined + tokensPerSecond: number | undefined + ttftMs: number | undefined + t: ChatViewSlotProps['t'] +}) { + return ( +
+ {runMs !== undefined && ( + <> +
{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) })}
+ + )} +
+ ) +} + +/** + * Turn-usage IconActions pill (or the temporary flat meta-line trigger) with + * a click-open Turn-usage details dialog. + * @param props - Turn usage buckets, trigger form, and locale seat. + * @returns The trigger and, while open, its portaled dialog anchored above the trigger. + */ +export function TurnUsagePanel({ usage, variant = 'pill', time, runMs, tokensPerSecond, ttftMs, t }: TurnUsagePanelProps) { + const { open, setOpen, rootRef, panelRef, pos } = useStatDialog() + const day = useCalendarDay() + const cacheHit = usage.cacheReadTokens === undefined ? null : formatCacheHitPercent(usage.cacheReadTokens, usage.totalTokens - usage.outputTokens, 1) const total = formatCompactCount(usage.totalTokens, t) const routes = usage.routes?.map(route => `${route.provider}/${route.model}`).join(', ') ?? '' + const flatSegments: string[] = [] + if (variant === 'flat') { + if (time !== undefined) flatSegments.push(formatMessageClock(time, t, day)) + if (runMs !== undefined) flatSegments.push(t('message.ranFor', { duration: formatRunDuration(runMs, t) })) + flatSegments.push(t('message.turnUsage.consumed', { total })) + if (cacheHit !== null) flatSegments.push(t('message.turnUsage.cacheHitRate', { percent: cacheHit })) + if (tokensPerSecond !== undefined) { + flatSegments.push(t('message.turnUsage.speed', { tps: formatTokensPerSecond(tokensPerSecond) })) + } + if (ttftMs !== undefined) flatSegments.push(t('message.ttft', { seconds: formatLatencySeconds(ttftMs) })) + } + const hasTimeFacts = runMs !== undefined || tokensPerSecond !== undefined || ttftMs !== undefined return ( {open && createPortal(
-
{t('message.turnUsage.title')}
+
+ + + {t('message.turnUsage.title')} + + {formatExactCount(usage.totalTokens, t)} +
+
{routes !== '' && ( <> @@ -144,10 +247,61 @@ export function TurnUsagePanel({ usage, t }: TurnUsagePanelProps) { )} -
-
{t('message.turnUsage.total')}
-
{formatExactCount(usage.totalTokens, t)}
+ {variant === 'flat' && hasTimeFacts && ( + <> +
+ + + {t('message.turnTime.title')} + +
+
+ + + )} +
, + document.body, + )} + + ) +} + +/** + * 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')} + +
+
+
, document.body, )} diff --git a/packages/client/ui-chat/src/client/locale.ts b/packages/client/ui-chat/src/client/locale.ts index cb34f59c7c..9236610d4f 100644 --- a/packages/client/ui-chat/src/client/locale.ts +++ b/packages/client/ui-chat/src/client/locale.ts @@ -101,8 +101,11 @@ export const zh = { 'message.turnUsage.cacheWrite': '缓存写入', 'message.turnUsage.output': '输出', 'message.turnUsage.reasoning': '(其中推理 {tokens})', - 'message.turnUsage.total': '总计', '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}秒', 'command.running': '执行中…', @@ -217,8 +220,11 @@ export const en = { 'message.turnUsage.cacheWrite': 'Cache write', 'message.turnUsage.output': 'Output', 'message.turnUsage.reasoning': ' ({tokens} reasoning)', - 'message.turnUsage.total': 'Total', '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': 'Average time to first token (TTFT)', 'duration.seconds': '{seconds}s', 'duration.minutes': '{minutes}m {seconds}s', 'command.running': 'Running…', 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 ef60154e61..301f788590 100644 --- a/packages/client/ui-chat/tests/chat-view.client.spec.tsx +++ b/packages/client/ui-chat/tests/chat-view.client.spec.tsx @@ -1562,19 +1562,71 @@ describe('ChatView', () => { }]]), }) const view = render() - // First-step ttft (1.2s) plus 100 tokens over 5s of decode stay plain - // text; the usage pill in the icon row is the only clickable trigger. + // The usage pill carries the compact total; cache hit stays dialog-only. const trigger = view.getByRole('button', { name: /用量 10\.1K tok/ }) - expect(trigger.textContent).toBe('用量 10.1K tok · 缓存命中 49.4%') - expect(view.getByText(/用时 19秒/).textContent) - .toMatch(/^.+ · 用时 19秒 · 速度 20 tok\/s · 首 token 1\.2秒$/) + expect(trigger.textContent).toBe('用量 10.1K tok') expect(view.queryByRole('dialog')).toBeNull() fireEvent.click(trigger) const dialog = view.getByRole('dialog') expect(dialog.getAttribute('aria-label')).toBe('本轮用量') + expect(dialog.firstChild?.textContent).toBe('本轮用量10,100 tok') expect(dialog.textContent).toContain('缓存命中49.4%') expect(dialog.textContent).toContain('未缓存输入5,060 tok') - expect(dialog.textContent).toContain('总计10,100 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秒') + }) + + it('TEMPORARY: ?usage-variant=flat renders the whole meta line as the dialog trigger', () => { + window.history.replaceState(null, '', '?usage-variant=flat') + try { + const first: AssistantMessageNode = { + kind: 'assistant', seq: 2, time: 2_000, turn: 1, step: 1, blocks: [{ kind: 'text', text: 'draft' }], + timing: { stepStartTime: 1_000, firstTokenTime: 2_200, completedTime: 5_200 }, + usage: { outputTokens: 40 }, + } + const second: AssistantMessageNode = { + kind: 'assistant', seq: 16, time: 16_000, turn: 1, step: 2, blocks: [{ kind: 'text', text: 'final' }], + timing: { stepStartTime: 10_000, firstTokenTime: 10_200, completedTime: 12_200 }, + usage: { outputTokens: 60 }, + } + const h = makeHarness({ + nodes: [user(1, 'hi'), first, second], + turnTimings: new Map([[1, { startTime: 1_000, endTime: 20_000 }]]), + turnEnds: new Map([[1, 20]]), + turnUsages: new Map([[1, { + uncachedInputTokens: 5_060, + cacheReadTokens: 4_940, + outputTokens: 100, + totalTokens: 10_100, + }]]), + }) + const view = render() + const trigger = view.getByRole('button', { name: /用量 10\.1K tok/ }) + // Separator dots live in spaced spans, so textContent carries bare `·`. + expect(trigger.textContent) + .toMatch(/^.+·用时 19秒·用量 10\.1K tok·缓存命中 49\.4%·速度 20 tok\/s·首 token 1\.2秒$/) + // The flat trigger absorbs the meta line, so no plain copy remains. + expect(view.getByText(/用时 19秒/)).toBe(trigger) + fireEvent.click(trigger) + const dialog = view.getByRole('dialog') + expect(dialog.firstChild?.textContent).toBe('本轮用量10,100 tok') + expect(dialog.textContent).toContain('本轮用时和速度') + expect(dialog.textContent).toContain('本轮总用时19秒') + expect(dialog.textContent).toContain('输出速度(TPS)20 tok/s') + expect(dialog.textContent).toContain('首 token 平均用时(TTFT)1.2秒') + } finally { + window.history.replaceState(null, '', window.location.pathname) + } }) it('withholds the usage-details trigger when turn usage is outside the window', () => { @@ -1589,9 +1641,9 @@ describe('ChatView', () => { turnEnds: new Map([[1, 20]]), }) const view = render() - // Timing facts stay visible as plain text, but with no usage in the - // window there is no pill to click. - expect(view.getByText(/首 token/)).toBeTruthy() + // 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() }) 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 e97b88af10..596005c844 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 { TurnUsagePanel } from '../src/client/chat/TurnUsagePanel.tsx' +import { TurnTimePanel, TurnUsagePanel } from '../src/client/chat/TurnUsagePanel.tsx' import type { TurnTokenUsage } from '../src/client/contract/chat-nodes.ts' import { en } from '../src/client/locale.ts' @@ -26,7 +26,7 @@ describe('TurnUsagePanel', () => { const view = render() const trigger = view.getByRole('button') - expect(trigger.textContent).toBe('Usage 15.8K tok · Cache hit 49.4%') + expect(trigger.textContent).toBe('Usage 15.8K tok') expect(trigger.querySelector('svg')).not.toBeNull() expect(trigger.getAttribute('aria-haspopup')).toBe('dialog') expect(trigger.getAttribute('aria-expanded')).toBe('false') @@ -36,9 +36,9 @@ describe('TurnUsagePanel', () => { expect(trigger.getAttribute('aria-expanded')).toBe('true') const dialog = view.getByRole('dialog') expect(dialog.getAttribute('aria-label')).toBe('Turn usage') - // Portaled out of the trigger's row, with a visible heading above the rows. + // Portaled out of the trigger's row, with a heading row carrying the total. expect(dialog.parentElement).toBe(document.body) - expect(dialog.firstChild?.textContent).toBe('Turn usage') + expect(dialog.firstChild?.textContent).toBe('Turn usage15,800 tok') const details = dialog.querySelector('[data-turn-usage-details]') as HTMLElement expect(details).toBeTruthy() expect(details.textContent).toContain('Provider / modeldeepseek/deepseek-chat') @@ -47,7 +47,7 @@ describe('TurnUsagePanel', () => { expect(details.textContent).toContain('Cached input4,940 tok') expect(details.textContent).toContain('Cache write0 tok') expect(details.textContent).toContain('Output5,800 tok (42 tok reasoning)') - expect(details.textContent).toContain('Total15,800 tok') + expect(details.textContent).not.toContain('Total') }) it('omits unavailable optional facts instead of inventing values', () => { @@ -77,9 +77,9 @@ describe('TurnUsagePanel', () => { } const view = render() const trigger = view.getByRole('button') - // The pill carries the compact total and cache-hit rate; exact token + // The pill carries the compact total; cache-hit rate and exact token // counts stay in the dialog. - expect(trigger.textContent).toBe('Usage 1.1K tok · Cache hit 99.9%') + expect(trigger.textContent).toBe('Usage 1.1K tok') fireEvent.click(trigger) const dialog = view.getByRole('dialog') @@ -95,4 +95,88 @@ describe('TurnUsagePanel', () => { fireEvent.pointerDown(document.body) expect(view.queryByRole('dialog')).toBeNull() }) + + it('TEMPORARY flat variant: the whole meta line triggers a dialog with usage and time sections', () => { + const usage: TurnTokenUsage = { + uncachedInputTokens: 5_060, + cacheReadTokens: 4_940, + outputTokens: 5_800, + totalTokens: 15_800, + } + const view = render( + , + ) + const trigger = view.getByRole('button') + // Separator dots live in spaced spans, so textContent carries bare `·`. + expect(trigger.textContent) + .toMatch(/^.+·Ran for 19s·Usage 15\.8K tok·Cache hit 49\.4%·20 tok\/s·TTFT 1\.2s$/) + expect(trigger.querySelector('svg')).toBeNull() + + fireEvent.click(trigger) + const dialog = view.getByRole('dialog') + expect(dialog.firstChild?.textContent).toBe('Turn usage15,800 tok') + expect(dialog.textContent).toContain('Turn time and speed') + const timeDetails = dialog.querySelector('[data-turn-time-details]') as HTMLElement + expect(timeDetails.textContent).toContain('Total run time19s') + expect(timeDetails.textContent).toContain('Tokens per second (TPS)20 tok/s') + expect(timeDetails.textContent).toContain('Average time to first token (TTFT)1.2s') + }) + + it('TEMPORARY flat variant: absent time facts leave no Turn-time section', () => { + const usage: TurnTokenUsage = { + uncachedInputTokens: 120, + outputTokens: 30, + totalTokens: 150, + } + const view = render() + const trigger = view.getByRole('button') + expect(trigger.textContent).toBe('Usage 150 tok') + fireEvent.click(trigger) + const dialog = view.getByRole('dialog') + expect(dialog.textContent).not.toContain('Turn time') + expect(dialog.querySelector('[data-turn-time-details]')).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('Average 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/client/ui-primitives/src/icons/index.tsx b/packages/client/ui-primitives/src/icons/index.tsx index 6f0a913f68..34bd5a8e13 100644 --- a/packages/client/ui-primitives/src/icons/index.tsx +++ b/packages/client/ui-primitives/src/icons/index.tsx @@ -761,6 +761,23 @@ export const IconDataOutline16 = ({ size = 16, className }: IconProps) => ( ) +/** IconDataOutline16 without its gear: a three-tier database cylinder. */ +export const IconDatabaseOutline16 = ({ size = 16, className }: IconProps) => ( + + + + + +) + +/** Thin-stroke clock: outlined dial with square-cut hour and minute hands. */ +export const IconClockOutline16 = ({ size = 16, className }: IconProps) => ( + + + + +) + /** ic_send_outline_14 (figma extract): thin-stroke upward send arrow. */ export const IconSendOutline14 = ({ size = 14, className }: IconProps) => ( diff --git a/snapshots/web/code-mode-round/ui.expected.md b/snapshots/web/code-mode-round/ui.expected.md index 7d470fc968..6e590dabd7 100644 --- a/snapshots/web/code-mode-round/ui.expected.md +++ b/snapshots/web/code-mode-round/ui.expected.md @@ -49,7 +49,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/cordis-tool-round/ui.expected.md b/snapshots/web/cordis-tool-round/ui.expected.md index f58131af8c..78605cc160 100644 --- a/snapshots/web/cordis-tool-round/ui.expected.md +++ b/snapshots/web/cordis-tool-round/ui.expected.md @@ -72,7 +72,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - button "Thought for a while": - text: Thought for a while - img @@ -85,7 +88,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} Use only Cordis tools. Call cordis_stop with pluginId "snap-1". After it succeeds, reply exactly CORDIS_UI_DONE and stop. {{clock}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} Use only Cordis tools. Call cordis_stop with pluginId "snap-1". After it succeeds, reply exactly CORDIS_UI_DONE and stop. {{clock}} - button "Copy": - img - button "1 tool call" [expanded]: @@ -104,7 +110,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/feedback-command/ack-expanded.expected.md b/snapshots/web/feedback-command/ack-expanded.expected.md index 6a97b82d8b..5b20a1e993 100644 --- a/snapshots/web/feedback-command/ack-expanded.expected.md +++ b/snapshots/web/feedback-command/ack-expanded.expected.md @@ -36,7 +36,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is enabled."': - img - img diff --git a/snapshots/web/feedback-command/ack.expected.md b/snapshots/web/feedback-command/ack.expected.md index 11f347f1d1..89f5b6f42d 100644 --- a/snapshots/web/feedback-command/ack.expected.md +++ b/snapshots/web/feedback-command/ack.expected.md @@ -28,7 +28,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is enabled."': - img - img diff --git a/snapshots/web/feedback-release/ack-expanded.expected.md b/snapshots/web/feedback-release/ack-expanded.expected.md index c992c0db80..5b753960a5 100644 --- a/snapshots/web/feedback-release/ack-expanded.expected.md +++ b/snapshots/web/feedback-release/ack-expanded.expected.md @@ -36,7 +36,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is feedback-gated; recording feedback uploads the session records not yet shared."': - img - img diff --git a/snapshots/web/feedback-release/ack.expected.md b/snapshots/web/feedback-release/ack.expected.md index b6c5cf91bc..057d13cd9c 100644 --- a/snapshots/web/feedback-release/ack.expected.md +++ b/snapshots/web/feedback-release/ack.expected.md @@ -28,7 +28,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is feedback-gated; recording feedback uploads the session records not yet shared."': - img - img diff --git a/snapshots/web/fresh-round-trip/ui-expanded.expected.md b/snapshots/web/fresh-round-trip/ui-expanded.expected.md index 1b72b70fe2..108ee4050c 100644 --- a/snapshots/web/fresh-round-trip/ui-expanded.expected.md +++ b/snapshots/web/fresh-round-trip/ui-expanded.expected.md @@ -44,7 +44,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/fresh-round-trip/ui.expected.md b/snapshots/web/fresh-round-trip/ui.expected.md index 49dc055318..9a83925d23 100644 --- a/snapshots/web/fresh-round-trip/ui.expected.md +++ b/snapshots/web/fresh-round-trip/ui.expected.md @@ -28,7 +28,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img 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 79e0af28fb..700f499256 100644 --- a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md +++ b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md @@ -97,7 +97,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - button "System prompt": - img - img @@ -217,7 +220,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - button "Back to bottom": - img - 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 32ac5cc62e..fd874dc327 100644 --- a/snapshots/web/goal-multi-turn-actions/ui.expected.md +++ b/snapshots/web/goal-multi-turn-actions/ui.expected.md @@ -42,7 +42,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - button "System prompt": - img - img @@ -107,7 +110,10 @@ - button "Branch into a new conversation": - img - tooltip "Branch into a new conversation" -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md index 9a45f772a8..00d271cff3 100644 --- a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md +++ b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md @@ -36,7 +36,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/lifecycle-chrome/reloaded.expected.md b/snapshots/web/lifecycle-chrome/reloaded.expected.md index 358fb187cc..bc5558ab03 100644 --- a/snapshots/web/lifecycle-chrome/reloaded.expected.md +++ b/snapshots/web/lifecycle-chrome/reloaded.expected.md @@ -28,7 +28,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/live-interactions/cancel-expanded.expected.md b/snapshots/web/live-interactions/cancel-expanded.expected.md index c64e864ff8..b61cd9e51a 100644 --- a/snapshots/web/live-interactions/cancel-expanded.expected.md +++ b/snapshots/web/live-interactions/cancel-expanded.expected.md @@ -33,7 +33,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/live-interactions/cancel.expected.md b/snapshots/web/live-interactions/cancel.expected.md index c54a4aee60..4dbfb09b4a 100644 --- a/snapshots/web/live-interactions/cancel.expected.md +++ b/snapshots/web/live-interactions/cancel.expected.md @@ -29,7 +29,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/live-interactions/retry-expanded.expected.md b/snapshots/web/live-interactions/retry-expanded.expected.md index c341b22062..1c6a87f6d1 100644 --- a/snapshots/web/live-interactions/retry-expanded.expected.md +++ b/snapshots/web/live-interactions/retry-expanded.expected.md @@ -38,7 +38,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/live-interactions/retry.expected.md b/snapshots/web/live-interactions/retry.expected.md index 695af671e3..93107fe00b 100644 --- a/snapshots/web/live-interactions/retry.expected.md +++ b/snapshots/web/live-interactions/retry.expected.md @@ -28,7 +28,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/message-actions/ui.expected.md b/snapshots/web/message-actions/ui.expected.md index 826509803f..53bfe16b3b 100644 --- a/snapshots/web/message-actions/ui.expected.md +++ b/snapshots/web/message-actions/ui.expected.md @@ -31,7 +31,11 @@ - img - button "Branch into a new conversation" [disabled]: - img -- text: Available only on the last message of a completed turn 7/25 {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- text: Available only on the last message of a completed turn +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} - button "Read a.txt": - img - img @@ -58,7 +62,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/minimal-preset/ui.expected.md b/snapshots/web/minimal-preset/ui.expected.md index f470df2a28..e26d5cdb11 100644 --- a/snapshots/web/minimal-preset/ui.expected.md +++ b/snapshots/web/minimal-preset/ui.expected.md @@ -33,7 +33,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/plan-review/approved-expanded.expected.md b/snapshots/web/plan-review/approved-expanded.expected.md index fa0facf822..0233cfa370 100644 --- a/snapshots/web/plan-review/approved-expanded.expected.md +++ b/snapshots/web/plan-review/approved-expanded.expected.md @@ -54,7 +54,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/plan-review/approved.expected.md b/snapshots/web/plan-review/approved.expected.md index 8135c83a0f..14ba8a416e 100644 --- a/snapshots/web/plan-review/approved.expected.md +++ b/snapshots/web/plan-review/approved.expected.md @@ -34,7 +34,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/question-composer/answered-expanded.expected.md b/snapshots/web/question-composer/answered-expanded.expected.md index 7c0a05c6c7..b2f7ba6294 100644 --- a/snapshots/web/question-composer/answered-expanded.expected.md +++ b/snapshots/web/question-composer/answered-expanded.expected.md @@ -46,7 +46,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/question-composer/answered.expected.md b/snapshots/web/question-composer/answered.expected.md index 252ca47499..a6dc5b28fd 100644 --- a/snapshots/web/question-composer/answered.expected.md +++ b/snapshots/web/question-composer/answered.expected.md @@ -28,7 +28,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/queue-actions/preserved-expanded.expected.md b/snapshots/web/queue-actions/preserved-expanded.expected.md index d92f39fc8a..5a5143fc2c 100644 --- a/snapshots/web/queue-actions/preserved-expanded.expected.md +++ b/snapshots/web/queue-actions/preserved-expanded.expected.md @@ -33,7 +33,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - button "2 queued messages" [expanded] - list: - listitem: diff --git a/snapshots/web/queue-actions/preserved.expected.md b/snapshots/web/queue-actions/preserved.expected.md index 66769e0488..9798e5da4f 100644 --- a/snapshots/web/queue-actions/preserved.expected.md +++ b/snapshots/web/queue-actions/preserved.expected.md @@ -29,7 +29,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - button "2 queued messages" [expanded] - list: - listitem: diff --git a/snapshots/web/seeded-history/command-row.expected.md b/snapshots/web/seeded-history/command-row.expected.md index 8f6b5b5e95..3f4ed25eb3 100644 --- a/snapshots/web/seeded-history/command-row.expected.md +++ b/snapshots/web/seeded-history/command-row.expected.md @@ -47,7 +47,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} - button "compact Compacted 5 history items (~{{tokens}} tokens)" - button "Context injection AGENTS.md": - img diff --git a/snapshots/web/seeded-history/feedback-row.expected.md b/snapshots/web/seeded-history/feedback-row.expected.md index 17eb33963d..1265a3e50e 100644 --- a/snapshots/web/seeded-history/feedback-row.expected.md +++ b/snapshots/web/seeded-history/feedback-row.expected.md @@ -47,7 +47,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} - button "compact Compacted 5 history items (~{{tokens}} tokens)" - button "Context injection AGENTS.md": - img diff --git a/snapshots/web/seeded-history/ui-expanded.expected.md b/snapshots/web/seeded-history/ui-expanded.expected.md index 28e2a90ec2..2d18d8ec62 100644 --- a/snapshots/web/seeded-history/ui-expanded.expected.md +++ b/snapshots/web/seeded-history/ui-expanded.expected.md @@ -47,7 +47,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} - button "compact Compacted 5 history items (~{{tokens}} tokens)" - button "Context injection AGENTS.md": - img diff --git a/snapshots/web/seeded-history/ui.expected.md b/snapshots/web/seeded-history/ui.expected.md index 5167001259..4711ff7304 100644 --- a/snapshots/web/seeded-history/ui.expected.md +++ b/snapshots/web/seeded-history/ui.expected.md @@ -29,7 +29,10 @@ - img - button "Branch into a new conversation": - img -- text: 7/25 {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: 7/25 {{clock}} - button "compact Compacted 5 history items (~{{tokens}} tokens)" - button "Context injection AGENTS.md": - img diff --git a/snapshots/web/skill-tool-row/ui.expected.md b/snapshots/web/skill-tool-row/ui.expected.md index 21e4c5c810..abf15517da 100644 --- a/snapshots/web/skill-tool-row/ui.expected.md +++ b/snapshots/web/skill-tool-row/ui.expected.md @@ -47,7 +47,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{date}} {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/steering/settled-expanded.expected.md b/snapshots/web/steering/settled-expanded.expected.md index f7a0b9b006..bfd48e7ed9 100644 --- a/snapshots/web/steering/settled-expanded.expected.md +++ b/snapshots/web/steering/settled-expanded.expected.md @@ -39,7 +39,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/steering/settled.expected.md b/snapshots/web/steering/settled.expected.md index 62ed474ccf..0d190d89e9 100644 --- a/snapshots/web/steering/settled.expected.md +++ b/snapshots/web/steering/settled.expected.md @@ -31,7 +31,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/subagent-conversation/ui-expanded.expected.md b/snapshots/web/subagent-conversation/ui-expanded.expected.md index ac152fe9ed..6a275ca53f 100644 --- a/snapshots/web/subagent-conversation/ui-expanded.expected.md +++ b/snapshots/web/subagent-conversation/ui-expanded.expected.md @@ -44,7 +44,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - button "System prompt": - img - img @@ -68,7 +71,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/subagent-conversation/ui.expected.md b/snapshots/web/subagent-conversation/ui.expected.md index 7212aa8d18..04d2e7885c 100644 --- a/snapshots/web/subagent-conversation/ui.expected.md +++ b/snapshots/web/subagent-conversation/ui.expected.md @@ -36,7 +36,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - button "System prompt": - img - img @@ -56,7 +59,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/turn-tail-actions/completed.expected.md b/snapshots/web/turn-tail-actions/completed.expected.md index c7dd77a4a3..637055a274 100644 --- a/snapshots/web/turn-tail-actions/completed.expected.md +++ b/snapshots/web/turn-tail-actions/completed.expected.md @@ -28,10 +28,13 @@ - img - button "Branch into a new conversation": - img -- button "Usage 15.8K tok · Cache hit 49.7%": +- button "Usage 15.8K tok": - img - - text: Usage 15.8K tok · Cache hit 49.7% -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} + - text: Usage 15.8K tok +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/turn-tail-actions/focused.expected.md b/snapshots/web/turn-tail-actions/focused.expected.md index f8964fa6d7..94870d19c8 100644 --- a/snapshots/web/turn-tail-actions/focused.expected.md +++ b/snapshots/web/turn-tail-actions/focused.expected.md @@ -41,10 +41,13 @@ - img - button "Branch into a new conversation": - img -- button "Usage 15.8K tok · Cache hit 49.7%": +- button "Usage 15.8K tok": - img - - text: Usage 15.8K tok · Cache hit 49.7% -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} + - text: Usage 15.8K tok +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/turn-tail-actions/settled.expected.md b/snapshots/web/turn-tail-actions/settled.expected.md index 90d71c1a86..26bd7a6d83 100644 --- a/snapshots/web/turn-tail-actions/settled.expected.md +++ b/snapshots/web/turn-tail-actions/settled.expected.md @@ -30,7 +30,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/turn-tail-actions/usage-expanded.expected.md b/snapshots/web/turn-tail-actions/usage-expanded.expected.md index 466d21613a..43d984fc53 100644 --- a/snapshots/web/turn-tail-actions/usage-expanded.expected.md +++ b/snapshots/web/turn-tail-actions/usage-expanded.expected.md @@ -28,10 +28,13 @@ - img - button "Branch into a new conversation": - img -- button "Usage 15.8K tok · Cache hit 49.7%" [expanded]: +- button "Usage 15.8K tok" [expanded]: - img - - text: Usage 15.8K tok · Cache hit 49.7% -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} + - text: Usage 15.8K tok +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/web-search-round/ui.expected.md b/snapshots/web/web-search-round/ui.expected.md index 5ceba38e31..0968ee2b75 100644 --- a/snapshots/web/web-search-round/ui.expected.md +++ b/snapshots/web/web-search-round/ui.expected.md @@ -36,7 +36,10 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}} - textbox "Message or run a task... / commands, @ files or sessions" - button "Commands": - img diff --git a/snapshots/web/workflow-run/ui.expected.md b/snapshots/web/workflow-run/ui.expected.md index 7f47cb6d63..4859403351 100644 --- a/snapshots/web/workflow-run/ui.expected.md +++ b/snapshots/web/workflow-run/ui.expected.md @@ -36,4 +36,7 @@ - img - button "Branch into a new conversation": - img -- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}} +- button "Ran for {{duration}}": + - img + - text: Ran for {{duration}} +- text: {{clock}}