DSH / Atlas
2026-07-28implementedbug-fixarchived 2026-08-07

Web conversation UI polish sweep

Web 对话 UI 视觉优化

A design review of the web GUI's conversation surfaces found a batch of presentation defects: portal menus painted one frame at the wrong position before repositioning (visible open jump), the chat column split one tool run into several groups whenever a step message carried only tool-call heads, tool row summaries printed workspace-absolute paths that consumed most of the row, the running-row sweep was implemented a

English

Problem

A design review of the web GUI's conversation surfaces found a batch of presentation defects: portal menus painted one frame at the wrong position before repositioning (visible open jump), the chat column split one tool run into several groups whenever a step message carried only tool-call heads, tool row summaries printed workspace-absolute paths that consumed most of the row, the running-row sweep was implemented as an alpha mask that dimmed the whole row, the hero workspace chip resurrected a deleted workspace's folder name from the session cwd, and the header showed a turns counter nobody asked for next to a 13px title.

Decision

The sweep lands as presentation-layer changes only; nothing enters the session log.

  • Portal menus pre-render hidden and measure before paint. The menu list mounts with visibility: hidden at (0,0), measures in useLayoutEffect, and becomes visible already at its final position. Menus keep 12px viewport clearance with internal scroll; workspace create actions pin in a non-scrolling footer.
  • The chat flow skips assistant nodes that render nothing. A finalized assistant node whose blocks are only tool-call heads and blank text/reasoning is dropped from the flow derivation, so consecutive tool results merge into one group. Interrupted nodes always render (they carry the 已停止 marker).
  • Tool row summaries relativize workspace-rooted paths. The session cwd threads through the toolview slot contract (ToolRowOwnerProps.cwd) and toolRowModel strips it from summaries that start with it; paths outside the workspace stay verbatim. Display-only — args and the log are untouched.
  • The running sweep is a glare-band overlay. A fixed-width ::after gradient band animates across the row (the deepsuite ShimmerText pattern), replacing the previous mask-image approach, in both ToolRow and the Bash toolview.
  • The hero workspace chip is a selector, not an echo. With no live selection (cold start, or the workspace was deleted after the list settled) it shows a "Choose workspace" placeholder; the cwd-derived name only bridges the initial list load, and stale pending picks clear when their workspace leaves a ready list.
  • One 16px vertical rhythm. The chat column gap and in-group tool-row gap are both 16px, replacing the 10px in-group gap plus a negative cross-group margin.
  • Header title reads 14/20 with no turns counter; StateDot ongoing and the turn tail use a stepped pixel-chase loading language; body gets grayscale antialiasing (-webkit-font-smoothing and the Firefox macOS equivalent).

Alternatives considered

  • Position menus synchronously from anchor rects before mount. Rejected: the list's own size is unknown until it lays out, so clamping to the viewport still needs a post-layout measure; measuring a hidden mounted node is the pattern React and Floating UI document.
  • Filter empty assistant messages host-side. Rejected: the node is real model output that Trajectory and replay must keep; only the chat presentation should skip it, and the web layer is pure presentation by contract.
  • Relativize paths in each tool's presenter. Rejected: the redundancy is shared by every path-summarizing tool; one display-only pass in toolRowModel covers them all and non-chat consumers keep absolute paths.
  • Keep the mask-based sweep. Rejected: the mask dims the entire row content including state dots, and its exit transition fought the hover icon crossfade; an overlay band composites above the content without touching its alpha.
  • Keep showing the deleted workspace's name in the chip. Rejected: the chip is the selector for the next session; echoing a cwd whose workspace the user just deleted misrepresents the current pick.

Consequences

Chat renders fewer flow items than the snapshot has nodes: anyone counting rendered blocks against nodes must account for skipped render-nothing assistants (the chat-view spec pins this). The path relativization is a prefix check against the session cwd, so a workspace rename mid-session shows absolute paths until the summary re-derives — accepted as display-only staleness. The uniform 16px rhythm retires the tighter 10px tool-run look; a future denser layout would reintroduce a second constant deliberately. The menu pre-render adds one hidden layout pass per open, negligible at menu sizes.

Testing

chat-view.spec.tsx pins the render-nothing grouping (including the interrupted exception); chat-tool-row.spec.tsx pins cwd relativization inside/outside the workspace and with an empty cwd; atoms.spec.tsx and workspace-picker.spec.tsx cover the menu and chip states; the full ui-conversation, ui-primitives, and ui-workspace suites pass.

中文

问题

