feat(desktop): render embeds, fences and alerts in assistant markdown

Wire the embeds module into the markdown surface: bare provider autolinks unfurl
to inline embeds, ```mermaid/```svg fences route to the rich renderers, and
`> [!NOTE]`-style blockquotes become alert callouts. Labeled links stay plain.
This commit is contained in:
Brooklyn Nicholson 2026-06-26 03:22:14 -05:00
parent 0c190083cd
commit e36d9862ec
2 changed files with 47 additions and 8 deletions

View file

@ -0,0 +1,5 @@
export { extractAlert, MarkdownAlert } from './alert'
export type { EmbedDescriptor } from './providers'
export { detectEmbed, isEmbeddableUrl } from './providers'
export { RICH_FENCE_LANGUAGES, RichCodeBlock } from './registry'
export { UrlEmbed } from './url-embed'

View file

@ -40,6 +40,8 @@ import { previewTargetFromMarkdownHref } from '@/lib/preview-targets'
import { tailBoundedRemend } from '@/lib/remend-tail'
import { cn } from '@/lib/utils'
import { detectEmbed, extractAlert, MarkdownAlert, RichCodeBlock, UrlEmbed } from './embeds'
// Math rendering plugin (KaTeX). Configured once at module scope — the
// plugin is stateless beyond its internal cache so re-creating per-render
// would needlessly thrash. We use a memoizing wrapper around rehype-katex
@ -270,6 +272,17 @@ function MarkdownLink({ children, className, href, ...props }: ComponentProps<'a
}
const text = childrenToText(children)
// Bare autolink → inline rich embed when a provider matches. Labeled links
// (`[watch](url)`) stay plain. Desktop only (webview / iframe renderers).
if (window.hermesDesktop && text && normalizeExternalUrl(text) === target) {
const embed = detectEmbed(target)
if (embed) {
return <UrlEmbed descriptor={embed} />
}
}
const fallbackLabel = text && normalizeExternalUrl(text) !== target ? text : undefined
return (
@ -535,13 +548,25 @@ function MarkdownTextSurface({ containerClassName, containerProps }: MarkdownTex
// owning per-line text direction. Inline code carries `dir="ltr"`
// (see the `code` override) so it doesn't vote here either, same
// contract as the CSS isolate.
blockquote: ({ className, ...props }: ComponentProps<'blockquote'>) => (
<blockquote
className={cn('border-s-2 border-border ps-3 text-muted-foreground italic', className)}
dir="auto"
{...props}
/>
),
// A `> [!NOTE]`/`[!WARNING]`/... blockquote renders as a GFM alert
// callout; everything else stays a plain quote.
blockquote: ({ children, className, ...props }: ComponentProps<'blockquote'>) => {
const alert = extractAlert(children)
if (alert) {
return <MarkdownAlert type={alert.type}>{alert.body}</MarkdownAlert>
}
return (
<blockquote
className={cn('border-s-2 border-border ps-3 text-muted-foreground italic', className)}
dir="auto"
{...props}
>
{children}
</blockquote>
)
},
ul: ({ className, ...props }: ComponentProps<'ul'>) => (
<ul className={cn('my-1 gap-0', className)} dir="auto" {...props} />
),
@ -578,7 +603,16 @@ function MarkdownTextSurface({ containerClassName, containerProps }: MarkdownTex
<td className={cn('px-2.5 py-1.5 align-top text-[0.8125rem] leading-snug', className)} {...props} />
),
img: MarkdownImage,
SyntaxHighlighter: (props: SyntaxHighlighterProps) => <SyntaxHighlighter {...props} defer={isStreaming} />
// ```mermaid / ```svg fences route to their lazy renderers; every other
// language falls back to the Shiki-highlighted code block.
SyntaxHighlighter: (props: SyntaxHighlighterProps) => (
<RichCodeBlock
code={props.code}
fallback={<SyntaxHighlighter {...props} defer={isStreaming} />}
language={props.language}
streaming={isStreaming}
/>
)
}) as StreamdownTextComponents,
[isStreaming]
)