feat(markdown): shared Markdown renderer - #2664
Open
camielvs wants to merge 2 commits into
Open
Conversation
Collaborator
Author
This stack of pull requests is managed by Graphite. Learn more about stacking. |
This was referenced Aug 26, 2026
🎩 PreviewA preview build has been created at: |
camielvs
commented
Aug 26, 2026
camielvs
commented
Aug 26, 2026
camielvs
commented
Aug 26, 2026
camielvs
commented
Aug 26, 2026
camielvs
commented
Aug 26, 2026
camielvs
commented
Aug 26, 2026
camielvs
force-pushed
the
banners-01-markdown
branch
from
August 27, 2026 01:18
47a18de to
b525bf9
Compare
camielvs
marked this pull request as ready for review
August 28, 2026 00:02
Extracts the markdown rendering AiChat had built for itself into a shared `Markdown` component, and adds an `UntrustedMarkdown` variant for text the app did not author. AiChat now renders through the shared base, keeping only its own overrides (entity/component chips, fenced code blocks).
- Fenced blocks no longer inherit the inline-code pill; `pre` owns the block surround for labelled and unlabelled fences alike. - Links render through `Link` rather than a raw anchor. That needed one fix in the primitive: it wrapped its children in a `div`, which is not allowed inside a `<p>` and which React flagged on every render — AiChat already renders `Link` inside markdown today. A `span` fixes it with no visual change, since the layout comes from the flex classes. - Internal links stay in the same tab and gain no external affordance; only absolute http(s) hrefs get `target="_blank"` and the icon. - Restore `last:border-b-0` on table rows. - `UntrustedMarkdown` locks `img` after the caller spread so the guard cannot be overridden. - `chatComponents` regains its `satisfies Components` check. - Add `toAbsoluteHttpUrl` unit tests covering the protocol filtering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
camielvs
force-pushed
the
banners-01-markdown
branch
from
September 3, 2026 18:50
b525bf9 to
adef9bb
Compare
morgan-wowk
approved these changes
Sep 3, 2026
morgan-wowk
left a comment
There was a problem hiding this comment.
🤖 Automated review
Approving. This is an exemplary trust boundary. Verified against the code and tests:
- Neither entry point loads
rehype-raw, so raw HTML never renders — the "does not render raw HTML" test confirms a<script>body doesn't execute and<b>doesn't render. UntrustedMarkdownlocksurlTransformtotoAbsoluteHttpUrl, sojavascript:/data:/relative hrefs collapse to""and render as plain text (not a dead anchor);imgis forced to alt-text-only and can't be re-enabled by a caller (tested).URL.ts'stoAbsoluteHttpUrlrejects non-string host input, non-http(s) protocols, and protocol-relative/relative/anchor URLs — all directly unit-tested.- The
link.tsxdiv→spanfix is correct (adivis invalid inside<p>), and the new dir is added to the react-compiler allowlist alongside its peers.
Clean.
Mbeaulne
approved these changes
Sep 3, 2026
Mbeaulne
left a comment
Collaborator
There was a problem hiding this comment.
Reviewed the shared renderer and its trusted/untrusted URL handling. No additional findings.
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.

Why
AiChathad built a full markdown renderer for itself. The notices feature later in this stack needs to render markdown too — and from a source outside the app, so it needs stricter handling. Rather than keep a second copy, this lifts AiChat's renderer into a shared component.What you get
Two entry points:
Markdown— for text the app authored. The same styling AiChat had, now in one place.UntrustedMarkdown— for text the app did not author. Raw HTML never renders, links are narrowed to absolutehttp(s)URLs (so nothing can point into our own routes or runjavascript:), and images degrade to their alt text instead of fetching a remote file.AiChat keeps only its own overrides: entity and component chips, and syntax-highlighted fenced code blocks.
Links render through the
Linkprimitive rather than a raw anchor, so an internal href stays in the same tab and only absolutehttp(s)getstarget="_blank"and the external icon. That needed one fix in the primitive: it wrapped its children in adiv, which is invalid inside a<p>and which React flagged on every render — AiChat already rendersLinkinside markdown today. Aspanfixes it with no visual change.Reviewer notes
This is the one PR in the stack that changes something you can already see. AiChat's markdown picks up the shared styling, which differs slightly from what it had:
h1h2h3h4h5h6- [ ])These are the shared component's choices; worth a glance to confirm they're an improvement and not a surprise. An earlier revision of this PR also dropped
last:border-b-0from table rows — that was an accident in the extraction and is restored.Tests: both entry points, with the untrusted path's escapes (script tags,
javascript:links, relative URLs, remote images) asserted directly, plus unit tests for the URL guard.Where this sits
This one stands alone: it is useful as a de-duplication whether or not anything above it lands.