一次针对 web GUI 对话界面的设计评审发现了一批视觉呈现缺陷:portal 菜单在重新定位前会先在错误位置绘制一帧(打开时可见跳动);只要某条步骤消息只携带工具调用头,聊天列就会把一次工具运行拆成好几组;工具行摘要打印以工作区为根的绝对路径,占掉行内大部分空间;运行中行的扫光效果用 alpha 遮罩实现,把整行都压暗;hero 区的工作区 chip 会从会话 cwd 里复现已删除工作区的文件夹名;标题栏还在 13px 的标题旁显示一个没人需要的轮次计数。

决策

本次修复仅改动展示层;不会有任何内容进入会话日志。

  • **Portal 菜单先隐藏预渲染,绘制前完成测量。**菜单列表以 visibility: hidden 挂载在 (0,0),在 useLayoutEffect 中测量,显示时已处于最终位置。菜单与视口保持 12px 间距并支持内部滚动;工作区创建操作固定在不滚动的页脚区。
  • **聊天流跳过不渲染任何内容的助手节点。**已定稿的助手节点若其块仅含工具调用头和空白的文本/推理(reasoning)内容,就会从流推导中剔除,于是连续的工具结果合并为一组。被中断的节点始终渲染(它们携带「已停止」标记)。
  • **工具行摘要把以工作区为根的路径转为相对路径。**会话 cwd 经由 toolview 插槽契约(ToolRowOwnerProps.cwd)逐层传递,toolRowModel 从以其开头的摘要中剥去该前缀;工作区之外的路径保持原样。这只影响显示:工具参数与日志均不受影响。
  • **运行中的扫光效果改为高光带叠加层。**一条固定宽度的 ::after 渐变光带横向扫过整行(即 deepsuite 的 ShimmerText 模式),取代先前的 mask-image 方案,ToolRow 与 Bash toolview 两处均已替换。
  • **hero 区的工作区 chip 是选择器,而非回显。**没有有效选中项时(冷启动,或列表稳定后工作区被删除),它显示「Choose workspace」占位文案;由 cwd 推导的名称只用于衔接列表的首次加载,尚待确认的选择所对应的工作区从已就绪的列表中消失时,该陈旧选择会被清除。
  • **统一为 16px 的纵向节奏。**聊天列间距与分组内工具行间距统一为 16px,取代原先「分组内 10px 间距加跨分组负外边距」的做法。
  • 标题文字改为 14/20,标题栏去掉轮次计数;StateDot 的进行中状态与轮次尾部采用逐格推进的像素追逐式加载视觉语言;body 启用灰度抗锯齿(-webkit-font-smoothing 及 Firefox 在 macOS 上的等价设置)。

考虑过的替代方案

  • **挂载前根据锚点矩形同步定位菜单。**不予采纳:列表自身尺寸在布局完成前无从得知,向视口内收拢仍然需要布局后测量;对已挂载的隐藏节点做测量正是 React 与 Floating UI 文档记载的模式。
  • **在宿主侧过滤空的助手消息。**不予采纳:该节点是真实的模型输出,Trajectory 与回放都必须保留它;只有聊天展示应当跳过它,且按契约 web 层只负责呈现。
  • **在每个工具各自的 presenter 中做路径相对化。**不予采纳:这种冗余是所有输出路径摘要的工具共有的;在 toolRowModel 里做一次仅影响显示的处理即可覆盖全部工具,非聊天消费方仍拿到绝对路径。
  • **保留基于遮罩的扫光。**不予采纳:遮罩会把包括状态圆点在内的整行内容压暗,其退出过渡还与悬停图标的交叉淡入淡出相互冲突;叠加光带在内容之上合成,完全不触碰内容的 alpha。
  • **让 chip 继续显示已删除工作区的名称。**不予采纳:chip 是为下一个会话服务的选择器;用户刚删掉某个工作区,还回显它的 cwd,就是在错误呈现当前的选择。

后果

聊天渲染出的流条目数少于快照中的节点数:凡是拿渲染出的块与节点数对账的人,都必须把被跳过的「不渲染任何内容」的助手节点计算在内(chat-view 规格测试固定了这一点)。路径相对化只是针对会话 cwd 的前缀检查,因此会话中途重命名工作区后,摘要在重新推导前会显示绝对路径,这被接受为仅影响显示的陈旧状态。统一的 16px 节奏淘汰了原先更紧凑的 10px 工具运行外观;将来若要更紧凑的布局,应当有意识地重新引入第二个常量。菜单预渲染让每次打开多一次隐藏布局计算,在菜单的尺寸量级下开销可忽略。

测试

chat-view.spec.tsx 锁定了「不渲染任何内容」节点的分组行为(含被中断节点这一例外);chat-tool-row.spec.tsx 锁定了工作区内、工作区外以及 cwd 为空时的 cwd 相对化行为;atoms.spec.tsxworkspace-picker.spec.tsx 覆盖菜单与 chip 的各种状态;ui-conversation、ui-primitives 与 ui-workspace 的全量测试套件通过。