DSH / Atlas
2026-07-27implementedbug-fixarchived 2026-08-04

TUI step timing trails the step's last message

TUI 步骤计时跟在该步骤最后一条消息之后

The per-step timing summary (`Model wait … · Completed …`) was a child of the assistant message component, so it rendered directly under the assistant text. When a step drove tool calls, the tool cards were appended to the chat *after* the assistant message, leaving the timing line stranded above them — one message before the step's actual last output. The summary is meant to close a step, so on any tool-calling step

English

Problem

The per-step timing summary (Model wait … · Completed …) was a child of the assistant message component, so it rendered directly under the assistant text. When a step drove tool calls, the tool cards were appended to the chat after the assistant message, leaving the timing line stranded above them — one message before the step's actual last output. The summary is meant to close a step, so on any tool-calling step it appeared in the wrong place.

Decision

The timing summary is its own StepTimingComponent, no longer a child of AssistantMessageComponent. StreamingAssistantComponent owns one and exposes it as timing, but the renderer attaches it to the chat as a sibling that follows the assistant message. Whenever a tool/call or tool/result of the open step appends a card, trailStreamingTiming() moves the footer back to the tail of the chat, so it always trails the step's last message. On step/end the footer is completed in place — already at the tail — and stays pinned while the next step's output follows. removeStreaming and the reasoning-toggle rebuild detach and reattach the footer together with its streaming component.

Event ordering makes this exact: within a step the loop appends tool/call and tool/result before step/end, so the footer is repositioned while streaming is still set, then frozen when the step ends.

Alternatives considered

Keep the timing inside the assistant message and reorder tool cards above it. Rejected: tool cards belong after the assistant text that requested them; moving them above the assistant message to sit under the timing would misrepresent the transcript order.

Recompute a single trailing footer for the whole turn instead of one per step. Rejected: a multi-step turn shows each step's own completed timing, and collapsing them would drop the per-step buckets the existing timing tests pin.

Reposition the footer from a step/end-only handler. Rejected: tool cards render before step/end, so a footer moved only at step end would already be trailing but would not track a mid-step re-render, and the running (pre-completion) footer would still sit above the tool cards during streaming.

Consequences

  • On a tool-calling step the timing summary renders below the tool cards, both while the turn runs and after it completes; the package snapshots (untrusted-controls, cordis-tools-pending, advanced-cards-*, code-mode-pending, dynamic-workflow-pending, surface-before-compaction) and the example transcripts (todo-plan, bash-terminal-card, code-mode, parallel-file-reads, dynamic-workflow, cordis-dynamic-toolchain, code-mode-dispatch-spill) pin the new order.
  • A unit test asserts the completed timing appears after a step's tool output; it fails on the pre-fix ordering.

中文

问题

每步的计时摘要(Model wait … · Completed …)原本是助手消息组件的子节点,因此直接渲染在助手文本下方。当某一步触发 tool call 时,tool card(工具卡片)会在助手消息之后追加到聊天区,使计时行被搁在它们上方——落在该步骤真正的最后一条输出之前一条消息处。该摘要本意是收束一个步骤,因此在任何含 tool call 的步骤上都出现在了错误的位置。

决策

计时摘要现在是独立的 StepTimingComponent,不再是 AssistantMessageComponent 的子节点。StreamingAssistantComponent 持有一个并以 timing 暴露它,但渲染器把它作为紧随助手消息之后的同级节点挂到聊天区。每当当前打开步骤的 tool/calltool/result 追加一张卡片,trailStreamingTiming() 就把该页脚移回聊天区末尾,使它始终跟在该步骤的最后一条消息之后。在 step/end 时该页脚就地定稿——此时已在末尾——并在后续步骤的输出接续时保持钉住。removeStreaming 与推理开关重建会把该页脚连同其流式组件一起摘除并重新挂上。

事件顺序让这一点精确成立:在一个步骤内,循环会先追加 tool/calltool/result,再追加 step/end,因此页脚是在 streaming 仍被设置时重新定位的,随后在步骤结束时冻结。

备选方案

把计时保留在助手消息内部,改为把 tool card 排到它上方。 否决:tool card 应位于请求它们的助手文本之后;把它们移到助手消息上方以贴在计时下方,会歪曲 transcript(文本记录)的顺序。

为整个轮次重算一个末尾页脚,而非每步一个。 否决:多步轮次会显示各步自己的完成计时,合并它们会丢掉现有计时测试所固定的每步分桶。

只在 step/end 处理器里重新定位页脚。 否决:tool card 在 step/end 之前渲染,因此仅在步骤结束时移动的页脚虽已处于末尾,却无法跟踪步骤中途的重新渲染,而且流式过程中运行态(完成前)的页脚仍会落在 tool card 上方。

后果

  • 在含 tool call 的步骤上,计时摘要渲染在 tool card 下方,轮次运行期间与完成之后皆如此;相关包快照(untrusted-controlscordis-tools-pendingadvanced-cards-*code-mode-pendingdynamic-workflow-pendingsurface-before-compaction)与示例 transcript(todo-planbash-terminal-cardcode-modeparallel-file-readsdynamic-workflowcordis-dynamic-toolchaincode-mode-dispatch-spill)固定了新顺序。
  • 一个单元测试断言完成计时出现在某步骤的工具输出之后;在修复前的顺序下它会失败。