Skip to content

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

Merged
jtomaszewski merged 4 commits into
mainfrom
jtomaszewski/markdown-preview
Sep 2, 2026
Merged

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
jtomaszewski merged 4 commits into
mainfrom
jtomaszewski/markdown-preview

Conversation

@jtomaszewski

@jtomaszewski jtomaszewski commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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 means
reading it — which is the one thing that view makes impossible. Every line
being an addition also means the diff's + markers carry no information at
all: 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 + on
a 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 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.

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.tsreadMarkdown(patch) walks the patch's new side and
    cuts 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 raw
    concatenation, which reproduces the source exactly, rather than guessing at
    which tokens carry their own terminator.
  • DiffBlock now renders a chapter file by file: markdown files in reading
    mode as MarkdownFile, consecutive others in one diff2html render, in patch
    order. The old imperative renderer is unchanged as DiffGroup.
  • MarkdownPreview and the finished render share one renderMarkdown(), so a
    preview 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.
  • The chapter fold counted every line as a table row. A document is not one:
    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.
  • A comment that cannot post inline renders under its own file's header — where
    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 comment
    naming no file in the chapter's patch still floats above the diff.

Verification

  • 27 new tests: 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 worth
    drawing).
  • pnpm typecheck && pnpm test && pnpm build all pass (558 tests).
  • Driven in a real browser against real review artifacts: an added spec renders
    as a document with the AI's comments in place; a modified file's toggle,
    stripes and ⋯ 310 lines the diff skips marker; the block + composer
    storing a real line; a comment on a removed line landing under its file
    header; and inline comments in code diffs unaffected.

Docs

SPEC.md gains §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.md and CLAUDE.md describe the reading view.

🤖 Generated with Claude Code

jtomaszewski and others added 3 commits September 2, 2026 07:48
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment thread web/src/mdblocks.ts
… 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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

  • Markdown always calls renderMarkdown() inside useMemo, even when text is blank and the component returns null. That still runs marked.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 in body as an extra empty file line (because split("\n") keeps a final "" entry). Real gh pr diff output 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

@jtomaszewski
jtomaszewski merged commit fc11dde into main Sep 2, 2026
3 checks passed
@jtomaszewski
jtomaszewski deleted the jtomaszewski/markdown-preview branch September 2, 2026 06:37
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 0.28.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants