fix(web): label the Turn-time TTFT row as first-token latency, not an average

The Turn fold publishes the first step's TTFT (contract/turn-metrics.ts
firstStepTtftMs), never an average; only the Session StatsLine averages
across steps. Rename the dialog row in both locales and align the Agent
Note with what actually ships.
This commit is contained in:
Yif committed 2026-08-28 15:03:47 +08:00
1 parent 1278877d91
commit 21d039be1b
7 files changed
+9 -9

No files matched your search

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-28-web-turn-stat-pills.md
2026-08-28-web-turn-stat-pills.md: cfba7db77507f88bdf1f03b80d62751830e5122d
2026-08-28-web-turn-stat-pills.zh.md: 05e5f193f246b3245c2ce169547950326fb7557f
2026-08-28-web-turn-stat-pills.md: 2661c228b9a5a7b55ba3698cf3ee2b4c76701bbc
2026-08-28-web-turn-stat-pills.zh.md: 5cc889b9f743eefeac1343b70743c23392efc908
@@ -10,7 +10,7 @@ A completed assistant Turn ended with two stacked footer rows: a `Turn usage` Di
## Decision
The tail keeps one `MessageIconActions` row. Two stat pills sit right of the branch action: a database pill labelled with the compact Turn total (`Usage 15.8K tok`) and a clock pill labelled with the wall time (`Ran for 19s`); the message clock stays plain text at the row end. Each pill is an `aria-haspopup="dialog"` trigger that portals a fixed-position dialog to `document.body`, placed above the trigger by `useAnchoredPosition` with a 12px viewport clamp and closed by outside pointerdown or Escape (ContextMeter's pattern). The usage dialog holds the exact total, provider/model routes, cache-hit rate, token buckets, and the reasoning subset inline in Output; the time dialog holds total run time, decode TPS, and average TTFT. Facts absent from the fold render no row, and a window without publishable Turn usage renders no usage pill; the token-meter fold and `turn/start` gating are unchanged from [exact per-Turn usage](2026-08-24-web-per-turn-token-usage.md).
The tail keeps one `MessageIconActions` row. Two stat pills sit right of the branch action: a database pill labelled with the compact Turn total (`Usage 15.8K tok`) and a clock pill labelled with the wall time (`Ran for 19s`); the message clock stays plain text at the row end. Each pill is an `aria-haspopup="dialog"` trigger that portals a fixed-position dialog to `document.body`, placed above the trigger by `useAnchoredPosition` with a 12px viewport clamp and closed by outside pointerdown or Escape (ContextMeter's pattern). The usage dialog holds the exact total, provider/model routes, cache-hit rate, token buckets, and the reasoning subset inline in Output; the time dialog holds total run time, decode TPS, and the Turn's first-token latency (the first step's TTFT). Facts absent from the fold render no row, and a window without publishable Turn usage renders no usage pill; the token-meter fold and `turn/start` gating are unchanged from [exact per-Turn usage](2026-08-24-web-per-turn-token-usage.md).
Row visibility follows recency: turn tails and user rows tag `data-actions-reveal`, the latest of each kind stays `always` visible, earlier rows reveal on hover or focus-within under `@media (hover: hover)`, and no-hover devices keep every row visible. Below 480px the pill labels hide and each pill takes the sibling action-button geometry — 28px width, 6px padding, centered glyph, and no adjacent-pill margin rebate — so the bare icons keep the row's 8px rhythm.
@@ -10,7 +10,7 @@
## 决定
尾部只保留一行 `MessageIconActions`。分叉操作右侧放两个统计 pill:数据库图标 pill 标注紧凑的本轮总量(`用量 15.8K tok`),时钟图标 pill 标注墙钟用时(`用时 19秒`);消息时钟保持纯文字置于行尾。每个 pill 是 `aria-haspopup="dialog"` 触发器,把固定定位的弹层 portal 到 `document.body`,由 `useAnchoredPosition` 锚定在触发器上方并保持 12px 视口边距,外部 pointerdown 或 Escape 关闭(沿用 ContextMeter 模式)。用量弹层承载精确总量、提供方/模型路由、缓存命中率、token 分桶及输出内联的推理子集;用时弹层承载本轮总用时、解码 TPS、首 token 平均用时。fold 未产出的事实不渲染行,窗口内无可发布的 Turn 用量则不渲染用量 pill;token-meter fold 与 `turn/start` 门控沿用[精确 per-Turn 用量](2026-08-24-web-per-turn-token-usage.zh.md),未做改动。
尾部只保留一行 `MessageIconActions`。分叉操作右侧放两个统计 pill:数据库图标 pill 标注紧凑的本轮总量(`用量 15.8K tok`),时钟图标 pill 标注墙钟用时(`用时 19秒`);消息时钟保持纯文字置于行尾。每个 pill 是 `aria-haspopup="dialog"` 触发器,把固定定位的弹层 portal 到 `document.body`,由 `useAnchoredPosition` 锚定在触发器上方并保持 12px 视口边距,外部 pointerdown 或 Escape 关闭(沿用 ContextMeter 模式)。用量弹层承载精确总量、提供方/模型路由、缓存命中率、token 分桶及输出内联的推理子集;用时弹层承载本轮总用时、解码 TPS、本轮首 token 用时(取首个 step 的 TTFT)。fold 未产出的事实不渲染行,窗口内无可发布的 Turn 用量则不渲染用量 pill;token-meter fold 与 `turn/start` 门控沿用[精确 per-Turn 用量](2026-08-24-web-per-turn-token-usage.zh.md),未做改动。
行可见性按新近度门控:turn 尾行与用户行标记 `data-actions-reveal`,各自最新一行保持 `always` 常显,更早的行在 `@media (hover: hover)` 下 hover 或 focus-within 才显示,无 hover 设备恒显示。480px 以下 pill 隐藏文字并取同排操作按钮的几何——28px 宽、6px 内边距、图标居中、取消相邻 pill 的边距补偿——让裸图标保持行的 8px 节奏。
+1 -1
View File
@@ -198,7 +198,7 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
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)
expect(await timeDialog.getByText('Time to first token (TTFT)', { exact: true }).count()).toBe(1)
await page.keyboard.press('Escape')
await trigger.click()
+2 -2
View File
@@ -102,7 +102,7 @@ export const zh = {
'message.turnTime.title': '本轮用时和速度',
'message.turnTime.duration': '本轮总用时',
'message.turnTime.speed': '输出速度(TPS)',
'message.turnTime.ttft': '首 token 平均用时(TTFT)',
'message.turnTime.ttft': '首 token 用时(TTFT)',
'duration.seconds': '{seconds}秒',
'duration.minutes': '{minutes}分{seconds}秒',
'command.running': '执行中…',
@@ -218,7 +218,7 @@ export const en = {
'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)',
'message.turnTime.ttft': 'Time to first token (TTFT)',
'duration.seconds': '{seconds}s',
'duration.minutes': '{minutes}m {seconds}s',
'command.running': 'Running…',
@@ -1583,7 +1583,7 @@ describe('ChatView', () => {
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(timeDialog.textContent).toContain('首 token 用时(TTFT)1.2秒')
})
it('withholds the usage-details trigger when turn usage is outside the window', () => {
@@ -116,7 +116,7 @@ describe('TurnTimePanel', () => {
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')
expect(details.textContent).toContain('Time to first token (TTFT)1.2s')
fireEvent.keyDown(document, { key: 'Escape' })
expect(view.queryByRole('dialog')).toBeNull()