Skip to content

feat(markdown): split scroll sync and review comments on the preview - #394

Merged
simion merged 2 commits into
simion:mainfrom
wagoodman:feature/better-markdown-preview
Oct 12, 2026
Merged

simion merged 2 commits into
simion:mainfrom
wagoodman:feature/better-markdown-preview

Conversation

@wagoodman

Copy link
Copy Markdown
Contributor

Two additions to the markdown source / preview shell.

Split-view scroll sync:

  • every rendered block is stamped with the source lines it came from (data-src-line / data-src-end from a markdown-it core rule, on the <pre> for fences), which gives (line, y) anchors between the editor and the preview
  • either pane drives the other, interpolating between anchors so tall images and long paragraphs scroll smoothly instead of jumping
  • anchors are cached until a re-render or a ResizeObserver hit (late images, mermaid)
  • the echo of a programmatic scrollTop write is recognised by the value it lands on, so the panes never feed each other; the pane already on screen leads when split opens, and the first sync waits for attachHiddenScrollRestore so a restored position is not undone

Review comments on rendered text:

  • selecting anything in the preview raises a "Comment" pill that opens the same composer as the code side (send now, add to pending, edit, delete)
  • the comment goes into the same reviewComments store as an editor comment, so it also shows beside its source in the editor, follows its code through edits, and ships with the pending batch
  • the quote is exactly the selection; the line range starts at the innermost block at each end and is narrowed to the source lines the selection spans, matching with whitespace runs elastic since most markdown is hard-wrapped and the rendering joins those lines; when the selection is not in the source as typed (inline markdown), the locator stays the block's lines
  • in the preview the quoted text itself is highlighted with a <mark> (not the CSS Custom Highlight API, which mispaints in WKWebView), the open composer's included; the whole-block stripe is only the fallback when the text cannot be found in the rendering
  • the composer and card moved out of reviewCommentsExt into commentBoxes.ts (framework-free DOM, .tc-comment-* rules now in index.css) so both surfaces render the same boxes
  • in the preview they live in slot divs kept after the top-level block the range ends on, surviving innerHTML rebuilds; find, copy and selection skip them, and copy unwraps the quote highlight
  • documents cannot forge the source-line, slot or highlight markers: our line stamps carry a per-load nonce and claimDocument() strips the rest, for the render and each mermaid SVG
  • task files only; whole-file comments stay in the editor

Fixes the comment textarea growing 2px short of its text (border-box height set from scrollHeight, which excludes the borders). It overflowed into scrollbars, visible when zoomed, in the editor and diff composers too. It now scrolls only at its height cap.

Screenshots

Selecting text shows a new "comment" button:

Screenshot 2026-10-11 at 10 20 26 PM

Clicking "comment" has the familiar agent comment box:

Screenshot 2026-10-11 at 10 22 40 PM

Not great with a screenshot, but here is the split pane scrolled in the preview, which synced the scroll in the editor:

Screenshot 2026-10-11 at 10 22 58 PM

Two additions to the markdown source / preview shell.

Split-view scroll sync:
- every rendered block is stamped with the source lines it came from
  (`data-src-line` / `data-src-end` from a markdown-it core rule, on the
  `<pre>` for fences), which gives (line, y) anchors between the editor
  and the preview
- either pane drives the other, interpolating between anchors so tall
  images and long paragraphs scroll smoothly instead of jumping
- anchors are cached until a re-render or a ResizeObserver hit (late
  images, mermaid)
- the echo of a programmatic `scrollTop` write is recognised by the value
  it lands on, so the panes never feed each other; the pane already on
  screen leads when split opens, and the first sync waits for
  `attachHiddenScrollRestore` so a restored position is not undone

Review comments on rendered text:
- selecting anything in the preview raises a "Comment" pill that opens
  the same composer as the code side (send now, add to pending, edit,
  delete)
- the comment goes into the same `reviewComments` store as an editor
  comment, so it also shows beside its source in the editor, follows its
  code through edits, and ships with the pending batch
- the quote is exactly the selection; the line range starts at the
  innermost block at each end and is narrowed to the source lines the
  selection spans, matching with whitespace runs elastic since most
  markdown is hard-wrapped and the rendering joins those lines; when the
  selection is not in the source as typed (inline markdown), the locator
  stays the block's lines
- in the preview the quoted text itself is highlighted with a `<mark>`
  (not the CSS Custom Highlight API, which mispaints in WKWebView), the
  open composer's included; the whole-block stripe is only the fallback
  when the text cannot be found in the rendering
- the composer and card moved out of `reviewCommentsExt` into
  `commentBoxes.ts` (framework-free DOM, `.tc-comment-*` rules now in
  index.css) so both surfaces render the same boxes
- in the preview they live in slot divs kept after the top-level block
  the range ends on, surviving innerHTML rebuilds; find, copy and
  selection skip them, and copy unwraps the quote highlight
- documents cannot forge the source-line, slot or highlight markers: our
  line stamps carry a per-load nonce and `claimDocument()` strips the
  rest, for the render and each mermaid SVG
- task files only; whole-file comments stay in the editor

Fixes the comment textarea growing 2px short of its text (border-box
height set from `scrollHeight`, which excludes the borders). It
overflowed into scrollbars, visible when zoomed, in the editor and diff
composers too. It now scrolls only at its height cap.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
…er shows

On the macos-14 runner the first programmatic selection after the
scroll-sync cases never raised the pill, and the five cases after it
cascaded off that. It does not reproduce locally. Re-select while
waiting, and on timeout report the selection, active element and window
focus so a persistent failure names its cause.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

@simion simion left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Approved. Nice work on the scroll sync and preview comments, thanks.

@simion
simion merged commit 38cac4a into simion:main Oct 12, 2026
6 of 7 checks passed
simion added a commit that referenced this pull request Oct 12, 2026
Six preview comment cases failed in a full run and passed alone. The
split opens with the editor focused, and a focused CodeMirror writes
its caret back into the document whenever its viewport redraws, which
collapses a selection made in the preview. The scroll-sync cases leave
that viewport settling, so the Comment pill came up and was gone before
the next command.

A real drag starts with a mousedown that takes focus out of the editor,
so the app is fine; the spec was skipping that half of the gesture.
selectInParagraph blurs first, and the re-select retry loop goes.
Measured both ways and written up in gotchas.md.

Refs #394

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants