Reader: the middle of a turn is markdown too - #88
Conversation
|
Found it, and it was none of the three candidates. Pushed as What it wasn'tMeasured on the expanded row rather than reasoned about:
Candidate 3 was worth chasing for the reason you gave, so I checked it from the other end too: a note whose text does begin with What it wasThe blank line was the head row. A So, to your question about the triangle: not intended, and the same bug wearing the other hat. One cause, two symptoms. The fixOpen, a .cs.note.open { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: start; }
.cs.note.open > .cs-head { grid-column: 1; width: auto; align-self: stretch; padding-right: 0; }
.cs.note.open > .cs-head .cs-detail { display: none; }
.cs.note.open > .cs-body { grid-column: 2; padding: 0 0 6px 0.45em; }The triangle now sits beside the first line it belongs to, the way it does collapsed, so toggling doesn't move the text. The gutter it lives in is
Pinned
Mutation-checked — dropping the grid rule fails One harness note, since it nearly cost me a wrong diagnosisMy capture script read |
> intermediate agent messages in the reader are not rendered as md although they
> come with md syntax.
Only the answer was ever rendered — `highlightHtml(renderMarkdown(answer), q)`
into `.markdown.cx-md`. Everything the agent said on the way there went through
`<Hi text={shown} />`, which is plain text plus search highlighting, so a
mid-turn message arrived with `## Plan`, `**index**` and
`[the docs](https://…)` and was shown exactly like that.
The prose kinds now take the same path. `proseOf()` (exchanges.ts) names them —
`note`, `think`, `compact`: the agent writing for a reader — and everything else
stays literal, because a tool's input is JSON, a shell's output and a tool
result are terminal bytes where two spaces mean two spaces, and an image is an
image.
**It renders in the body, not in the head row.** That row is a `<button>` and
markdown carries links and block elements, neither of which is valid or
clickable inside one. So the row keeps its one-line preview while shut, hands
the text over when open, and the rendered prose sits in `.cs-body` the way a
tool's input and a shell's output already do. That replaces "expanding never
repeats itself" with something narrower: expanding never *shows the same words
twice*, which printing syntax above a rendered copy would.
**The collapsed preview keeps its syntax.** `## Plan` says the message opens
with a heading and ``` says code is coming; `Plan` says less. Stripping it would
also mean a second markdown pass over a string that may be cut mid-token.
**A cut message cannot take the panel with it.** These steps carry a `more`
tail, so the text can end mid-fence or mid-table. `marked` closes the block
itself and DOMPurify reparses what it emits; verified on a 27,603-char message
the 20,000-char cap cuts inside a fence — the step renders, its "+7,603 chars
not retained" line follows, and every later step and answer still renders.
Each kind keeps the colour it had as plain text (`.cs.think .cs-md` stays
purple, a note stays quiet), so a rendered aside does not present itself as the
agent's reply. The dead `.cs-detail.full` rule goes with the in-place expansion
it styled.
Search highlighting survives, which was the first thing to check: searching
"index" marks two hits inside the rendered prose, their parents `strong` and
`p` — the bold survived and the mark landed inside it.
`web/test/stepMarkdown.test.mjs` pins the two testable halves: truncated fences,
tables and lists close (an unterminated code span stays literal), and `proseOf`
answers which kinds render. Mutation-checked by letting shell output through the
renderer, which fails it. The highlight-over-markdown path needs a DOM
(`highlightHtml` walks text nodes with DOMParser), so it is checked in the
browser rather than in node.
Branched off design/reader-order (#85), which restructures this file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator, on dev: "it seems to add an empty line at the beginning when expanded." Three causes were plausible and measurement ruled out all three: - the first rendered block already had `margin-top: 0px` (`.cs-md > :first-child`); - the renderer emits no empty node — a source starting with a blank line, or with blank lines and spaces, yields no leading `<p></p>`, because markdown drops leading whitespace; - and `oneLine()` trims the same whitespace for the collapsed preview, so the two views never disagreed about it. The blank line WAS the head row. A `note` has no label, so when its text moved to the body the row held nothing but a triangle — and a row whose only content is a 14px triangle still takes a line: 16.3px, plus 2px of body padding. That is the same reason the triangle sat alone, so it was one bug wearing two hats, and neither was intended. Open, a note is now two columns — the disclosure gutter, and the prose beside it — so the triangle keeps company with the first line and the gutter is what closes the step. Desktop row 244px → 222px; the triangle stays at y=2 while the first block moves from below it to y=0, level with it. `think` and `compact` keep the stacked layout: their row says `thinking` or `context compacted`, which is not an empty line. Pinned in `stepMarkdown.test.mjs`: the two ruled-out candidates (no empty node for three leading-whitespace shapes, `oneLine` trims), and the layout itself — `.cs.note.open` is a two-column grid, its emptied detail slot is out of the flow, the body has no top padding, and `think` did *not* gain the grid. Dropping the grid rule fails it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… line
Two states the operator hit on dev, both in the exchange meta row.
**A turn with no tool calls indented its summary.** Measured from the ink rather
than the boxes: everything in an exchange starts on one column at x=16 — the
prompt's words, the answer's words, the step rows — and a flat summary started at
x=35, because `.cx-fold.flat` paid `padding-left: 1.75em` to reserve the gutter
for an expand triangle that cannot exist in that state.
§3.2 already answers which way to fix that: the prompt's `❯` is the only thing
outside the text column. So the marks hang instead — the fold's `▸` and the
working line's spinner both — and nothing reserves space inside the column. The
cell is one number, `--cx-mark` on `.cx-meta`, read by the negative margin and by
the glyph's width so they cannot drift apart. Flat and expandable summaries now
start on the same column as the prose: 35 → 16, with the mark at 0..16 in the
gutter. `working` moves 31 → 16 by the same rule.
**A turn with nothing yet had an empty row above its working line.** No steps, no
duration, no tokens means `stepSummary` is empty, so the meta row rendered with a
blank left half and the working line sat on the row below — dropped rather than
placed. There is no meta row in that state now: the turn's facts ride the working
line, one row instead of one and a half.
The working line does not move for this, which was the constraint that mattered:
it is the last row at the text column in both states. Checked live rather than
inferred — appending a tool call to the transcript under an open reader and
letting the poll pick it up gives `ink 16 → 16`, `last row true → true`, and the
facts moving up into the newly-rendered meta row.
The DOM assertions for that state all passed while the row rendered
`workingturn 3/301:02 PM`: `.spacer { flex: 1 }` was scoped to `.cx-meta`, and
`.cx-running` cannot take a `gap` (its spinner is a ::before flex item, and a gap
would push `working` off the column). Both the spacer and the facts' own margins
are pinned now, because only a screenshot caught it.
Pinned in `pendingExchange.test.mjs`: an empty turn summarises to nothing and its
facts render inside `.cx-running` with no meta row; the same turn with a duration
puts them back on the meta row and not on both; a finished turn with no tools
gets a flat fold; and in CSS, a flat fold has no left padding, the fold hangs by
`--cx-mark`, the spinner hangs by `--mark-w`, and the working row spaces its
facts. Mutation-checked: restoring the 1.75em gutter fails one, forcing
`factsOnRunningRow` false fails another.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0efb74f to
f1e1bfd
Compare
fedb89a to
2486300
Compare
|
Both fixed, on this branch ( The indent: a gutter reserved for a control that cannot existMeasured from the ink, not the boxes — a flat fold's box starts where an expandable one's does, which is why this looks fine in a DOM dump:
Which of the two fixes the design wants: §3.2 already answers it — "the prompt's The working line: it was the empty row above it
The constraint you set — the line must not jump when the first step arrives — is met, and I checked it in motion rather than inferring it. Appending a tool call to the transcript under an open reader and letting the poll pick it up: The working line keeps its place — last row, text column — and what moves is the facts, up one line into the meta row that now exists. That is the smallest thing that can move, and it happens once. One thing worth flagging, because tests missed itEvery DOM assertion for the new state passed while the row actually rendered:
PinnedIn
Mutation-checked — restoring the CollisionsTrial-merged #84 at its current head: clean now, both files. The |
Before/after captures →
Stacked on #85 (
design/reader-order), which restructures this same file —StepRowand the step rail. Base is that branch, not main, so this diff is only the markdown change; it needs #85 to land first, and GitHub will retarget it to main automatically when that happens.What was wrong
Only the answer was ever rendered —
highlightHtml(renderMarkdown(answer), q)into.markdown.cx-md(Exchange.tsx:181,221). Everything the agent said on the way there is aStepof kindnote/think/compact, rendered through<Hi text={shown} q={q} />: plain text plus search highlighting. So a mid-turn message arrived with headings, lists, code spans and links and was printed as syntax.What changed
proseOf()inexchanges.tsnames the kinds that are the agent writing for a reader —note,think,compact— andStepRowsends those through the answer's own path. Everything else stays literal.It renders in the body, not in the head row. That row is a
<button>, and markdown carries links and block elements; neither is valid — or clickable — inside one. So the row keeps its one-line preview while shut, hands the text over when open, and the rendered prose sits in.cs-body, the way a tool's input and a shell's output already do.That narrows the old rule rather than breaking it: "expanding never repeats itself" becomes expanding never shows the same words twice — which printing the syntax above a rendered copy would. I built it the other way first (preview kept above the rendered prose) and the duplication was the reason to change it; both variants are in the captures.
The four things you flagged
highlightHtml(renderMarkdown(text), q)— the helper walks text nodes only, so tags survive. Searching “index” marks 2 hits inside the rendered prose, parentsstrongandp: the bold survived and the mark went inside it.## Plansays the message opens with a heading and```says code is coming —Plansays less. Stripping would also mean a second markdown pass over a string that may be cut mid-token, i.e. a second renderer path to get wrong.markedcloses a cut fence, table or list itself, and DOMPurify reparses what it emits. Verified on a 27,603-char message that the 20,000-charVIEW_BLOCK_CAPcuts inside a fence: the step renders, its…+7,603 chars not retainedline follows, and every later step and answer still renders (steps in the last exchange: 2, answers on screen: 3, no page errors).note,think,compact. Out: a tool's input (JSON, andToolCallalready formats it), a shell's output and a tool result (terminal bytes — two spaces mean two spaces, and markdown would eat them), an image.Each kind also keeps the colour it had as plain text, so a rendered aside doesn't present itself as the reply and rendered thinking still looks like thinking. And the now-dead
.cs-detail.fullrule goes with the in-place expansion it styled.Verified
Measured on both builds against a fixture whose middle messages carry a heading, an ordered list, code spans, a link, a fenced block and a quote:
markdown=false,tags=-markdown=true,tags=h2,p,ol,li,code,strong,a,pre,blockquoteweb/test/stepMarkdown.test.mjs(new, wired intonpm test) pins the two halves node can hold: truncated fences/tables/lists close and an unterminated code span stays literal; andproseOfanswers which kinds render. Mutation-checked — letting shell output through the renderer fails it. The highlight-over-markdown path needs a DOM (highlightHtmlusesDOMParser), so it is checked in the browser instead, and the number above is from that run rather than from a green tick.Web suite and typecheck green; server suite green.
Collisions
Trial-merged against #84 (
feat/trace-share-and-reader-info) at its current head: one conflict,web/package.json— the recurringtestscript line. Its branch now also carriesstatusMark,fileWrapToggleandsettingsMobilesuites plus atest:renderscript, so the resolution is to keep both lists and both scripts.ConversationView.tsxandconversation.cssdo not conflict: #84's work is the bar and theipanel, mine isStepRowand.cs-md.🤖 Generated with Claude Code