DSH / Atlas
2026-08-21implementedbug-fixarchived 2026-09-04

Sent user text projects inline, and queue rows fold wire references

已发送用户文本行内投影,Queue 行折叠 wire 引用

Two display gaps in sent user text, both older than the Lexical composer. The user-bubble decorator (`projectUserText`, then private to `MessageItem`) split one message into plain runs and reference chips, but rendered every plain run through the block-level `MessageText` div — so a decorated single-line message broke into one line per run, and the single space between two adjacent tokens rendered as a whole blank li

English

Problem

Two display gaps in sent user text, both older than the Lexical composer. The user-bubble decorator (projectUserText, then private to MessageItem) split one message into plain runs and reference chips, but rendered every plain run through the block-level MessageText div — so a decorated single-line message broke into one line per run, and the single space between two adjacent tokens rendered as a whole blank line. Separately, the queue dock's read-only row printed row.preview verbatim, so a queued message carrying a chip showed the wire session form @[查看并分析图片](dsh-session:InNlc3Npb24t…) — the model-facing text, unreadable as a preview. The logged model text was correct in both cases (verified against the session log bytes); both defects were presentation only.

Decision

One shared inline projection, reference/user-text.tsx, owns the display of sent user text and is consumed by the bubble and the queue row:

  • Everything inline. Plain runs render as spans; the block-level MessageText leaves the path entirely. White-space policy stays with the consumer: the bubble declares pre-wrap (real newlines survive), the queue preview keeps its nowrap/ellipsis single line — the shared spans pin neither.
  • Wire session forms fold. A new highest-precedence rule folds @[label](dsh-session:…) to a session chip showing the label (the source text stays on title). The existing rules — recall-associated exact labels, then bare /name / @name tokens by shape — follow at their old precedence, so the fold also shields the URI from the bare-token scan that would otherwise misread it as a file path.
  • Queue edit stays literal. The row's edit field exposes row.text unchanged: the user edits exactly what will be sent, and folding an editable surface would detach the visible text from the durable one.

queue-actions.e2e locators moved from getByText(…).locator('..') to row-container matching (li with hasText): the projection adds one span layer, so a parent hop from the matched text no longer lands on the row.

Alternatives considered

  • Carry a display text beside the queued model text: rejected — it adds a wire/session field for a presentation concern and violates the single-truth rule; folding at render needs no new state.
  • Fold inside the editor field too: rejected — the edit target is the literal sent text; a folded editable view would let the user "edit" text that is not what gets sent.
  • Fix only the bubble's blank line with CSS (collapse empty runs): rejected — the runs were block-level by construction, and the queue gap needed the shared projection anyway.

Consequences

  • A decorated single-line message renders on one line; the bubble in the field report dropped from four visual lines (one blank) to its natural wrapped height.
  • Queue previews read as the composer showed them: label chips instead of dsh-session: URIs; the wire form also folds in the bubble if it ever reaches durable text.
  • MessageItem and QueueDock share one decoration vocabulary and stylesheet (user-text.module.css); the chip styles left MessageItem.module.css.
  • Tests: user-text.client.spec pins the inline guarantee (zero divs, whitespace-preserving runs) and every fold rule; the chat-view literal-text matcher followed the element change.

中文

问题

已发送用户文本存在两个显示缺口,都早于 Lexical composer。用户气泡装饰器(projectUserText,当时是 MessageItem 的私有函数)把一条消息切成普通片段与引用 chip,但每个普通片段都经块级的 MessageText div 渲染——被装饰的单行消息因此被拆成每段一行,两个相邻 token 之间的单个空格更是渲染成一整行空白。另一处,queue dock 的只读行原样打印 row.preview,携带 chip 的排队消息因此显示 wire 会话形式 @[查看并分析图片](dsh-session:InNlc3Npb24t…)——面向模型的文本,作为预览不可读。两处的持久模型文本都是正确的(已对照会话日志字节核实);两个缺陷都纯属呈现层。

决策

由一个共享的行内投影 reference/user-text.tsx 负责已发送用户文本的显示,气泡与 queue 行共同消费:

  • 一切行内。 普通片段渲染为 span;块级 MessageText 彻底退出该路径。换行策略归消费方:气泡声明 pre-wrap(真实换行保留),queue 预览保持 nowrap/省略号单行——共享 span 两者都不钉死。
  • wire 会话形式折叠。 新增最高优先级规则把 @[label](dsh-session:…) 折叠为显示标签的会话 chip(原文保留在 title)。既有规则——recall 关联的精确标签、按形状识别的裸 /name / @name token——按原优先级跟随,因此折叠同时挡住了裸 token 扫描(否则它会把 URI 误读成文件路径)。
  • Queue 编辑态保持原文。 行的编辑框展示未经处理的 row.text:用户编辑的正是将要发送的内容,折叠一个可编辑表面会让可见文本与持久文本脱钩。

queue-actions.e2e 的定位器从 getByText(…).locator('..') 改为行容器匹配(带 hasText 的 li):投影多了一层 span,从命中文本向上跳一级不再落在行元素上。

曾考虑的替代方案

  • 在排队模型文本旁另存一份显示文本:否决——为呈现关切增加 wire/会话字段,违背单一真源;渲染期折叠不需要新状态。
  • 编辑框内也折叠:否决——编辑对象就是字面发送文本,可编辑表面折叠会让用户"编辑"一段并非实际发送的文本。
  • 仅用 CSS 修气泡空行(折叠空片段):否决——片段的块级性是结构性的,且 queue 缺口本来就需要共享投影。

后果

  • 被装饰的单行消息渲染为一行;现场报告中的气泡从四视觉行(含一空行)降到自然换行高度。
  • Queue 预览按 composer 中的样子呈现:标签 chip 取代 dsh-session: URI;wire 形式若进入持久文本,气泡同样折叠。
  • MessageItem 与 QueueDock 共享一套装饰词汇与样式表(user-text.module.css);chip 样式移出 MessageItem.module.css。
  • 测试:user-text.client.spec 钉住行内保证(零 div、片段保留空白)与每条折叠规则;chat-view 的字面文本匹配器随元素变化更新。