Skip to content

Render chat markdown through MarkView.Avalonia - #834

Merged
alexeyzimarev merged 3 commits into
mainfrom
feat/markview-renderer
Sep 9, 2026
Merged

alexeyzimarev merged 3 commits into
mainfrom
feat/markview-renderer

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

No GitHub issue and no Linear id: the work started as a spike in chat, so neither half of the reference exists.

What & why

The desktop chat rendered markdown through a hand-written Markdig-to-controls mapper that covered only the constructs agents emit and carried its own layout workarounds. This swaps it for MarkView.Avalonia behind the unchanged MarkdownView surface, adds diff and code highlighting through its TextMate package, and renders user turns as markdown too. The library's defaults are wrapped rather than trusted: KcapMarkdownExtension keeps every link under LinkPolicy, turns a bare URL into an inline link instead of MarkView's self-navigating button, shows an image as its source text with no fetch, and shows HTML instead of dropping it.

Where to look

MarkdownView holds one static TextMateExtension: the extension caches its highlighters per instance, and a per-view instance rebuilds them on every render. Paragraphs carry a transparent background so a click reaches the viewer's own handlers. Links are inline spans now, so they no longer take keyboard focus.

Verification

  • Capacitor.App.Tests.Unit on this tree: 1584 passed, 0 failed, nine of them the new MarkdownViewTests (link click through the command, refused scheme and bare URL as plain text, diff colouring, theming).
  • Render cost per message, 20 warm renders each:
Document shared highlighter per-view highlighter old renderer
two code blocks 1.0 ms 25 ms 1.7 ms
prose only 0.05 ms 13 ms 0.1 ms
  • Debug build run on macOS: connects, chat renders, nothing in the log.

MarkView's link, autolink and HTML renderers are replaced so a link exists only where LinkPolicy would open it and nothing agent-authored is fetched. One TextMate extension serves every view: a per-view instance rebuilds its highlighters on each render.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Render chat markdown with MarkView.Avalonia

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Replaces custom chat markdown rendering with MarkView.Avalonia and shared TextMate highlighting.
• Preserves link, image, and HTML safety policies through custom renderers.
• Renders user turns as markdown and verifies styling, interactions, and syntax highlighting.
Diagram

