Skip to main content

ui/components/ai/
agent_markdown.rs

1use gpui::Entity;
2use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownFontConfig, MarkdownStyle};
3
4use crate::prelude::*;
5
6/// Builds the font configuration `boltz-markdown` needs to size/family its
7/// text, sourced from this workspace's `theme::theme_settings` global (no
8/// per-crate settings global exists here — see `code_editor.rs`'s
9/// `CODE_FONT_FAMILY` module docs for the established stateless-config
10/// alternative). Agent/preview font sizes reuse the same UI/buffer sizes,
11/// since this workspace has no distinct agent-panel-specific size setting.
12pub fn agent_markdown_font_config(cx: &App) -> MarkdownFontConfig {
13    let settings = theme::theme_settings(cx);
14    let ui_font = settings.ui_font(cx).clone();
15    let buffer_font = settings.buffer_font(cx).clone();
16    let ui_font_size = settings.ui_font_size(cx);
17    let buffer_font_size = settings.buffer_font_size(cx);
18
19    MarkdownFontConfig {
20        ui_font_family: ui_font.family.clone(),
21        ui_font_fallbacks: ui_font.fallbacks.clone(),
22        ui_font_features: ui_font.features.clone(),
23        ui_font_size,
24        buffer_font_family: buffer_font.family.clone(),
25        buffer_font_fallbacks: buffer_font.fallbacks.clone(),
26        buffer_font_features: buffer_font.features.clone(),
27        buffer_font_weight: buffer_font.weight,
28        buffer_font_size,
29        agent_buffer_font_size: buffer_font_size,
30        agent_ui_font_size: ui_font_size,
31        markdown_preview_font_size: ui_font_size,
32        markdown_preview_font_family: ui_font.family,
33        markdown_preview_code_font_family: buffer_font.family,
34    }
35}
36
37/// The [`MarkdownStyle`] used to render agent chat markdown bodies
38/// (messages, thinking blocks, tool-call text content).
39pub fn agent_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
40    let fonts = agent_markdown_font_config(cx);
41    MarkdownStyle::themed(MarkdownFont::Agent, &fonts, window, cx)
42}
43
44/// Creates a freshly parsed [`Markdown`] entity from `text`.
45///
46/// `Markdown` parses in a background task and only shows content once that
47/// task completes and updates the entity — an entity with no remaining
48/// strong reference is dropped before that update can land, so its content
49/// never reaches the screen. Callers that render the same logical message
50/// across multiple frames (e.g. a streaming assistant turn) must create this
51/// once and keep the returned `Entity` alive for as long as it's displayed,
52/// updating its text via `Markdown::reset`/`Markdown::append` as new content
53/// arrives, rather than calling this again on every render.
54pub fn agent_markdown_entity(text: impl Into<SharedString>, cx: &mut App) -> Entity<Markdown> {
55    cx.new(|cx| Markdown::new(text.into(), cx))
56}
57
58/// Renders a single agent chat message body as full CommonMark/GFM markdown
59/// (tables, footnotes, task lists, syntax-highlighted fenced code, etc.) via
60/// `boltz-markdown`.
61///
62/// Wraps a caller-owned [`Entity<Markdown>`] rather than raw text, since
63/// `Markdown` parses asynchronously (see [`agent_markdown_entity`]'s docs for
64/// why a persistent entity is required for content to ever render).
65#[derive(IntoElement, RegisterComponent)]
66pub struct AgentMarkdown {
67    id: ElementId,
68    markdown: Entity<Markdown>,
69}
70
71impl AgentMarkdown {
72    pub fn new(id: impl Into<ElementId>, markdown: Entity<Markdown>) -> Self {
73        Self {
74            id: id.into(),
75            markdown,
76        }
77    }
78}
79
80impl RenderOnce for AgentMarkdown {
81    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
82        let style = agent_markdown_style(window, cx);
83        div()
84            .id(self.id)
85            .w_full()
86            .child(MarkdownElement::new(self.markdown, style))
87    }
88}
89
90impl Component for AgentMarkdown {
91    fn scope() -> ComponentScope {
92        ComponentScope::Agent
93    }
94
95    fn description() -> Option<&'static str> {
96        Some(
97            "Renders a single agent chat message body as full CommonMark/GFM \
98             markdown (tables, footnotes, task lists, syntax-highlighted \
99             fenced code) via boltz-markdown, wrapping a caller-owned \
100             Entity<Markdown>.",
101        )
102    }
103
104    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
105        let source = "# Heading One\n\n\
106             Some **bold** text with `inline code` and a [link](https://example.com).\n\n\
107             | Left | Right |\n|---|---|\n| a | b |\n\n\
108             - [x] Done item\n\
109             - [ ] Pending item\n\n\
110             1. Step one\n\
111             2. Step two\n\n\
112             ```rust\nfn main() {\n    println!(\"hi\");\n}\n```\n\n\
113             A footnote reference[^1].\n\n[^1]: The footnote body.";
114        let entity = agent_markdown_entity(source, cx);
115
116        Some(
117            v_flex()
118                .w_96()
119                .gap_4()
120                .child(single_example(
121                    "Full document",
122                    AgentMarkdown::new("am-preview-1", entity).into_any_element(),
123                ))
124                .into_any_element(),
125        )
126    }
127}