agent: Render empty path-only code blocks as file reference pills

When an agent emits a fenced code block whose info string is a project path (e.g. ```path/to/foo.rs#L1-1) with no content between the fences, render it as a small clickable file-reference pill instead of an empty code block. Clicking the pill opens the file in the workspace; if the path doesn't resolve to a project file, the click is a no-op.
This commit is contained in:
Martin Ye 2026-05-15 08:53:45 -07:00
parent d2927ffa45
commit 369d209879
2 changed files with 109 additions and 2 deletions

View file

@ -38,7 +38,8 @@ use gpui::{
use language::Buffer;
use language_model::{LanguageModelCompletionError, LanguageModelRegistry};
use markdown::{
CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownFont, MarkdownStyle,
CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownFont,
MarkdownStyle, PathWithRange,
};
use parking_lot::RwLock;
use project::{AgentId, AgentServerStore, Project, ProjectEntryId};
@ -3071,11 +3072,37 @@ fn render_agent_markdown(
.collect()
})
.unwrap_or_default();
let path_reference_workspace = workspace.clone();
MarkdownElement::new(markdown, style)
.image_resolver(move |dest_url| resolve_agent_image(dest_url, &worktree_roots))
.on_url_click(move |text, window, cx| {
thread_view::open_link(text, &workspace, window, cx);
})
.on_path_reference_click(move |path_range, window, cx| {
open_path_reference(path_range, &path_reference_workspace, window, cx);
})
}
fn open_path_reference(
path_range: &PathWithRange,
workspace: &WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
) {
let Some(workspace) = workspace.upgrade() else {
return;
};
workspace.update(cx, |workspace, cx| {
let project = workspace.project();
let Some(project_path) = project.update(cx, |project, cx| {
project.find_project_path(path_range.path.as_ref(), cx)
}) else {
return;
};
workspace
.open_path(project_path, None, true, window, cx)
.detach_and_log_err(cx);
});
}
fn plan_label_markdown_style(

View file

@ -55,6 +55,7 @@ use crate::parser::CodeBlockKind;
type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>;
type SourceClickCallback = Box<dyn Fn(usize, usize, &mut Window, &mut App) -> bool>;
type CheckboxToggleCallback = Rc<dyn Fn(Range<usize>, bool, &mut Window, &mut App)>;
type PathReferenceClickCallback = Rc<dyn Fn(&PathWithRange, &mut Window, &mut App)>;
#[derive(Clone, Copy, Default)]
pub struct BlockQuoteKindColors {
@ -1063,6 +1064,7 @@ pub struct MarkdownElement {
on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
on_source_click: Option<SourceClickCallback>,
on_checkbox_toggle: Option<CheckboxToggleCallback>,
on_path_reference_click: Option<PathReferenceClickCallback>,
image_resolver: Option<Box<dyn Fn(&str) -> Option<ImageSource>>>,
show_root_block_markers: bool,
autoscroll: AutoscrollBehavior,
@ -1080,6 +1082,7 @@ impl MarkdownElement {
on_url_click: None,
on_source_click: None,
on_checkbox_toggle: None,
on_path_reference_click: None,
image_resolver: None,
show_root_block_markers: false,
autoscroll: AutoscrollBehavior::Propagate,
@ -1136,6 +1139,17 @@ impl MarkdownElement {
self
}
/// Register a handler invoked when the user clicks a path-reference pill. A path-reference pill
/// is rendered in place of an otherwise-empty fenced code block whose info string is a project
/// path (e.g. ```` ```path/to/foo.rs#L1-1 ```` followed immediately by a closing fence).
pub fn on_path_reference_click(
mut self,
handler: impl Fn(&PathWithRange, &mut Window, &mut App) + 'static,
) -> Self {
self.on_path_reference_click = Some(Rc::new(handler));
self
}
pub fn image_resolver(
mut self,
resolver: impl Fn(&str) -> Option<ImageSource> + 'static,
@ -1697,6 +1711,7 @@ impl Element for MarkdownElement {
let mut current_img_block_range: Option<Range<usize>> = None;
let mut handled_html_block = false;
let mut rendered_mermaid_block = false;
let mut rendered_path_reference_block = false;
for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
// Skip alt text for images that rendered
if let Some(current_img_block_range) = &current_img_block_range
@ -1720,6 +1735,13 @@ impl Element for MarkdownElement {
continue;
}
if rendered_path_reference_block {
if matches!(event, MarkdownEvent::End(MarkdownTagEnd::CodeBlock)) {
rendered_path_reference_block = false;
}
continue;
}
match event {
MarkdownEvent::RootStart => {
if self.show_root_block_markers {
@ -1803,7 +1825,7 @@ impl Element for MarkdownElement {
markdown_end,
);
}
MarkdownTag::CodeBlock { kind, .. } => {
MarkdownTag::CodeBlock { kind, metadata } => {
if render_mermaid_diagrams
&& let Some(mermaid_diagram) =
parsed_markdown.mermaid_diagrams.get(&range.start)
@ -1833,6 +1855,26 @@ impl Element for MarkdownElement {
continue;
}
if let CodeBlockKind::FencedSrc(path_range) = kind
&& let Some(on_click) = self.on_path_reference_click.clone()
&& parsed_markdown
.source
.get(metadata.content_range.clone())
.is_some_and(|content| content.trim().is_empty())
{
builder.push_sourced_element(
range.clone(),
render_path_reference_pill(
path_range.clone(),
on_click,
range.start,
cx,
),
);
rendered_path_reference_block = true;
continue;
}
let language = match kind {
CodeBlockKind::Fenced => None,
CodeBlockKind::FencedLang(language) => {
@ -2420,6 +2462,44 @@ fn apply_heading_style(
heading
}
fn render_path_reference_pill(
path_range: PathWithRange,
on_click: PathReferenceClickCallback,
element_id_seed: usize,
cx: &App,
) -> AnyElement {
let display: SharedString = path_range.path.to_string().into();
let colors = cx.theme().colors();
h_flex()
.id(("markdown-path-ref", element_id_seed))
.my_1()
.flex_none()
.gap_1p5()
.px_1p5()
.py_0p5()
.rounded_md()
.border_1()
.border_color(colors.border_variant)
.bg(colors.element_background)
.hover(|this| this.bg(colors.element_hover))
.cursor_pointer()
.child(
Icon::new(IconName::File)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new(display)
.size(LabelSize::Small)
.color(Color::Muted),
)
.on_click(move |_event, window, cx| {
on_click(&path_range, window, cx);
})
.into_any_element()
}
fn render_copy_code_block_button(
id: usize,
code: String,