DSH / Atlas
2026-08-06implementedsimplification

User and steering bubbles drop the branch action

user 与 steering 气泡移除分支操作

Every user and consumed-steering bubble rendered the branch control under the completed-turn-tail gate of the [completed-turn-tail decision](../bug-fix/2026-08-02-message-fork-actions-require-completed-turn-tail.md). On those bubbles the gate is effectively permanent: a turn-opening user message is followed by its own turn's nodes, and a consumed steering message is mid-turn by construction, so the control could enab

English

Problem

Every user and consumed-steering bubble rendered the branch control under the completed-turn-tail gate of the completed-turn-tail decision. On those bubbles the gate is effectively permanent: a turn-opening user message is followed by its own turn's nodes, and a consumed steering message is mid-turn by construction, so the control could enable only when the turn ended with no node after the message at all — a cancel before the first model event. Readers therefore saw a control that never enables, with a tooltip promising a state the button cannot reach. The affordance also misled when read at all: a fork at a message seq cuts at the containing turn/end, so "branch at my message" includes the answer below it — the opposite of the branch-to-re-ask reading a control on one's own bubble suggests.

Decision

User and steering bubbles render no branch action. MessageItem loses its fork props, PendingSteeringBubble loses its showBranch special case, and messageBranchSeqs narrows to assistantBranchSeqs: only a completed turn's transcript tail that is the turn's own content-text assistant may fork. The branch affordance lives solely under the settled answer.

A turn containing a steer keeps its fork point unchanged: fork is a log-prefix cut at turn/end, and the steer is model-visible history the child must inherit, so the settled answer of a steered turn forks like any other. The assistant-side gate and its visible-but-unavailable presentation are also unchanged — under an answer, unavailable is a transient, reachable state (a trailing tool or error row currently owns the tail), which is exactly what the tooltip is for.

Alternatives considered

Hide the control on message bubbles only while ineligible. Rejected: it preserves the near-unreachable enabled case at the cost of an icon that appears on one's own bubble only when a turn died before producing anything, an inconsistency not worth the case it serves.

Keep the visible-but-unavailable control (status quo). Rejected: the completed-turn-tail decision chose visibility so the tooltip could explain a boundary the reader can reach; on user and steering bubbles the boundary is unreachable in practice, so the explanation props up a control that should not exist there.

Branch-before-the-message semantics on user bubbles. Out of scope: re-asking from one's own prompt needs a cut before the message plus composer prefill, a different Host operation. Removing the current control keeps that seat free for such a feature instead of squatting on it with opposite semantics.

Consequences

The only fork handles are the enabled branch controls under settled answers. A turn cancelled before any node followed its message loses its only handle and has no fork point, matching turns whose tail is a content-free interrupted node. Web aria goldens across apps/web drop the user-bubble disabled-branch row and its hidden explanation text. Package tests pin that user and steering bubbles render no branch control and that a steering-tail turn leaves the narration's control unavailable.

中文

问题

每个 user 气泡和已消费的 steering(中途引导)气泡都渲染分支控件,受已完成轮次尾部决策的门禁约束。在这些气泡上,该门禁实际上是永久性的:开轮的 user 消息后面必然跟着本轮自己的节点,已消费的 steering 消息按构造就处在轮次中间,因此只有当轮次结束时该消息之后一个节点都没有——即在第一个模型事件之前就取消——控件才可能启用。读者因此看到一个永远不会启用的控件,tooltip 许诺的是这个按钮到达不了的状态。这个操作入口本身也有误导:在消息 seq 处 fork 会切在所在轮次的 turn/end,「在我的消息处分支」实际会把下方的回答一并带走,与在自己气泡上看到分支时「分叉重问」的直觉预期恰好相反。

决策

user 与 steering 气泡不再渲染分支操作。MessageItem 移除其 fork props,PendingSteeringBubble 移除其 showBranch 特例,messageBranchSeqs 收窄为 assistantBranchSeqs:只有已完成轮次的 transcript(文本记录)尾部、且该尾部是本轮自己的带 text 内容 assistant 节点才可 fork。分支入口只存在于已定稿的回答之下。

含有 steer 的轮次的 fork 点保持不变:fork 是切在 turn/end 上的日志前缀,steer 是子会话必须继承的模型可见历史,因此被引导过的轮次的已定稿回答与其他轮次一样可以 fork。assistant 侧的门禁及其可见但不可用的呈现也保持不变——在回答之下,不可用是一个短暂且可到达的状态(当前尾部被后续工具行或错误行占据),这正是 tooltip 的用武之地。

考虑过的替代方案

仅在不可用时隐藏消息气泡上的控件。 否决:它保住了那个几乎不可达的启用场景,代价是图标只在轮次尚未产出任何东西就中止时才出现在自己的气泡上,这种不一致不值得为它服务的场景付出。

保留可见但不可用的控件(现状)。 否决:已完成轮次尾部决策选择可见,是为了让 tooltip 解释一个读者可以到达的边界;在 user 与 steering 气泡上这个边界实际不可达,解释文本是在为一个不该存在于此的控件打补丁。

在 user 气泡上采用切在消息之前的分支语义。 不在本次范围内:从自己的提示词重问需要切在消息之前并预填输入框,是另一个 Host 操作。移除当前控件恰好为这样的功能留出位置,而不是让语义相反的控件占着它。

后果

唯一的 fork 入口是已定稿回答下方启用的分支控件。在任何节点跟上其消息之前就被取消的轮次失去了它唯一的入口,从此没有 fork 点,与尾部是无内容 interrupted 节点的轮次一致。apps/web 的 aria golden 全部移除 user 气泡的禁用分支行及其隐藏说明文本。包测试钉住:user 与 steering 气泡不渲染分支控件,steering 作为尾部的轮次让叙述节点的控件保持不可用。