Agent Note: Sent user text projects inline, and queue rows fold wire references
Status: implemented Archived: 2026-09-04
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 @[查看并分析图片](https://github.com/deepseek-ai/deepseek-harness/blob/c291e7961a515f6d7af9304e7fd1d257929aef26/.agents/notes/archived/bug-fix/dsh-session:InNlc3Npb24t…/README.md) — 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-levelMessageTextleaves the path entirely. White-space policy stays with the consumer: the bubble declarespre-wrap(real newlines survive), the queue preview keeps itsnowrap/ellipsis single line — the shared spans pin neither. - Wire session forms fold. A new highest-precedence rule folds
@[label](https://github.com/deepseek-ai/deepseek-harness/blob/c291e7961a515f6d7af9304e7fd1d257929aef26/.agents/notes/archived/bug-fix/dsh-session:…/README.md)to a session chip showing the label (the source text stays ontitle). The existing rules — recall-associated exact labels, then bare/name/@nametokens 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.textunchanged: 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. MessageItemandQueueDockshare one decoration vocabulary and stylesheet (user-text.module.css); the chip styles leftMessageItem.module.css.- Tests:
user-text.client.specpins the inline guarantee (zerodivs, whitespace-preserving runs) and every fold rule; the chat-view literal-text matcher followed the element change.