Reader: markdown in the middle of a turn, and one column for the meta row - #90
Merged
Conversation
> 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>
Reader: the middle of a turn is markdown too
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.
Carries #88 onto main. It merged into
design/reader-orderrather thanmain, because that branch was its base — #88 was deliberately stacked on #85 so the two would not fork while both were editingExchange.tsx. #85 then merged to main first, so #88's merge landed on a branch that was already upstream, and main ended up without any of it.Nothing here is new work. Four commits, all previously reviewed or operator-verified on the dev Space:
0e58be8— mid-turn messages render as markdown, not raw syntax (improv.md iteration two)633d492— the empty line at the top of an expanded note: it was the head row, not a marginf1e1bfd— one column for the meta row, and no empty row above a working line5f379a1— the Reader: the middle of a turn is markdown too #88 merge commitVerified as part of the
dev5assembly currently running onagent-manager-dev: web typecheck, all 13 web suites, and both server suites green.The lesson worth keeping: a stacked PR merges into its stack parent, not into main. When the parent lands first, the child's merge silently goes nowhere useful. Either retarget the child's base to
mainbefore merging it, or merge the child into the parent first and the parent last.