graph TD
  A["Chat turns"] --> B["MarkdownView"] --> C["MarkView viewer"] --> D["Kcap extension"] --> E["Link policy"]
  C --> F["TextMate highlighting"]
  C --> G["Markdown styles"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Retain the custom Markdig renderer
  • ➕ Keeps complete control over generated Avalonia controls.
  • ➕ Avoids adding two third-party package dependencies.
  • ➖ Requires maintaining markdown coverage and layout workarounds in-house.
  • ➖ Needs custom syntax and diff highlighting.
  • ➖ Continues duplicating behavior already supported by MarkView.
2. Use another Avalonia markdown control
  • ➕ Could reduce integration code if its defaults matched application policy.
  • ➕ May provide a different API optimized for streaming content.
  • ➖ Markdown.Avalonia's Avalonia 12 support is alpha and uses another parser.
  • ➖ LiveMarkdown targets append-only streaming rather than immutable chat items.
  • ➖ Neither alternative aligns as closely with the existing Markdig-based behavior.

Recommendation: Use MarkView.Avalonia with the application-specific extension, as implemented. It removes a large custom renderer while preserving the established trust boundary, and the shared TextMate extension avoids the significant per-view initialization cost. The retained MarkdownView facade also limits migration impact.

Files changed (10) +478 / -9

Enhancement (3) +170 / -2
ChatTabView.axamlRender user turns as markdown +2/-2

Render user turns as markdown

• Replaces the user-turn SelectableTextBlock with MarkdownView and routes approved link clicks through the existing command.

src/Capacitor.App/Views/ChatTabView.axaml

KcapMarkdownExtension.csEnforce application markdown content policy +90/-0

Enforce application markdown content policy

• Overrides MarkView renderers so only policy-approved links are interactive, images remain source text, and HTML remains visible. It disables all image loaders to prevent agent-authored network fetches.

src/Capacitor.App/Views/KcapMarkdownExtension.cs

MarkdownStyles.axamlApply scoped application styling to MarkView +78/-0

Apply scoped application styling to MarkView

• Styles paragraphs, headings, code, quotes, tables, rules, and links with the application palette. Transparent text backgrounds preserve pointer handling across whitespace.

src/Capacitor.App/Views/MarkdownStyles.axaml

Refactor (1) +25 / -4
MarkdownView.csReplace custom rendering with MarkView +25/-4

Replace custom rendering with MarkView

• Hosts MarkdownViewer behind the existing MarkdownView API, shares TextMate highlighters across views, and forwards handled link events to OpenLink. Nested viewer scrolling is disabled in favor of the surrounding chat list.

src/Capacitor.App/Views/MarkdownView.cs

Tests (2) +258 / -3
ChatTabViewSmokeTests.csAdapt chat smoke tests to MarkView controls +23/-3

Adapt chat smoke tests to MarkView controls

• Updates link interaction and system-note queries for MarkView's inline structure. Adds coverage proving user turns render markdown emphasis.

test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs

MarkdownViewTests.csVerify MarkView rendering and policy integration +235/-0

Verify MarkView rendering and policy integration

• Covers markdown blocks, command-routed links, refused schemes, autolinks, images, HTML, tables, and line breaks. It also verifies diff highlighting and application theme propagation.

test/Capacitor.App.Tests.Unit/MarkdownViewTests.cs

Documentation (1) +19 / -0
CHANGES.mdDocument the MarkView renderer decision +19/-0

Document the MarkView renderer decision

• Records why MarkView was selected, how unsafe content degrades, and why the TextMate extension is shared. It also documents styling and pointer hit-testing constraints.

docs/CHANGES.md

Other (3) +6 / -0
Directory.Packages.propsPin MarkView rendering packages +2/-0

Pin MarkView rendering packages

• Adds centrally managed versions for MarkView.Avalonia and its syntax-highlighting package.

Directory.Packages.props

App.axamlLoad MarkView and application markdown themes +2/-0

Load MarkView and application markdown themes

• Registers MarkView's base theme and the application-scoped markdown overrides globally.

src/Capacitor.App/App.axaml

Capacitor.App.csprojReference MarkView rendering dependencies +2/-0

Reference MarkView rendering dependencies

• Adds the MarkView.Avalonia renderer and TextMate syntax-highlighting packages to the desktop application.

src/Capacitor.App/Capacitor.App.csproj

@qodo-code-review

qodo-code-review Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Keyboard users cannot open links ✗ Dismissed 🐞 Bug ≡ Correctness
Description
PolicyLinkRenderer and PolicyAutolinkRenderer emit MarkdownHyperlink document spans instead of
focusable controls, while MarkdownView only forwards the viewer's pointer-driven LinkClicked
event. Because rendered markdown now contains no button descendants, keyboard-only users cannot tab
to or press Enter on links in chat messages or pull-request content.
Code

src/Capacitor.App/Views/KcapMarkdownExtension.cs[50]

+            var link = Hyperlink(obj.Url!);
Relevance

●●● Strong

Clear accessibility regression: replacing buttons with inline spans removes keyboard focus and Enter
activation.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both allowed-link renderers construct inline MarkdownHyperlink objects, and the view handles only
LinkClicked. The new test confirms there are no button controls and validates only mouse
activation, so the former focus-and-Enter route is absent across every MarkdownView call site.

src/Capacitor.App/Views/KcapMarkdownExtension.cs[25-28]
src/Capacitor.App/Views/KcapMarkdownExtension.cs[40-66]
src/Capacitor.App/Views/MarkdownView.cs[35-38]
test/Capacitor.App.Tests.Unit/MarkdownViewTests.cs[70-90]
src/Capacitor.App/Views/PullRequestReader.axaml[36-36]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Markdown links are now non-focusable inline spans, removing the previous keyboard activation path. Restore a focusable link representation or provide equivalent keyboard navigation and Enter/Space activation while retaining `LinkPolicy` enforcement.

## Issue Context
The old renderer represented allowed links as focusable buttons and tested Enter-key activation. The replacement tests assert that no buttons remain and only exercise pointer activation, while the same renderer is used for chat and pull-request markdown.

## Fix Focus Areas
- src/Capacitor.App/Views/KcapMarkdownExtension.cs[40-67]
- src/Capacitor.App/Views/MarkdownView.cs[35-38]
- test/Capacitor.App.Tests.Unit/MarkdownViewTests.cs[70-90]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. One test comment repeats its method ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
A_user_turn_renders_as_markdown is preceded by an XML comment that merely restates the behavior
already named by the method. Because the assertion directly demonstrates the same fact, the comment
documents no additional constraint and can become stale when the test changes.
Code

test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[479]

+    /// Pins that a user turn is rendered as markdown, like the assistant's text.
Relevance

●●● Strong

Recent repository precedent accepts removing test comments that merely restate adjacent assertions
or method behavior.

PR-#831
PR-#703

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2762993 requires comments to document non-obvious, behavior-critical constraints
and explicitly rejects comments that merely restate code. The added comment only paraphrases the
immediately following test method.

Rule 2762993: Restrict comments to documenting non-obvious, behavior‑critical constraints
test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[479-482]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Remove the XML comment that only repeats the test method's purpose.

## Issue Context
The method name and assertions already establish that user turns render as markdown, while the comment adds no non-obvious constraint or rationale.

## Fix Focus Areas
- test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[479-479]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 61 rules
✅ Cross-repo context — repo relationships
Review mode: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 16/18, lines 988/200; both must reach the floor). Router rationale: This substantial UI rendering replacement spans parsing, link/security policy, theming, syntax highlighting, interaction behavior, dependency integration, and multiple code paths with many independent opportunities for subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can commit Qodo's fix in one click with committable suggestions (GitHub & GitLab)

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3170590b6a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

var text = new MarkdownSelectableTextBlock { TextWrapping = TextWrapping.Wrap };
text.Classes.Add("markdown-paragraph");
renderer.Push(text.Inlines!);
renderer.WriteLeafRawLines(obj);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep multiline HTML out of inline collections

When an agent response or PR body contains a multiline HTML block such as <div>\ntext\n</div>, WriteLeafRawLines writes those embedded newlines into this InlineCollection. The renderer removed by this commit explicitly avoided that shape because Avalonia 12's line breaker does not finish laying out inline text containing newlines under the unconstrained StackPanel/ScrollViewer used by these views, so such content can hang the desktop UI. Render the raw block through the text block's Text property (or split it into newline-free runs/line breaks) instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

WriteLeafRawLines emits one Run per source line with a LineBreak inline between them, so no run carries a newline; the hang the old renderer worked around was a newline inside an inline's text. Pinned by Multi_line_html_and_hard_breaks_lay_out_under_an_unconstrained_parent in 9fbd932: a multi-line HTML block and a hard break inside ScrollViewer > StackPanel lay out under a 30 s timeout, and the assertion checks no run contains a newline.

Comment thread test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs Outdated
Comment thread src/Capacitor.App/Views/KcapMarkdownExtension.cs
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-09T13:48:48.977912Z 3170590 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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.

1 participant