Web turn run time and hover-revealed time chrome
Web 轮次运行时长与悬停显示的时间附属元素
The Web chat shows when a message arrived but not how long the agent worked on it. Long turns give no live progress signal beyond the static activity label, and after the turn settles the wall time is not recoverable from the UI. Meanwhile the always-visible clock row adds visual noise to every message.
English
Problem
The Web chat shows when a message arrived but not how long the agent worked on it. Long turns give no live progress signal beyond the static activity label, and after the turn settles the wall time is not recoverable from the UI. Meanwhile the always-visible clock row adds visual noise to every message.
Decision
Turn wall time uses the existing logged turn/start and turn/end timestamps, with no new session events. The client Session folds each in-window pair into turnTimings; the actions-owning assistant footer renders endTime - startTime as a localized Ran for {duration} label after the turn ends. The running TurnStatus clock uses the latest timing without an end, so reload preserves elapsed time, steering does not reset it, and a retry starts from its own logged boundary. Both readings use the same localized formatter and whole-second floor. The clock appears only after 15 seconds and is hidden from the live region so screen readers announce the activity status without replaying every tick.
Action chrome is recency-gated on hover-capable devices: the latest user-authored row and latest Turn tail remain visible, while each earlier row fades the complete actions line in on :hover or :focus-within. Turn tails publish an explicit recency attribute. User and steering rows use a CSS following-sibling selector over their existing flow-kind attributes, so no mounted message subscribes to and reverse-scans the aggregate Chat snapshot. Touch devices keep every action line visible, and opacity preserves layout.
Alternatives considered
Deriving timing from message nodes. The nearest user or steering timestamp is available in the rendered transcript, but it mismeasures retry turns and lets mid-turn steering reset the live clock. Existing turn boundary events provide the authoritative timestamps without changing the log format.
Anchoring the live clock to component mount. Simpler, but a mid-turn reload would restart the clock at zero and disagree with the eventual footer label. Mount time remains only the fallback when turn/start is outside the loaded window.
Compute the latest user-authored row inside every message renderer. Rejected because each mounted row would subscribe to the aggregate Chat snapshot and reverse-scan its order whenever any Chat value changed. The flow already expresses row order and kind in the DOM, so CSS owns this visual recency rule.
Consequences
Turn duration is visible live and after settlement without new session events, and both readings share exact log boundaries and formatting. The settled duration includes activity after the last assistant text up to turn/end; the label is absent when turn/start is outside the loaded window. Earlier action rows do not compete with message content at rest, their hidden opacity still reserves layout, and the ticking clock remains visual rather than repeatedly announced.
中文
问题
Web 聊天界面会显示消息的到达时间,却不显示 agent(智能体)处理这条消息花了多久。长轮次除静态活动标签外没有任何实时进度信号,轮次结束后也无法从 UI 中还原实际耗时。与此同时,始终可见的时钟行给每条消息都增加了视觉噪音。
决策
轮次实际耗时(wall time)采用日志中已有的 turn/start 和 turn/end 时间戳,不新增任何会话事件。客户端 Session 将加载窗口内的每对边界归并到 turnTimings 中;轮次结束后,承载操作图标的 assistant 页脚把 endTime - startTime 渲染为本地化的 Ran for {duration} 标签。运行中的 TurnStatus 时钟采用最新一条没有结束时间的计时记录,因此重新加载会保留已用时长,steering(中途引导)不会重置计时,重试也从自身的日志边界开始。两处读数共用同一个本地化格式化器,并向下取整到整秒。该时钟在 15 秒后才出现,并从实时区域中隐藏,因此屏幕阅读器会播报活动状态而不会重复播报每次时钟跳动。
在支持 hover 的设备上,操作附属元素按新近程度显示:最新的用户消息行与最新 Turn 尾部保持可见,每个更早的行只在 :hover 或 :focus-within 时淡入完整操作行。Turn 尾部发布显式的新近属性;用户与 steering 行通过既有 flow-kind 属性上的 CSS 后继兄弟选择器判定,因此每个已挂载消息都无需订阅并反向扫描聚合 Chat snapshot。触屏设备保持每条操作行可见,opacity 则保持布局稳定。
考虑过的替代方案
从消息节点推导计时。 渲染后的 transcript(文本记录)中可以取得最近的用户或 steering 时间戳,但这会错误计算重试轮次,并让轮次中途的 steering 重置实时时钟。已有的轮次边界事件无需改变日志格式即可提供权威时间戳。
将实时时钟锚定到组件挂载时刻。 更简单,但轮次进行中重新加载会让时钟从零重新计时,并与最终的页脚标签不一致。仅当 turn/start 位于已加载窗口之外时,才回退到挂载时刻。
在每个消息 renderer 内计算最新的用户消息行。 不采用:每个已挂载行都会订阅聚合 Chat snapshot,并在任意 Chat 值变化时反向扫描 order。现有 DOM flow 已表达行顺序和 kind,因此该视觉新近规则归 CSS 所有。
后果
轮次时长在运行中和结束后都可见,且不需要新的会话事件;两处读数共用精确的日志边界和格式化方式。结束后的时长包括最后一条 assistant 文本之后、直至 turn/end 的活动;若 turn/start 位于已加载窗口之外,则不显示标签。较早的操作行在未交互时不与消息内容争夺注意力,透明隐藏仍保留布局,持续跳动的时钟也只保留视觉呈现,不会被重复播报。