feat(cockpit): a markdown file the PR adds opens as the document it is, and every box you write markdown in shows how it will read - #49
Conversation
A comment goes to GitHub as markdown, so "did that list come out as a list, and where did my backticks go" is a question the cockpit should answer before the Send rather than after it. Now it does, in all four places you write: a comment being edited, a comment being added, the line composer, and the chat input. The preview sits under the box with no tab and no toggle — a Write / Preview pair is two clicks to answer a question you have continuously — and it appears exactly when it has something to say: when the render reads back as something other than what was typed. A one-line note, or two plain paragraphs, get nothing, because a second identical copy of them is noise. A list, a heading, `code`, **bold**, a [link] all lose or move characters on the way through, and those are the drafts worth seeing first. A comment is previewed with its grade badge, since that is the body GitHub gets — the same withGrade() the finished comment, the payload and the export all use. Preview and render share one markdown path for the same reason: a preview that can drift from the real thing is worse than none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A PR that adds a 1,400-line spec rendered as 1,400 rows of `+ ## Heading` — the one form in which the document cannot be reviewed, because reviewing it means reading it. Every line being an addition means the diff's markers carry no information at all, and cost the reader the document. So a markdown file the PR creates now opens rendered. It is not a detour off the review. The file's patch is turned back into the document in blocks that keep their source line numbers, so the review's comments sit under the paragraph they point at — the nit about "three things to adopt" now stands directly above the two bullets it is counting — and any paragraph takes a comment or a question where it stands, the same gesture as the diff's gutter `+`. A markdown file the PR merely edits stays a diff, because there the change is the point; "read as a document" sits in its own file header. Read that way it says what it cannot show: the header warns it is the hunks alone, the lines between them are counted and marked as skipped, the blocks the PR added carry a stripe, and what the PR removed is not in a document at all — the diff is one click back. The chapter fold counted every line as a table row. A document is not one: measured on a 1,028-line spec, 2,874 nodes against a diff's 12.7 a row, so a file that opens as a document now counts a quarter, and the folded label says "lines to draw" rather than claiming they are diff lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ys it posts in the body Click the diff's gutter `+` on a line the PR removed and the composer warns that the comment posts on the file rather than the line — GitHub takes inline comments on the new side only. What it didn't say is that the card then leaves: with no line to anchor to it floated to the top of the chapter, unlabelled and indistinguishable from an inline comment that had lost its place. You wrote it on line 251 and it appeared above the file. Now it renders under its own file's header, where GitHub puts a file-level comment, and carries the same kind of tag a drifted comment does: "on the file — posts in the body". Only a comment naming no file in the chapter's patch still floats above the diff, which is the one case where there is nowhere else to put it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
newSide() in web/src/mdblocks.ts currently treats a trailing split("\n") artifact as a real line, which can skew block line ranges and make deleted Markdown files appear readable.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Updates the cockpit’s diff rendering to make large Markdown additions reviewable as readable documents (with inline comment anchors preserved), and adds always-on Markdown previews in all markdown-capable inputs so authors can see exactly what will be posted to GitHub.
Changes:
- Add a Markdown “read as a document” mode that reconstructs the new-side content into top-level Markdown blocks with source line spans and hunk-gap markers.
- Add inline Markdown previews under comment editors / composers / chat input, shown only when the rendered output reads differently than the source.
- Improve placement and labeling of comments that cannot post inline (file-level / removed-line cases), and adjust chapter fold cost accounting accordingly.
File summaries
| File | Description |
|---|---|
| web/src/styles.css | Styling for file-level comment placement, markdown previews, and rendered Markdown documents/blocks. |
| web/src/mdblocks.ts | New Markdown patch-to-document parser that produces block/gap items with source line spans. |
| web/src/mdblocks.test.ts | Tests for markdown patch parsing (blocks, spans, gaps, new/modified/deleted). |
| web/src/Markdown.tsx | Centralize markdown rendering/sanitization and add preview + “reads as” comparison helpers. |
| web/src/markdown.test.ts | Tests for when markdown preview should/shouldn’t render. |
| web/src/Icon.tsx | Add a “file” icon for the Markdown document header. |
| web/src/Detail.tsx | Integrate Markdown document rendering, read/diff toggles, preview components, and file-level comment slots. |
| SPEC.md | Specify markdown-document rendering behavior, preview rules, fold arithmetic, and file-level comment handling. |
| README.md | Document the new behavior for added vs edited Markdown files in the cockpit. |
| CLAUDE.md | Update internal architecture notes to include the new markdown document/block feature. |
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… the file
`split("\n")` on a patch ending in a newline leaves an empty string
behind, and `newSide()` counted it as an empty context line. It is the
last file of a chapter's diff that gets one, so a markdown file the PR
*deleted* — which has no new side at all — came back with one line to
read, enough to clear the `lines > 0` guard and be offered as a
document that would then render empty. Every other file merely counted
one line too many, which the chapter fold's arithmetic reads.
`src/core/diff.ts` drops the same artifact for the same reason; this
helper now does too. The existing tests passed only because their
fixtures had no trailing newline — the two new ones fail without the
fix.
Found by Copilot's review on #49.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The implementation matches the described behavior and includes targeted tests; only minor perf/test-fixture nits were identified.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
web/src/Markdown.tsx:17
Markdownalways callsrenderMarkdown()insideuseMemo, even whentextis blank and the component returnsnull. That still runsmarked.parse()+DOMPurify.sanitize()for empty strings, which is unnecessary work on renders where there is nothing to display.
web/src/mdblocks.test.ts:16- The
added()test helper treats a trailing newline inbodyas an extra empty file line (becausesplit("\n")keeps a final "" entry). Realgh pr diffoutput for a file that ends with a newline does not include an extra added blank line, so this helper can produce slightly unrealistic patches.
- Files reviewed: 10/10 changed files
- Comments generated: 0 new
- Review effort level: Lite
|
🎉 This PR is included in version 0.28.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
The problem
A PR that adds a spec was unreviewable in the cockpit. A 1,400-line markdown
file arrived as 1,400 rows of
+ ## Heading, and reviewing a document meansreading it — which is the one thing that view makes impossible. Every line
being an addition also means the diff's
+markers carry no information atall: they cost the reader the document and give nothing back.
The same gap showed up wherever you write markdown. Comment bodies go to
GitHub as markdown, so "did that list come out as a list, and where did my
backticks go" was a question you could only answer after sending.
And a third, found while testing the first two: click the diff's gutter
+ona line the PR removed and the composer warns the comment posts on the file
rather than the line — GitHub takes inline comments on the new side only. What
it didn't say is that the card then leaves: with no line to anchor to, it
floated to the top of the chapter, unlabelled and indistinguishable from an
inline comment that had lost its place. You wrote it on line 251 and it
appeared above the file.
The fix
A markdown file the PR adds opens as the document it is. Not as a detour
off the review, either: the file's patch is turned back into the document in
blocks that keep their source line numbers, so the review's comments sit under
the paragraph they point at, and any paragraph takes a comment or a question
where it stands — the same gesture as the diff's gutter
+.A markdown file the PR merely edits stays a diff, because there the change is
the point;
read as a documentsits in its own file header. Read that way itsays what it cannot show: the header warns it is the hunks alone, the lines
between them are counted and marked as skipped, the blocks the PR added carry a
stripe, and what the PR removed is not in a document at all — the diff is one
click back.
Every box you type markdown into shows how it will read, under the box,
with no tab and no toggle — a Write/Preview pair is two clicks to answer a
question you have continuously. It appears exactly when it has something to
say: when the render reads back as something other than what was typed. A
one-line note gets nothing, because a second identical copy of itself is noise.
A file-level comment stays with its file and says what it is.
Technical details
web/src/mdblocks.ts—readMarkdown(patch)walks the patch's new side andcuts it into markdown's own top-level blocks (paragraph, heading, fenced
block, whole list), each carrying the source lines it came from and whether
the PR added any of them. Token spans are counted through
rawconcatenation, which reproduces the source exactly, rather than guessing at
which tokens carry their own terminator.
DiffBlocknow renders a chapter file by file: markdown files in readingmode as
MarkdownFile, consecutive others in one diff2html render, in patchorder. The old imperative renderer is unchanged as
DiffGroup.MarkdownPreviewand the finished render share onerenderMarkdown(), so apreview cannot drift from what it previews. A comment is previewed with its
grade badge, since that is the body GitHub gets. The show/hide rule is
"rendered text ≠ source text, whitespace flattened" — a rule, not a regex zoo.
measured on a 1,028-line spec, 2,874 DOM nodes against a diff's 12.7 a row,
so a file that opens as a document counts a quarter, and the folded label now
says "lines to draw" rather than claiming they are diff lines.
GitHub puts a file-level comment — with a tag beside it:
on the file — posts in the body, the same treatment a drifted comment already got. Only a commentnaming no file in the chapter's patch still floats above the diff.
Verification
web/src/mdblocks.test.ts(block spans, changed-block marking,hunk gaps, added/modified/deleted files, and the trailing-newline artifact
Copilot caught) and
web/src/markdown.test.ts(when a preview is worthdrawing).
pnpm typecheck && pnpm test && pnpm buildall pass (558 tests).as a document with the AI's comments in place; a modified file's toggle,
stripes and
⋯ 310 lines the diff skipsmarker; the block+composerstoring a real line; a comment on a removed line landing under its file
header; and inline comments in code diffs unaffected.
Docs
SPEC.mdgains §17.7 (markdown files as documents) and normative lines in§17.5 (the fold's new arithmetic) and §17.6 (the preview, and comments that
cannot post inline).
README.mdandCLAUDE.mddescribe the reading view.🤖 Generated with Claude Code