acp_thread: Preserve ordered mixed content in agent messages (#64456)

Preserve ordered mixed content in agent messages without disrupting text
streaming for existing native/v1 agents. The previous single-block
reduction could replace images with placeholders or lose non-text
content as subsequent chunks arrived.

- Introduce [ordered message display
content](https://github.com/zed-industries/zed/blob/929fba3e16f7a378eb1dca3d85ac2f1df2b902c6/crates/acp_thread/src/acp_thread.rs#L1500),
reusing adjacent text entities while retaining image/resource-preview
boundaries.
- Update rendering, text search, response copy/export, Mermaid cache
invalidation, and CLI output to visit the collection.
- Use block-local context menus and existing Markdown getters so copy
commands target the clicked part rather than another part's cached
state.
- Keep existing link/mention formatting, resource-button and blob-label
search behavior, compact prompt mentions, and original user-message
chunks.
- Preserve the separator between a leading resource link and subsequent
direct or streamed text.

Release Notes:

- N/A
This commit is contained in:
Ben Brandt authored and GitHub committed 2026-09-21 09:24:15 +00:00
1 parent 85b8198e86
commit df673c1b85
5 files changed
+996 -307

No files matched your search

File diff suppressed because it is too large. Load diff
+398 -8
View File
@@ -5680,6 +5680,31 @@ pub(crate) mod tests {
setup_conversation_view_with_initial_content_opt(agent, None, cx).await
}
fn png_image() -> acp::ContentBlock {
acp::ContentBlock::Image(acp::ImageContent::new(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
"image/png",
))
}
async fn setup_full_size_conversation_and_send<'a>(
connection: StubAgentConnection,
prompt: &str,
cx: &'a mut TestAppContext,
) -> (Entity<ConversationView>, &'a mut VisualTestContext) {
let (conversation_view, cx) =
setup_conversation_view(StubAgentServer::new(connection), cx).await;
add_to_workspace_with_size(conversation_view.clone(), true, cx);
let thread =
active_thread(&conversation_view, cx).read_with(cx, |view, _| view.thread.clone());
thread
.update(cx, |thread, cx| thread.send_raw(prompt, cx))
.await
.expect("prompt should succeed");
cx.run_until_parked();
(conversation_view, cx)
}
#[gpui::test]
async fn test_completed_plan_snapshot_keeps_list_state_in_sync(cx: &mut TestAppContext) {
init_test(cx);
@@ -5791,13 +5816,24 @@ pub(crate) mod tests {
}
fn add_to_workspace(conversation_view: Entity<ConversationView>, cx: &mut VisualTestContext) {
add_to_workspace_with_size(conversation_view, false, cx);
}
fn add_to_workspace_with_size(
conversation_view: Entity<ConversationView>,
fill_space: bool,
cx: &mut VisualTestContext,
) {
let workspace =
conversation_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone());
workspace
.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(
Box::new(cx.new(|_| ThreadViewItem(conversation_view.clone()))),
Box::new(cx.new(|_| ThreadViewItem {
conversation_view: conversation_view.clone(),
fill_space,
})),
None,
true,
window,
@@ -5807,7 +5843,10 @@ pub(crate) mod tests {
.unwrap();
}
struct ThreadViewItem(Entity<ConversationView>);
struct ThreadViewItem {
conversation_view: Entity<ConversationView>,
fill_space: bool,
}
impl Item for ThreadViewItem {
type Event = ();
@@ -5825,7 +5864,7 @@ pub(crate) mod tests {
impl Focusable for ThreadViewItem {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.0.read(cx).focus_handle(cx)
self.conversation_view.read(cx).focus_handle(cx)
}
}
@@ -5834,12 +5873,15 @@ pub(crate) mod tests {
// Render the title editor in the element tree too. In the real app
// it is part of the agent panel
let title_editor = self
.0
.conversation_view
.read(cx)
.active_thread()
.map(|t| t.read(cx).title_editor.clone());
v_flex().children(title_editor).child(self.0.clone())
v_flex()
.when(self.fill_space, |this| this.size_full())
.children(title_editor)
.child(self.conversation_view.clone())
}
}
@@ -7265,6 +7307,17 @@ pub(crate) mod tests {
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new(
"Hidden papaya reasoning.".into(),
)),
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new(
acp::ContentBlock::Resource(acp::EmbeddedResource::new(
acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents::new(
"A second hidden papaya.",
"thought://details",
)
.mime_type("text/markdown".to_string()),
),
)),
)),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
"Final answer without that fruit.".into(),
)),
@@ -7331,11 +7384,297 @@ pub(crate) mod tests {
assert_eq!(
bar.read_with(cx, |bar, _| bar.match_count()),
1,
"expanded thinking content should be searchable",
2,
"every Markdown leaf in expanded thinking content should be searchable",
);
}
#[gpui::test]
async fn test_empty_assistant_text_followed_by_image_is_rendered(cx: &mut TestAppContext) {
init_test(cx);
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("".into())),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(png_image())),
]);
let (_conversation_view, cx) =
setup_full_size_conversation_and_send(connection, "Show an image", cx).await;
let image_bounds = cx
.debug_bounds("agent-output-image")
.expect("image-only output must not be hidden as a blank assistant message");
assert!(
image_bounds.size.width > px(0.) && image_bounds.size.height > px(0.),
"decoded image must occupy nonzero rendered bounds, got {image_bounds:?}",
);
}
#[gpui::test]
async fn test_mixed_assistant_content_search_and_copy_preserve_order(cx: &mut TestAppContext) {
init_test(cx);
let image = png_image();
let resource = acp::ContentBlock::Resource(acp::EmbeddedResource::new(
acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents::new(
"Resource needle follows the image.",
"memory://ordered",
)
.mime_type("text/markdown".to_string()),
),
));
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new(
"Private needle must not be copied.".into(),
)),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("Before needle.".into())),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(image.clone())),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("After needle.".into())),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(image)),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(resource)),
]);
let (conversation_view, cx) =
setup_full_size_conversation_and_send(connection, "Show mixed content", cx).await;
let thread_view = active_thread(&conversation_view, cx);
let thread = thread_view.read_with(cx, |view, _| view.thread.clone());
let first_image_bounds = cx
.debug_bounds("message-content-1-1-1")
.expect("first mixed-content image should be rendered");
let second_image_bounds = cx
.debug_bounds("message-content-1-1-3")
.expect("second mixed-content image should be rendered");
for image_bounds in [first_image_bounds, second_image_bounds] {
assert!(
image_bounds.size.width > px(0.) && image_bounds.size.height > px(0.),
"each decoded image must occupy nonzero rendered bounds, got {image_bounds:?}",
);
}
assert!(
first_image_bounds.origin.y < second_image_bounds.origin.y,
"images must remain visible in content order: first {first_image_bounds:?}, second {second_image_bounds:?}",
);
let markdowns = thread.read_with(cx, |thread, _| {
let message = thread
.entries()
.iter()
.find_map(|entry| match entry {
AgentThreadEntry::AssistantMessage(message) => Some(message),
_ => None,
})
.expect("assistant message should exist");
message
.chunks
.iter()
.flat_map(|chunk| match chunk {
AssistantMessageChunk::Message { block, .. }
| AssistantMessageChunk::Thought { block, .. } => block.markdowns(),
})
.cloned()
.collect::<Vec<_>>()
});
let first_text_bounds = cx
.debug_bounds("message-content-1-1-0")
.expect("first text content");
cx.simulate_mouse_down(
first_text_bounds.center(),
gpui::MouseButton::Right,
gpui::Modifiers::default(),
);
cx.simulate_mouse_up(
first_text_bounds.center(),
gpui::MouseButton::Right,
gpui::Modifiers::default(),
);
let copy_response = cx
.debug_bounds("MENU_ITEM-Copy This Agent Response")
.expect("Copy This Agent Response");
cx.simulate_click(copy_response.center(), gpui::Modifiers::default());
let copied = cx
.update(|_, cx| cx.read_from_clipboard().and_then(|item| item.text()))
.expect("assistant response should be copied");
assert_eq!(
copied,
"Before needle.\n\n`Image`\n\nAfter needle.\n\n`Image`\n\nResource needle follows the image."
);
assert!(!copied.contains("Private needle"));
thread_view.update_in(cx, |view, window, cx| {
view.toggle_search(&crate::ToggleSearch, window, cx);
});
let search_bar = thread_view
.read_with(cx, |view, _| view.thread_search_bar.clone())
.expect("thread search should be open");
search_bar.update_in(cx, |search_bar, window, cx| {
search_bar.query_editor.update(cx, |editor, cx| {
editor.set_text("needle", window, cx);
});
search_bar.update_matches(window, cx);
});
cx.run_until_parked();
assert_eq!(search_bar.read_with(cx, |bar, _| bar.match_count()), 3);
for (source, should_be_highlighted) in [
("Private needle must not be copied.", false),
("Before needle.", true),
("After needle.", true),
("Resource needle follows the image.", true),
] {
let markdown = markdowns
.iter()
.find(|markdown| markdown.read_with(cx, |markdown, _| markdown.source() == source))
.unwrap_or_else(|| panic!("Markdown entity for {source:?} should exist"));
assert_eq!(
markdown.read_with(cx, |markdown, _| !markdown.search_highlights().is_empty()),
should_be_highlighted,
"unexpected search highlights for {source:?}",
);
}
}
#[gpui::test]
async fn test_message_context_menu_uses_clicked_content(cx: &mut TestAppContext) {
init_test(cx);
let acp::ContentBlock::Image(image) = png_image() else {
panic!("expected image fixture");
};
let linked_image = format!(
"[![second](data:{};base64,{})](https://two.example/)",
image.mime_type, image.data,
);
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
"[first](https://one.example/)".into(),
)),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(png_image())),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(linked_image.into())),
]);
let (conversation_view, cx) =
setup_conversation_view(StubAgentServer::new(connection), cx).await;
add_to_workspace_with_size(conversation_view.clone(), true, cx);
let thread_view = active_thread(&conversation_view, cx);
let thread = thread_view.read_with(cx, |view, _| view.thread.clone());
thread
.update(cx, |thread, cx| thread.send_raw("Show links", cx))
.await
.expect("link prompt");
cx.run_until_parked();
for (selector, offset, expected) in [
(
"message-content-1-0-0",
point(px(12.), px(8.)),
"https://one.example/",
),
(
"message-content-1-0-2",
point(px(0.5), px(0.5)),
"https://two.example/",
),
] {
let bounds = cx.debug_bounds(selector).expect("content leaf");
let position = bounds.origin + offset;
cx.simulate_mouse_down(
position,
gpui::MouseButton::Right,
gpui::Modifiers::default(),
);
cx.simulate_mouse_up(
position,
gpui::MouseButton::Right,
gpui::Modifiers::default(),
);
let copy = cx.debug_bounds("MENU_ITEM-Copy Link").expect("Copy Link");
cx.simulate_click(copy.center(), gpui::Modifiers::default());
let copied = cx.update(|_, cx| cx.read_from_clipboard().and_then(|item| item.text()));
assert_eq!(copied.as_deref(), Some(expected));
}
let image = cx
.debug_bounds("message-content-1-0-1")
.expect("image leaf");
cx.simulate_mouse_down(
image.center(),
gpui::MouseButton::Right,
gpui::Modifiers::default(),
);
cx.simulate_mouse_up(
image.center(),
gpui::MouseButton::Right,
gpui::Modifiers::default(),
);
assert!(cx.debug_bounds("MENU_ITEM-Copy Link").is_none());
assert!(
cx.debug_bounds("MENU_ITEM-Copy This Agent Response")
.is_some()
);
}
#[gpui::test]
async fn test_message_context_menu_copies_selected_content(cx: &mut TestAppContext) {
init_test(cx);
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("**first**".into())),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(png_image())),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("*second*".into())),
]);
let (conversation_view, cx) =
setup_conversation_view(StubAgentServer::new(connection), cx).await;
add_to_workspace_with_size(conversation_view.clone(), true, cx);
let thread_view = active_thread(&conversation_view, cx);
let thread = thread_view.read_with(cx, |view, _| view.thread.clone());
thread
.update(cx, |thread, cx| thread.send_raw("Show text", cx))
.await
.expect("text prompt");
cx.run_until_parked();
for (selector, plain, markdown) in [
("message-content-1-0-0", "first", "**first**"),
("message-content-1-0-2", "second", "*second*"),
] {
for (menu_item, expected) in [
("MENU_ITEM-Copy", plain),
("MENU_ITEM-Copy as Markdown", markdown),
] {
let bounds = cx.debug_bounds(selector).expect("content leaf");
let position = bounds.origin + point(px(12.), bounds.size.height / 2.);
cx.simulate_event(gpui::MouseDownEvent {
position,
button: gpui::MouseButton::Left,
modifiers: gpui::Modifiers::default(),
click_count: 2,
first_mouse: false,
});
cx.simulate_mouse_up(
position,
gpui::MouseButton::Left,
gpui::Modifiers::default(),
);
cx.simulate_mouse_down(
position,
gpui::MouseButton::Right,
gpui::Modifiers::default(),
);
cx.simulate_mouse_up(
position,
gpui::MouseButton::Right,
gpui::Modifiers::default(),
);
let copy = cx.debug_bounds(menu_item).expect("selection copy command");
cx.simulate_click(copy.center(), gpui::Modifiers::default());
let copied =
cx.update(|_, cx| cx.read_from_clipboard().and_then(|item| item.text()));
assert_eq!(copied.as_deref(), Some(expected));
}
}
}
#[gpui::test]
async fn test_thread_search_includes_expanded_tool_call_content(cx: &mut TestAppContext) {
init_test(cx);
@@ -7397,6 +7736,57 @@ pub(crate) mod tests {
1,
"expanded tool-call content should be searchable",
);
thread
.update(cx, |thread, cx| {
thread.handle_session_update(
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
"search-tool-content",
acp::ToolCallUpdateFields::new().content(vec![
acp::ToolCallContent::Content(acp::Content::new(
acp::ContentBlock::Text(acp::TextContent::new(
"Replacement contains mango.",
)),
)),
acp::ToolCallContent::Content(acp::Content::new(
acp::ContentBlock::Resource(acp::EmbeddedResource::new(
acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents::new(
"Resource mango output.",
"tool://replacement",
)
.mime_type("text/markdown".to_string()),
),
)),
)),
]),
)),
cx,
)
})
.expect("tool output replacement should apply");
cx.run_until_parked();
cx.executor()
.advance_clock(super::thread_search_bar::SEARCH_UPDATE_DEBOUNCE * 2);
cx.run_until_parked();
assert_eq!(
bar.read_with(cx, |bar, _| bar.match_count()),
0,
"replaced tool output should no longer match the old query",
);
bar.update_in(cx, |bar, window, cx| {
bar.query_editor.update(cx, |editor, cx| {
editor.set_text("mango", window, cx);
});
bar.update_matches(window, cx);
});
cx.run_until_parked();
assert_eq!(
bar.read_with(cx, |bar, _| bar.match_count()),
2,
"all Markdown leaves in replacement tool output should be searchable",
);
}
#[gpui::test]
@@ -7632,7 +8022,7 @@ pub(crate) mod tests {
AgentThreadEntry::AssistantMessage(message) => {
message.chunks.iter().find_map(|chunk| match chunk {
AssistantMessageChunk::Message { block, .. } => {
block.markdown().cloned()
block.markdowns().next().cloned()
}
AssistantMessageChunk::Thought { .. } => None,
})
@@ -891,18 +891,14 @@ fn collect_markdowns(
for (chunk_ix, chunk) in message.chunks.iter().enumerate() {
match chunk {
AssistantMessageChunk::Message { block, .. } => {
if let Some(md) = block.markdown() {
out.push(md.clone());
}
out.extend(block.markdowns().cloned());
}
AssistantMessageChunk::Thought { block, .. }
if entry_view_state
.thinking_block_state((entry_ix, chunk_ix), cx)
.0 =>
{
if let Some(md) = block.markdown() {
out.push(md.clone());
}
out.extend(block.markdowns().cloned());
}
AssistantMessageChunk::Thought { .. } => {}
}
@@ -974,7 +970,6 @@ mod tests {
)),
markdown: unsupported.clone(),
},
ContentBlock::Empty,
],
error: Some(error.clone()),
};
@@ -6353,43 +6353,31 @@ impl ThreadView {
let mut is_blank = true;
let is_last = entry_ix + 1 == total_entries;
let style = MarkdownStyle::themed(MarkdownFont::Agent, window, cx);
let message_body = v_flex()
.w_full()
.gap_3()
.children(chunks.iter().enumerate().filter_map(
|(chunk_ix, chunk)| match chunk {
AssistantMessageChunk::Message { block, .. } => {
block.markdown().and_then(|md| {
let this_is_blank = md.read(cx).source().trim().is_empty();
is_blank = is_blank && this_is_blank;
if this_is_blank {
return None;
}
Some(
self.render_markdown(md.clone(), style.clone(), cx)
.into_any_element(),
)
let this_is_blank = !block.visible_content(cx);
is_blank = is_blank && this_is_blank;
(!this_is_blank).then(|| {
div()
.id(("assistant-message-chunk", chunk_ix))
.child(self.render_message_content(
entry_ix, chunk_ix, block, window, cx,
))
.into_any_element()
})
}
AssistantMessageChunk::Thought { block, .. } => {
block.markdown().and_then(|md| {
let this_is_blank = md.read(cx).source().trim().is_empty();
is_blank = is_blank && this_is_blank;
if this_is_blank {
return None;
}
Some(
self.render_thinking_block(
entry_ix,
chunk_ix,
md.clone(),
window,
cx,
)
.into_any_element(),
let this_is_blank = !block.visible_content(cx);
is_blank = is_blank && this_is_blank;
(!this_is_blank).then(|| {
self.render_thinking_block(
entry_ix, chunk_ix, block, window, cx,
)
.into_any_element()
})
}
},
@@ -6407,7 +6395,7 @@ impl ThreadView {
.when(is_last, |this| this.pb_4())
.w_full()
.text_ui(cx)
.child(self.render_message_context_menu(entry_ix, message_body, cx))
.child(message_body)
.when_some(
self.entry_view_state
.read(cx)
@@ -7458,11 +7446,44 @@ impl ThreadView {
cx.notify();
}
fn render_message_content(
&self,
entry_ix: usize,
chunk_ix: usize,
content: &acp_thread::MessageContent,
window: &Window,
cx: &Context<Self>,
) -> Div {
v_flex().w_full().gap_3().children(
content
.blocks()
.iter()
.enumerate()
.filter(|(_, block)| block.visible_content(cx))
.map(|(block_ix, block)| {
let content = self.render_output_content_block(
entry_ix, block_ix, block, None, false, window, cx,
);
div()
.id(("message-content-block", block_ix))
.debug_selector(move || {
format!("message-content-{entry_ix}-{chunk_ix}-{block_ix}")
})
.child(self.render_message_context_menu(
entry_ix,
block.markdown().cloned(),
content,
cx,
))
}),
)
}
fn render_thinking_block(
&self,
entry_ix: usize,
chunk_ix: usize,
chunk: Entity<Markdown>,
chunk: &acp_thread::MessageContent,
window: &Window,
cx: &Context<Self>,
) -> AnyElement {
@@ -7487,6 +7508,7 @@ impl ThreadView {
let panel_bg = cx.theme().colors().panel_background;
v_flex()
.id(("thinking-block", chunk_ix))
.gap_1()
.child(
h_flex()
@@ -7524,7 +7546,10 @@ impl ThreadView {
)
.on_click(cx.listener(move |this, _event: &ClickEvent, window, cx| {
this.toggle_thinking_block_expansion(key, window, cx);
})),
}))
.map(|header| {
self.render_message_context_menu(entry_ix, None, header.into_any(), cx)
}),
)
.when(is_open, |this| {
this.child(
@@ -7542,11 +7567,11 @@ impl ThreadView {
this.track_scroll(&scroll_handle)
})
.overflow_hidden()
.child(self.render_markdown(
chunk,
MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
cx,
)),
.child(
self.render_message_content(
entry_ix, chunk_ix, chunk, window, cx,
),
),
)
.when(is_constrained, |this| {
this.child(
@@ -7570,6 +7595,7 @@ impl ThreadView {
fn render_message_context_menu(
&self,
entry_ix: usize,
markdown: Option<Entity<Markdown>>,
message_body: AnyElement,
cx: &Context<Self>,
) -> AnyElement {
@@ -7582,51 +7608,18 @@ impl ThreadView {
let focus = window.focused(cx);
let entity = entity.clone();
let workspace = workspace.clone();
let markdown = markdown.clone();
ContextMenu::build(window, cx, move |menu, _, cx| {
let this = entity.read(cx);
let is_at_top = this.list_state.logical_scroll_top().item_ix == 0;
let chunks =
this.thread.read(cx).entries().get(entry_ix).and_then(
|entry| match &entry {
AgentThreadEntry::AssistantMessage(msg) => Some(&msg.chunks),
_ => None,
},
);
let context_menu_link = chunks.and_then(|chunks| {
chunks.iter().find_map(|chunk| {
let markdown = match chunk {
AssistantMessageChunk::Message { block, .. } => block.markdown(),
AssistantMessageChunk::Thought { block, .. } => block.markdown(),
};
markdown
.and_then(|markdown| markdown.read(cx).context_menu_link().cloned())
})
});
let selected_text = chunks.and_then(|chunks| {
chunks.iter().find_map(|chunk| {
let markdown = match chunk {
AssistantMessageChunk::Message { block, .. } => block.markdown(),
AssistantMessageChunk::Thought { block, .. } => block.markdown(),
};
markdown.and_then(|markdown| {
markdown.read(cx).context_menu_selected_text().cloned()
})
})
});
let selected_markdown = chunks.and_then(|chunks| {
chunks.iter().find_map(|chunk| {
let markdown = match chunk {
AssistantMessageChunk::Message { block, .. } => block.markdown(),
AssistantMessageChunk::Thought { block, .. } => block.markdown(),
};
markdown.and_then(|markdown| {
markdown.read(cx).context_menu_selected_markdown().cloned()
})
})
});
let markdown = markdown.as_ref().map(|markdown| markdown.read(cx));
let context_menu_link =
markdown.and_then(|markdown| markdown.context_menu_link().cloned());
let selected_text = markdown
.and_then(|markdown| markdown.context_menu_selected_text().cloned());
let selected_markdown = markdown
.and_then(|markdown| markdown.context_menu_selected_markdown().cloned());
let copy_this_agent_response =
ContextMenuEntry::new("Copy This Agent Response").handler({
@@ -7746,7 +7739,7 @@ impl ThreadView {
if markdown.trim().is_empty() {
None
} else {
Some(markdown.to_string())
Some(markdown)
}
}
AssistantMessageChunk::Thought { .. } => None,
@@ -10571,6 +10564,7 @@ impl ThreadView {
cx: &Context<Self>,
) -> AnyElement {
v_flex()
.debug_selector(|| "agent-output-image".into())
.gap_2()
.map(|this| {
if card_layout {
+1 -1
View File
@@ -971,7 +971,7 @@ fn log_acp_thread_event(
if let Some(acp_thread::AgentThreadEntry::AssistantMessage(message)) = entries.last() {
for chunk in &message.chunks {
if let acp_thread::AssistantMessageChunk::Message { id: _, block } = chunk {
if let acp_thread::ContentBlock::Markdown { markdown } = block {
for markdown in block.markdowns() {
let text = markdown.read(cx).source().to_string();
if !text.is_empty() {
eprint!("{text}");