You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Add pull request comments to the markdown preview - #9074
Reviewers can now comment on markdown files while reading them rendered in the built-in markdown preview. This includes the side-by-side rendered diff, so they don't have to switch to the raw source first.
Hovering over a block that's part of the diff (a paragraph, heading, list item, table row or code block) outlines it and shows a + in the left margin.
A block that already has comment threads shows a comment-count badge in the right margin.
Clicking either one opens the source beside the preview with the regular comment widget on that block's lines, limited to the diff hunk. Clicking a badge expands the existing thread instead.
Comments go through the existing comment controllers, so pending reviews, single comments and suggestions work the same as in the editor.
It works for:
files of the checked-out PR (file), and the base side of its diff (review)
both sides of the diff of a PR that isn't checked out (pr)
How
The extension can't add UI to the markdown preview webview directly, so this uses the markdown extension's contribution points:
markdown.markdownItPlugins.extendMarkdownIt, exported from activate, does two things:
It adds a data-pr-end-line attribute to every block. The preview already has data-line, so the script knows each block's line span.
At render time it adds a hidden element with the commenting ranges and thread locations of PR files, keyed by document URI. This happens at render time because the markdown extension caches tokens.
When the rendered diff highlights changes within lines, it renders a marked-up copy of the text, so markdown-it isn't told which document it's rendering. In that case the data for all open markdown documents is rendered, and the preview script picks its own document using the source in the preview's settings.
The data is computed asynchronously and cached. When it changes, for example after a thread is added or after local edits move the ranges, previews are refreshed with markdown.preview.refresh.
markdown.previewScripts.webviews/markdownPreviewComments draws the + buttons and badges in an overlay, so the rendered content's layout is left alone.
URI handler. Preview scripts can't post messages to their extension, so the buttons are vscode://github.vscode-pull-request-github/comment-on-markdown-preview?document=<id>&start=&end= links. The markdown preview lets vscode: links through. The id is opaque and only issued for documents that have been rendered in a preview, so outside links can't target arbitrary files. The handler limits the lines to the diff hunk and runs workbench.action.addComment with that range.
ReviewCommentController gets getMarkdownPreviewCommentInfo. ReviewCommentController, ReviewManager and ReviewsManager get an onDidChangeCommentThreads event, so previews refresh when threads change. For PRs that aren't checked out, the data comes from the PR's commenting range provider and reviewThreadsCache.
Known limitations / open questions
One-time confirmation. The first click shows VS Code's "Allow 'GitHub Pull Requests' extension to open this URI?" dialog, because webview links aren't opened as trusted. Checking "Don't ask again" removes it. A cleaner long-term fix would be a markdown preview API that lets contributed preview scripts message their own extension. I'm happy to open a VS Code issue for that.
Desktop only. On vscode.dev, webviews don't forward vscode: links (MainThreadWebviews.isSupportedLink).
Not in the new Markdown Editor. The Markdown Editor (vscode.markdown.editor) doesn't load contributed preview scripts or markdown-it plugins, so it would need support in VS Code itself.
Comments are still written in the editor's comment widget, not inline in the preview. This keeps the change small and reuses the existing comment UI.
the preview data for checked-out files and for both sides of a PR that isn't checked out
rendering when markdown-it isn't told the document
opening the comment widget limited to the diff hunk, and rejecting lines outside the diff
webviews/markdownPreviewComments/test/overlay.test.ts covers which document's data the preview script uses, plus its comment links and thread badges.
npm run lint, npm run hygiene, npm run test:webviews (83 passing) and the extension tests (1055 passing) all pass.
Manually tested on VS Code 1.142 by commenting from the rendered diff of real PRs that change markdown files, both when the source file was already open and when it wasn't. Because of the engine version, this used a build of this change on top of v0.170.0.
Lets reviewers comment on markdown files while reading them rendered in the
built-in markdown preview, including the side-by-side rendered diff.
- A markdown-it plugin (`markdown.markdownItPlugins`) records the line span
of each rendered block, and renders the commenting ranges and comment
threads of pull request files into the preview, keyed by document URI.
The rendered diff renders a copy of the text when it highlights changes
within lines, so the preview script picks its document from the
preview's settings.
- A preview script (`markdown.previewScripts`) shows a "+" next to blocks
that are part of the diff, and a comment count next to blocks with
threads.
- Clicking either opens the source beside the preview with the comment
widget on the block's lines, clamped to the diff hunk, through a new
`/comment-on-markdown-preview` URI handler path.
Works for files of the checked out pull request (`file` and the `review`
base side) and for pull requests that aren't checked out (`pr`).
Fixesmicrosoft#8607
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One or more custom setup steps configured for this repository failed during this Copilot code review run:
Install dependencies
Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review.
Note
You can configure setup steps for Copilot code review separately from Copilot cloud agent with a copilot-code-review.yml file. Read the docs for details.
- Only update open documents when comment threads change, and forget
closed documents.
- Prefer the deepest repository for nested repositories and submodules.
- For `pr` documents, find the repository that contains the file before
matching the remote name and pull request number, which aren't unique
across repositories.
- Look up the base side threads of a renamed file by its current file name.
- Keep the add-comment links focusable, and reveal them and their block on
keyboard focus.
- Give each thread of a block its own badge, so threads in different diff
hunks can each be revealed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One or more custom setup steps configured for this repository failed during this Copilot code review run:
Install dependencies
Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review.
Note
You can configure setup steps for Copilot code review separately from Copilot cloud agent with a copilot-code-review.yml file. Read the docs for details.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Incorrect ranges for hidden unsaved documents, missing nested-list controls, and misleading historical-preview badges remain unresolved.
No open issues 🎉
3 previously missed issues
In code that hasn't changed since last review
Use dirty workspace documents for hidden preview diff ranges
src/view/reviewCommentController.ts:587
With Auto Save disabled, inserting lines before a diff hunk and then hiding the source editor behind the preview produces incorrect commenting ranges. This call reaches getContentDiff, which reads dirty text only from visibleTextEditors; otherwise it diffs the saved file. The preview renders the unsaved buffer, so controls appear on the wrong blocks and valid clicks can be rejected or target incorrect lines. Look up the document in workspace.textDocuments instead of requiring a visible editor, and cover a hidden dirty document in a regression test.
Avoid current thread maps for historical review documents
src/view/reviewCommentController.ts:595
A historical review document, such as one opened through pr.diffOutdatedCommentWithHead, can fail the current file-change match because its commit differs. Falling back to reviewQuery.path still loads current-version threads, displaying badges at unrelated lines in the historical content. Clicking them is rejected because that document has no commenting ranges. Only read these current-version thread maps when the review document matches a current file change.
Preserve parent-line targets in nested list rendering
webviews/markdownPreviewComments/overlay.ts:109
For a tight nested list such as - parent followed by - child, the parent text is rendered directly inside its <li> without a paragraph element. This check discards that <li> because it contains nested blocks, leaving only the child's lines represented. Changes and threads on the parent line therefore get neither an add-comment button nor a badge. Preserve a target for the parent's own source lines without duplicating descendant controls, and add a test for this rendered shape.
… preview
When the source of a previewed file wasn't open yet, `workbench.action.addComment`
ran before VS Code had the commenting ranges of the new editor, and failed with
"The cursor must be within a commenting range to add a comment." Wait until the
commenting range provider has given VS Code the document's ranges first.
The commenting range providers fire `onDidProvideCommentingRanges`, and the
preview gets ranges through `getCommentingRanges` so that its own requests
don't fire it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e available
A preview rendered before the pull request's commenting range provider was
registered cached empty commenting ranges and never showed the add-comment
links. Update the open previews whenever a commenting range provider is
registered or removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
- Diff the unsaved changes of documents that aren't visible, such as a document
whose markdown preview is shown instead of its text editor, so that commenting
ranges match the rendered text.
- Only show the current threads on review documents of the current file changes,
not on other versions of a file.
- Add comment targets for the own lines of blocks that contain other blocks,
such as the text of a list item in a tight nested list, and prefer the
innermost block when hovering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addressed the three "previously missed issues" from the second Copilot review in 21bd4a8:
Use dirty workspace documents for hidden preview diff ranges:getContentDiff now finds the document in workspace.textDocuments instead of visibleTextEditors, so unsaved changes count even when the source editor is hidden behind the preview. Covered by diffs the unsaved changes of a document that is not visible.
Avoid current thread maps for historical review documents:getMarkdownPreviewCommentInfo only reads the thread maps for review documents that match a current file change, and otherwise returns no threads. Covered by does not show the current threads on other versions of a file.
Preserve parent-line targets in nested list rendering: a block that contains other blocks now gets a target for its own lines before them, like the parent text of a tight nested list item. Children keep their own targets, and hover prefers the innermost block. Covered by adds comment links to the text of list items that contain other lists.
Each test fails without its fix. Extension tests: 1048 passing. Webview tests: 81 passing.
Two more fixes came from recording the demo:
ecb59b6 waits until a newly opened editor has its commenting ranges before running workbench.action.addComment. Before, the first click failed with "The cursor must be within a commenting range".
a247aa2 refreshes previews when a PR's commenting range provider is registered.
- Require the start and end lines in comment links from the preview, since
`Number(null)` is 0.
- Type `PRCommentControllerRegistry` as a `CommentingRangeProvider2`, since its
providers can return an object with the ranges, which is needed for file
comments.
- Cancel the scheduled update of a closed document.
- Dispose the cancellation token source used to get commenting ranges.
- Measure the blocks with comment links when rendering instead of on every
pointer move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Comment actions can target the wrong editor, and thread badges can be rejected or use stale buffer locations.
No open issues 🎉
3 previously missed issues
In code that hasn't changed since last review
Allow badges outside commenting ranges to reveal existing threads
src/view/markdownPreviewComments.ts:190
Thread badges can appear outside commentingRanges, but this guard rejects their clicks instead of revealing the thread. For example, when viewing changes since the last review, the provider returns only newer hunks while getPullRequestInfo includes current threads from the whole PR. A badge for an earlier hunk in the same file therefore only shows a warning. Give badge links an existing-thread target and reveal that thread separately from the range validation used to add new comments.
Abort comment commands when the active editor changes
src/view/markdownPreviewComments.ts:203
If the user switches editors while commenting ranges are loading (up to three seconds), workbench.action.addComment runs in the newly active editor using the original document's lines. Its arguments contain no document URI. After the wait, abort if the editor returned by showTextDocument is no longer active.
Map workspace threads against the current unsaved buffer
src/view/reviewCommentController.ts:616
If workspace threads are initialized while the Markdown buffer already has unsaved edits, these cached ranges can point to the wrong blocks. createWorkspaceCommentThread maps thread locations using the on-disk diffWithHEAD, but getCommentingRanges accounts for the unsaved buffer. Inserting unsaved lines above a thread can therefore place its badge on another paragraph and give it the wrong link target. Compute preview thread locations from the current buffer too, starting from the original PR coordinates so existing working-tree adjustments are not applied twice.
One or more custom setup steps configured for this repository failed during this Copilot code review run:
Install dependencies
Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review.
Note
You can configure setup steps for Copilot code review separately from Copilot cloud agent with a copilot-code-review.yml file. Read the docs for details.
- Thread badges reveal their thread with `workbench.action.focusCommentOnCurrentLine`
instead of adding a comment, so that threads outside of the commenting ranges,
such as when viewing the changes since the last review, can be opened.
- Don't run comment commands when the active editor changed while waiting for
the commenting ranges, since they act on the active editor.
- Place the threads of workspace files by the current text, mapping their pull
request lines through the diff that includes unsaved changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addressed the three "previously missed issues" from the latest Copilot review in 4e16322:
Allow badges outside commenting ranges to reveal existing threads: thread badges now link with thread=1. The handler skips the commenting-range check for them, puts the cursor on the thread's last line (where threads are anchored), and runs workbench.action.focusCommentOnCurrentLine, which expands and focuses the thread without toggling it. Covered by reveals an existing thread, even outside of the commenting ranges.
Abort comment commands when the active editor changes: after waiting for the commenting ranges, nothing runs if the file is no longer the active editor's document. Covered by does not open the comment widget when the active editor changed while opening the document.
Map workspace threads against the current unsaved buffer: for workspace files, the preview now places threads by mapping their pull request lines (startLine/endLine from reviewThreadsCache) through getContentDiff, which includes unsaved changes. This starts from the PR coordinates, so the diffWithHEAD adjustment isn't applied twice. Covered by places the threads of a workspace file by its current text, including unsaved changes.
The second and third tests fail without their fixes. The first covers new behavior. Extension tests: 1053 passing. Webview tests: 82 passing.
The "Install dependencies" setup-step failure Copilot reported comes from the repository's Copilot code review configuration, not from this PR.
One or more custom setup steps configured for this repository failed during this Copilot code review run:
Install dependencies
Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review.
Note
You can configure setup steps for Copilot code review separately from Copilot cloud agent with a copilot-code-review.yml file. Read the docs for details.
Several threads can end on the same line, and
`workbench.action.focusCommentOnCurrentLine` reveals the first one. Thread
badges now link with the GitHub thread id, and the thread is revealed with its
`reveal()` API, through the new `getCommentThread` lookups of the comment
controllers. The command is still used if the thread can't be found.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The new URI entry point and asynchronous preview lifecycle need human validation, with an unresolved update-tracking issue.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
If VS Code closes the source document while its preview stays visible, forgetDocument() removes…
src/view/markdownPreviewComments.ts:186
If VS Code closes the source document while its preview stays visible, forgetDocument() removes its _info entry. Clicking a preview link reopens the source here but does not restore tracking. Subsequent thread-change events therefore skip it in updateAll(), leaving badges stale until another render. Restore the entry and schedule an update when reopening through a preview link; this preserves the existing restriction against updating closed historical documents.
One or more custom setup steps configured for this repository failed during this Copilot code review run:
Install dependencies
Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review.
Note
You can configure setup steps for Copilot code review separately from Copilot cloud agent with a copilot-code-review.yml file. Read the docs for details.
Addressed the "previously missed issue" from the latest Copilot review in 80307d1:
Restore tracking when a preview link reopens a closed document:commentOnLines now starts tracking the document again when it isn't tracked, and schedules an update. Later thread changes then refresh its preview again. Documents that are closed and not reopened from a preview are still left alone. Covered by tracks a closed document again when a link in its preview reopens it, which fails without the fix.
VS Code can close a document while its preview is still shown, which stops its
comment information from being updated. When a link in the preview reopens the
document, track it again and schedule an update.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One or more custom setup steps configured for this repository failed during this Copilot code review run:
Install dependencies
Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review.
Note
You can configure setup steps for Copilot code review separately from Copilot cloud agent with a copilot-code-review.yml file. Read the docs for details.
One or more custom setup steps configured for this repository failed during this Copilot code review run:
Install dependencies
Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review.
Note
You can configure setup steps for Copilot code review separately from Copilot cloud agent with a copilot-code-review.yml file. Read the docs for details.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Cross-extension rendering and editor timing require human validation, and keyboard focus preservation remains unresolved.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Preserve comment link focus across layout renders
webviews/markdownPreviewComments/overlay.ts:157
Every render removes the focused comment link, including layout-only renders triggered by resizing or an image loading (index.ts:33–35). The replacement link never receives focus, so a keyboard user can no longer activate it with Enter. Before removing the overlay, capture the focused link's href if focus is inside it. After rebuilding, focus the matching replacement with { preventScroll: true }. Add coverage for both add links and thread badges retaining focus after another render.
This is ready for a maintainer review. All review feedback so far has been addressed and the threads are resolved, the branch is up to date with main, and there's a short demo video in the description. Happy to make changes to the approach. In particular, I'm open to feedback on the one-time "Allow ... to open this URI?" prompt and on the feature being desktop-only, which are covered under "Known limitations" in the description.
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
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.
markdown-preview-comments-demo.mp4
Fixes #8607
What
Reviewers can now comment on markdown files while reading them rendered in the built-in markdown preview. This includes the side-by-side rendered diff, so they don't have to switch to the raw source first.
Comments go through the existing comment controllers, so pending reviews, single comments and suggestions work the same as in the editor.
It works for:
file), and the base side of its diff (review)pr)How
The extension can't add UI to the markdown preview webview directly, so this uses the markdown extension's contribution points:
markdown.markdownItPlugins.extendMarkdownIt, exported fromactivate, does two things:data-pr-end-lineattribute to every block. The preview already hasdata-line, so the script knows each block's line span.When the rendered diff highlights changes within lines, it renders a marked-up copy of the text, so markdown-it isn't told which document it's rendering. In that case the data for all open markdown documents is rendered, and the preview script picks its own document using the
sourcein the preview's settings.The data is computed asynchronously and cached. When it changes, for example after a thread is added or after local edits move the ranges, previews are refreshed with
markdown.preview.refresh.markdown.previewScripts.webviews/markdownPreviewCommentsdraws the + buttons and badges in an overlay, so the rendered content's layout is left alone.URI handler. Preview scripts can't post messages to their extension, so the buttons are
vscode://github.vscode-pull-request-github/comment-on-markdown-preview?document=<id>&start=&end=links. The markdown preview letsvscode:links through. Theidis opaque and only issued for documents that have been rendered in a preview, so outside links can't target arbitrary files. The handler limits the lines to the diff hunk and runsworkbench.action.addCommentwith that range.ReviewCommentControllergetsgetMarkdownPreviewCommentInfo.ReviewCommentController,ReviewManagerandReviewsManagerget anonDidChangeCommentThreadsevent, so previews refresh when threads change. For PRs that aren't checked out, the data comes from the PR's commenting range provider andreviewThreadsCache.Known limitations / open questions
vscode:links (MainThreadWebviews.isSupportedLink).vscode.markdown.editor) doesn't load contributed preview scripts or markdown-it plugins, so it would need support in VS Code itself.Testing
src/test/view/markdownPreviewComments.test.tscovers:webviews/markdownPreviewComments/test/overlay.test.tscovers which document's data the preview script uses, plus its comment links and thread badges.npm run lint,npm run hygiene,npm run test:webviews(83 passing) and the extension tests (1055 passing) all pass.🤖 Generated with Claude Code