Blade Agent · Chat UI bug handoff · 2026-06-17

聊天过程中消息列表高度瞬时变小导致滚动丢失

本文是给后续 coder 的说明性 handoff。重点是问题现象、用户观察、相关代码路径、已排除方向和候选根因。不包含验证脚本。

用户观察,必须保留

  1. 高度突然变更和这一轮回答是折叠还是展开没有关系。每次高度突然变更时,从来没有发生过可见折叠;变更前和变更后当前轮次都是展开的,并且仍有智能体不断进行工具调用。
  2. 高度突然变更似乎和上方历史轮次没有关系。历史轮次一直都是折叠的,不应把“历史轮次从展开变折叠”作为主因。
  3. 跳变非常快,视觉上大约 0.01 到 0.05 秒内完成。
  4. 示例会话地址:http://localhost:5927/sol/vibe-coding-v2/sess/2026-0617-0937-sic8

问题现象

在智能体持续响应过程中,聊天列表高度会突然变小,然后后续 token 或工具状态更新又让高度继续增长。

用户原本处于底部或跟随最新消息时,跳变后滚动位置会丢失,表现为列表突然上跳、无法持续跟随底部,或者画面闪烁。

截图里第二帧并不是“这一轮完成后的最终状态”,因为后面仍然有工具调用继续发生。

重要排除项

不是 Unicode 不完整的主因

后端经过 SDK 和 JSON/Socket.IO 传给前端的是字符串。Unicode 边界问题最多影响最后几个字符或 Markdown 局部解析,不太可能让整个消息列表高度大幅坍缩。

不是子智能体主因

用户指出示例会话里没有调用子智能体。不要把问题优先归因到 AgentLoopBlock 的 running/done 切换。

不是历史轮次折叠主因

历史轮次早已折叠,并且跳变前后没有观察到历史轮次展开/折叠变化。

相关代码路径

背景:MessageList 如何渲染 assistant

后端/投影层可能产生多条 root assistant message。前端不会把每条 assistant message 都显示成独立气泡,而是在 MessageList 中把连续的 assistant messages 放进 assistantBuffer,最后统一渲染成一个视觉上的助手回答块。

数据层可能是多条

assistant message 1: thinking + tool
assistant message 2: thinking + tool
assistant message 3: thinking + tool
assistant message 4: final text

UI 上合成一个助手块

AssistantTurnBlock
  message 1
  message 2
  message 3
  message 4

因此,如果这个合并块在流式 patch 中短暂被误判、重建、变 key、或者内容被过滤,用户看到的是整个当前回答区域高度瞬间变化。

候选根因 1:当前 assistant turn 的 active 判定过窄

现有逻辑重点看 message.status === "streaming"。但工具执行阶段可能出现 message 本身已经是 completed,内部 tool_call 仍然是 pending / awaiting_answer。用户视觉上看到的是“当前轮次还在执行工具”,但 UI 可能短暂认为这个 assistant turn 已不处于 streaming。

如果 active 判定短暂变 false,AssistantTurnBlock 可能走非流式路径,导致过程渲染树临时变短。下一次 patch 到来又恢复 active,形成 0.01–0.05 秒闪烁。

建议验证点:

  • 跳变那一帧,最后一个视觉 assistant turn 里是否没有任何 message.status === streaming。
  • 同一帧是否仍有 tool_call.status === pending / awaiting_answer。
  • 如果是,则 active 应该按 tool_call 状态一起判断。

候选根因 2:assistant turn key / 分组边界不稳定

MessageList 的 assistant turn key 使用合并组第一条 assistant message 的 entry_id。如果流式 patch 中合并组第一条 message 变化,或中间插入了非 assistant 类型,React 可能认为这是一个新的 assistant turn,导致旧 DOM 卸载、新 DOM 挂载。这个过程可能造成高度瞬间变化和滚动锚点丢失。

建议验证点:

  • 跳变前后 data-turn-id 是否变化。
  • 跳变前后同一个 assistant block 的 DOM 节点是否被 remove/add。
  • 跳变前后 renderBlocks 数量、assistant block key 是否变化。

候选根因 3:store materialize 出现短暂不完整状态

前端每次收到 turn patch 都会把 TurnProjection 重新转换为 ChatMessage,并重新生成 messages。若 turn:end、turn:patch、history sync、patch flush 的顺序中出现短暂旧状态,当前正在执行的工具消息可能在一帧中丢失或状态回退,导致当前 visual turn 变短。

建议验证点:

  • 在 updateSessionState 后记录 messages.length、最后几条 message 的 entry_id/status/tool_call.status。
  • 确认跳变那一帧是否少了一条正在执行的 assistant message。
  • 确认 chat:end 触发的 history sync 是否在还未真正结束时覆盖了 streaming state。

候选根因 4:滚动库放大了小幅高度变化

如果 DOM 高度只短暂减少几十或几百 px,StickToBottom / 浏览器 scroll anchoring 可能把这个变化放大成明显上跳。这个方向不是“高度为什么变小”的根因,但可能是“为什么滚动进度丢失”的直接原因。

建议验证点:

  • 如果 scrollHeight 下降后又上升,是 DOM 高度问题。
  • 如果 scrollHeight 不变但 scrollTop 突变,是滚动策略问题。
  • 如果用户在底部时 gap 突然变大,说明跟随底部状态丢失。

建议的最小验证方法

不要用截图或 250ms 轮询判断。需要在真实 agent 流式过程中记录以下数据,频率至少 requestAnimationFrame 级别:

不要做的修复

当前仓库状态提醒

当前工作区可能已经包含实验性改动:包括让 AssistantTurnBlock 在 streaming 后保持 detail,以及让 MessageList 用 pending/awaiting tool call 判断 assistant turn active。后续 coder 应先审查这些改动是否符合本文约束,再决定保留、调整或回滚。