Reader: render user prompts as markdown, in one band component - #107
Merged
Conversation
The reader rendered everything except the thing a person wrote. The answer always went through renderMarkdown; an aside, thinking and a compaction have since #88; the prompt band was still raw text, so a typed `**important**`, a pasted fence or a list showed its syntax. It renders through the answer's exact path — highlightHtml(renderMarkdown(...)) — so a search term lands in a rendered prompt the way it lands in a rendered answer, rather than through a second mechanism that agrees with the first until it doesn't. ONE component, PromptBand, for both call sites. #77 was precisely the real turn and the optimistic echo drifting: written out by hand twice, the echo lost the `.cx` wrapper whose padding cancels `.cx-prompt`'s `margin-left: -1.23em` and the newest prompt in every conversation hung a gutter's width left of the ones above it. Two copies of markdown-plus-highlight is that bug with more surface. `breaks: true`, for the prompt only. A prompt is typed, not authored: its newlines are where someone pressed Enter, and the band showed every one of them as pre-wrap. Under strict markdown every multi-line prompt in the reader would have silently reflowed into one paragraph. An agent writes markdown deliberately, so an answer's single newline stays a soft wrap. The band keeps its geometry, measured rather than asserted. New promptBand.render.test.mjs builds the real component for the browser — real marked, real DOMPurify — renders it inside the reader's own frame and compares it against the band as it was: text left edge 29.984375px → 29.984375px, and equal to the answer's below it band left/right 14 / 640px, unchanged: still full bleed one-line height 30px → 30px (first and last child give up their margins) four typed lines 75px → 75px (as three <br>, not one paragraph) queued one-liner 30px, pill on the last line's baseline, right of the words column overflow 626 of 612 either way — the band's own bleed, nothing new Mutation-checked, each against the assertions it should break: default last-child margin (2), no inline-block (2), no max-width (3), no `breaks` (2), no font-size/ line-height reset (2), no highlight (1), a fence that scrolls (1). Two judgement calls worth stating, since the band was designed for one line: - A fence in a prompt WRAPS where a fence in an answer scrolls. This band held the operator's own text as pre-wrap until now, so a pasted error was fully on screen; trading that for a horizontal swipe inside a slab is the same mistake as putting something behind a hover, and worst on a phone where the band is widest relative to the screen. An answer's fence is code an agent wrote, where the line structure is the content. - Nothing is capped. A prompt with a 200-line paste makes a tall tinted band — but it already did as raw text, and the answer's own fences are uncapped too. A cap here would be a new behaviour, not a preserved one. Headings step down to 1.05em and blocks space at 6px: a band is a strip in a rhythm, and the shared .markdown card's 22px h1 and 12px paragraph gaps read as a document dropped into the conversation. Screens (plain, multi-line, fence and list, queued, search, dark, phone): https://lvwerra-agent-artifacts.static.hf.space/prompt-markdown.html Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lvwerra
commented
Aug 21, 2026
lvwerra
left a comment
Member
Author
There was a problem hiding this comment.
VERDICT: APPROVE
Correctness
No correctness findings.
- Geometry: the PR's browser measurements reproduce exactly: text left
29.984375px, band edges14/640px, one-line height30px, four typed lines75px, and column width626/612px. I also rendered first-child heading, list and fence blocks independently at 320px and 390px. The band reaches the same right edge in every case;.cxv-bodyand the page both retain zero horizontal scroll room, and the fence/table remain contained. The Overview card stays within its own 480px frame. breaks: the live component emits one<br>for a prompt's single newline and none for the same newline in an answer. The source test would fail both obvious convergence mutations: passing{ breaks: true }to the answer putsbreaksin the guarded answer slice, and making the renderer unconditional removes the assertedbreaks: !!opts?.breakscontract.- One component/two sites:
PromptBandis the sole writer ofclassName="cx-prompt"; bothExchangeViewandPendingExchangecall it. The count assertions fail if a normal second hand-written band is introduced or either call is removed, so they pin the #77 regression rather than merely observing current markup. - Search: independent browser probes put
mark.cx-hitinside the original<code>,<a>and<h1>nodes, preserving each rendered structure. This is the samehighlightHtml(renderMarkdown(...))path as answers. - Safety: the prompt takes the existing
renderMarkdown→ DOMPurify path. In a live browser probe,<script>disappeared,onerrordisappeared, ajavascript:href was removed, and safe formatting remained; inserting the result did not execute anything.
Does it solve the operator's problem?
Yes. The one-line and typed-multiline cases keep the old rhythm, while lists, emphasis and code now read as structure instead of syntax. The phone fence wraps without widening the page, and the Overview card uses the same band cleanly. Of the four shapes, the fence/list is necessarily denser than raw text, but the study's treatment keeps it legible and in the band's visual register in both themes; I do not see a worse regression there.
Verification
- Inspected the supplied reader, phone, Overview-card, search and optimistic-echo screens.
npm test: 17 suites pass.npm run test:render: all 3 Chromium suites pass.npm run typecheckandnpm run buildpass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The reader rendered everything except the thing a person wrote. The answer has always gone through
renderMarkdown; an aside, thinking and a compaction have since #88. The prompt band was raw text, so a typed**important**, a pasted fence or a list showed its syntax.Screens (four prompt shapes, dark, phone, the Overview card, search, the optimistic echo): https://lvwerra-agent-artifacts.static.hf.space/prompt-markdown.html
What changed
PromptBand— one component, used by both call sites — renders the prompt through the answer's exact path,highlightHtml(renderMarkdown(text, { breaks: true }), q).One band, not two. #77 was precisely the real turn and the optimistic echo drifting: written out by hand in two places, the echo lost the
.cxwrapper whose padding cancels.cx-prompt'smargin-left: -1.23em, and the newest prompt in every conversation hung a gutter's width left of the ones above it. Two copies of markdown-plus-highlight is that bug with more surface.pendingExchange.test.mjsnow fails if a second hand-written band appears in the file, and if either call site stops using the component.breaks: true, for the prompt only. A prompt is typed, not authored: its newlines are where someone pressed Enter, and the band showed every one of them aspre-wrap. Under strict markdown every multi-line prompt in the reader would have quietly reflowed into one paragraph — a loss of what was typed, not a rendering choice. An agent writes markdown deliberately, so an answer's single newline stays a soft wrap; a test reads the answer's own call to keep it that way.The band keeps its geometry, and it is measured rather than argued.
promptBand.render.test.mjsbuilds the real component for the browser — realmarked, realDOMPurify— renders it inside the reader's own frame, and compares against the band as it was:29.984375px29.984375px14 / 640px14 / 640px30px30px75px75px30px30px626 of 612626 of 612The rendered prompt's text also starts on the same pixel as the answer below it, which is §3.2's one-column rule. The column overflows its client width by the gutter in both cases — the band is full-bleed by design, and that is the number a wide prompt must not move; my first version of that check asserted equality with
clientWidthand was wrong about the baseline, not about the code.How the heights hold:
.cx-pmd > :first-child/:last-childgive up their margins, so a<p>does not add a fraction of a line to every turn in the reader..markdowncard's border, background,14pxand1.6are reset — it is a card elsewhere; here it is the band's own text at the band's own size.inline-block, so thequeuedpill still sits on the last line's baseline instead of dropping below it and making a queued one-liner a line taller than every other prompt.1.05emand blocks space at6px: the shared22pxh1 and12pxparagraph gaps read as a document dropped into a strip.Mutation-checked, each against the assertions it should break: default last-child margin (2 fail), no
inline-block(2), nomax-width(3), nobreaks(2), no font-size/line-height reset (2), no highlight (1), a fence that scrolls (1), a hand-written second band (2).Two judgement calls, since the band was designed for one line of text
A fence in a prompt wraps; a fence in an answer scrolls. I looked at it before claiming it worked, and the first version scrolled — which on a phone hides the end of a pasted error behind a horizontal drag inside a slab. This band held the operator's own text as
pre-wrapuntil now, so all of it was on screen; trading that for a swipe is the same mistake as putting something behind a hover. An answer's fence is code an agent wrote, where the line structure is the content and reflowing it lies about it; a prompt's fence is a paste. The divergence is written down where the rule lives.Nothing is capped. A prompt with a 200-line paste makes a tall tinted band — but it already did as raw text, and the answer's own fences are uncapped too, so a cap here would be new behaviour rather than preserved behaviour. If it bites, it belongs on both.
One more thing worth stating: raw HTML in a prompt now renders as HTML. It goes through the same
DOMPurifysanitiser as agent output, so a<script>or anonerrordoes not survive, but<div>hi</div>will draw a div where it used to show angle brackets.State
Typecheck clean. 17 web suites, 3 browser suites (
npm run test:render, which now includes this one), 24 server suites — all green. Branch off3ce91a9.