feat(web): focus the rendered markdown diff on changed blocks and list items - #629
Merged
Merged
Conversation
… details[open] in e2e
danyaberezun
requested review from
Olga Lavrichenko (OLavrik),
Rustam Sadykov (SBOne-Kenobi) and
Rinat S (rsolmano)
as code owners
October 5, 2026 01:39
danyaberezun
enabled auto-merge
October 5, 2026 01:43
…licit li values in the focused markdown diff
…cal twin cannot mask an attribute-only change
…disabled boolean mapping
Rinat S (rsolmano)
approved these changes
Oct 5, 2026
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.
Problem
Opening the Preview (rendered) diff of a markdown file showed the entire merged document with
<ins>/<del>marks sprinkled through it. For a long document — this repo's ownSPEC.mdfiles, a README with forty sections — a one-paragraph edit meant scrolling the whole rendering to find it. The Source diff already collapses unchanged lines around changes (Pierre, git-U3semantics); the rendered diff had no equivalent, so reviewers either hunted or switched to Source and lost the rendering.Approach
Keep the worker-isolated htmldiff merge exactly as it is and change only what is shown afterwards: parse the merged HTML once, classify each top-level block as changed when it is or contains
ins/del/[data-diff-node], and collapse runs of unchanged blocks with git hunk semantics — two blocks of context on each side of a change, leading/trailing runs keep context only where they touch a change, and a lone block is never hidden (an expander that replaces one paragraph saves nothing and costs a click). Each hidden run becomes one in-place expander bar:⇕ 10 unchanged blocks · § Section 2, the trailing§naming the last heading hidden in the run — the section the visible content below belongs to, the markdown analogue of Pierre's line-info hunk separators.Two decisions confirmed with the author before building:
ul/ol(4 unchanged items), because a spec here routinely carries a thirty-bullet list with one edited bullet; collapsing only top-level blocks would still show the whole list. Ordered items keep their original number viavalue, so hiding items never renumbers the rest. Tables, quotes, and nested lists render whole (deferred).Rejected alternatives: rendering source-line hunks as markdown fragments (a slice loses list/table/fence context and renders wrong), and hiding nodes with CSS after injecting the full HTML (fights React on every re-render). Visible blocks are re-created from the parsed elements (tag + attributes +
innerHTML) rather than wrapped, so the DOM thetr-prose-docdescendant styles target is unchanged; boolean attributes (details[open]) are mapped explicitly because React drops an empty-string boolean.A merge in which no block changed — front matter is stripped before rendering, whitespace and HTML comments don't render — now shows a
rendered-diff-emptynotice pointing at Source and collapses the document to a single expander, instead of presenting an unmarked full document as if it were a diff.Changes
apps/web/src/panels/renderedDiffFocus.ts(new):focusSegments(items, isChanged, context)— the DOM-free collapsing rule (FOCUS_CONTEXT_BLOCKS= 2, minimum two hidden), generic over the item type; unit-tested inrenderedDiffFocus.test.ts.apps/web/src/panels/RenderedDiff.tsx: parses the merged HTML (DOMParser), renders the prose root throughfocusSegments, recurses into changed lists, emitsrendered-diff-collapsedexpander buttons (one-way, component-local positional state so a live refresh keeps an expansion whose run still starts at the same position), and therendered-diff-emptynotice. Loading/error placeholders, token marks, and scroll-state restore are unchanged.apps/web/src/panels/SPEC.md: records the focus rule, expander semantics, the no-toggle decision, the empty case, and the re-create-not-wrap / boolean-attribute hazards in theRenderedDiffpassage.e2e/changes.spec.ts: two new tests — collapse + expand for blocks and list items (including a<details open>block surviving reconstruction), and the front-matter-only "preview is identical" case. Both use the Uncommitted scope, because "All changes vs main" treats a file committed only on the workspace branch as wholly added. The five existing rendered-diff tests needed no changes.No contract, store, or renderer-capability changes; the markdown renderer registration (
thinkrail/markdown) is untouched.Screenshots
Same scenario (28-block doc, one paragraph and one bullet edited, Uncommitted scope), same viewport.
Before — as opened: the whole document from the top; the change is three sections down.
Before — after scrolling to the change:
After — as opened: the changed paragraph with two blocks of context on each side, hidden runs as expander bars naming their count and section; the edited bullet further down gets the same treatment inside its list.
Checklist
bun run lint→ clean ·bun run typecheck→ 17/17 packages green viaturbo run typecheck --filter='!@thinkrail/desktop'; the@thinkrail/desktoptask could not complete locally because itselectrobun preparestep deadlocks on a shared~/.hutchlock in this environment (desktop is untouched by this PR — CI covers it) ·bun run test→ 18/18 tasks green (incl. the 6 newfocusSegmentstests)bun run e2e→ all 8 shards passed (467 tests); focusede2e/changes.spec.ts+e2e/live-refresh.spec.tsrendered-diff tests → 7 passed on the final head. Also run:bun run check:deps,check:boundaries,check:seams,check:spec-surface→ OK;apps/webcolour/spacing/typography usage guards → passSPEC.md/ top-level specs updated to reflect any boundary, contract, or behavior change