agent: Use Markdown to render tool input and output content (#28127)
Release Notes: - agent: Tool call's input and output content are now rendered with Markdown, which allows them to be selected and copied. --------- Co-authored-by: Agus Zubiaga <hi@aguz.me>
This commit is contained in:
parent
b8d05bb641
commit
288da0f072
2 changed files with 161 additions and 37 deletions
|
@ -49,7 +49,7 @@ pub struct ActiveThread {
|
|||
show_scrollbar: bool,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
|
||||
rendered_tool_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
|
||||
rendered_tool_uses: HashMap<LanguageModelToolUseId, RenderedToolUse>,
|
||||
editing_message: Option<(MessageId, EditMessageState)>,
|
||||
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
|
||||
expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
|
||||
|
@ -65,6 +65,13 @@ struct RenderedMessage {
|
|||
segments: Vec<RenderedMessageSegment>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RenderedToolUse {
|
||||
label: Entity<Markdown>,
|
||||
input: Entity<Markdown>,
|
||||
output: Entity<Markdown>,
|
||||
}
|
||||
|
||||
impl RenderedMessage {
|
||||
fn from_segments(
|
||||
segments: &[MessageSegment],
|
||||
|
@ -235,7 +242,7 @@ fn render_markdown(
|
|||
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
|
||||
font_features: Some(theme_settings.buffer_font.features.clone()),
|
||||
font_size: Some(buffer_font_size.into()),
|
||||
background_color: Some(colors.editor_foreground.opacity(0.1)),
|
||||
background_color: Some(colors.editor_foreground.opacity(0.08)),
|
||||
..Default::default()
|
||||
},
|
||||
link: TextStyleRefinement {
|
||||
|
@ -270,6 +277,74 @@ fn render_markdown(
|
|||
})
|
||||
}
|
||||
|
||||
fn render_tool_use_markdown(
|
||||
text: SharedString,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Markdown> {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let colors = cx.theme().colors();
|
||||
let ui_font_size = TextSize::Default.rems(cx);
|
||||
let buffer_font_size = TextSize::Small.rems(cx);
|
||||
let mut text_style = window.text_style();
|
||||
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(theme_settings.ui_font.family.clone()),
|
||||
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
|
||||
font_features: Some(theme_settings.ui_font.features.clone()),
|
||||
font_size: Some(ui_font_size.into()),
|
||||
color: Some(cx.theme().colors().text),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let markdown_style = MarkdownStyle {
|
||||
base_text_style: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
code_block_overflow_x_scroll: true,
|
||||
code_block: StyleRefinement {
|
||||
margin: EdgesRefinement::default(),
|
||||
padding: EdgesRefinement::default(),
|
||||
background: Some(colors.editor_background.into()),
|
||||
border_color: None,
|
||||
border_widths: EdgesRefinement::default(),
|
||||
text: Some(TextStyleRefinement {
|
||||
font_family: Some(theme_settings.buffer_font.family.clone()),
|
||||
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
|
||||
font_features: Some(theme_settings.buffer_font.features.clone()),
|
||||
font_size: Some(buffer_font_size.into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
inline_code: TextStyleRefinement {
|
||||
font_family: Some(theme_settings.buffer_font.family.clone()),
|
||||
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
|
||||
font_features: Some(theme_settings.buffer_font.features.clone()),
|
||||
font_size: Some(TextSize::XSmall.rems(cx).into()),
|
||||
..Default::default()
|
||||
},
|
||||
heading: StyleRefinement {
|
||||
text: Some(TextStyleRefinement {
|
||||
font_size: Some(ui_font_size.into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
cx.new(|cx| {
|
||||
Markdown::new(text, markdown_style, Some(language_registry), None, cx).open_url(
|
||||
move |text, window, cx| {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn open_markdown_link(
|
||||
text: SharedString,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
|
@ -374,7 +449,7 @@ impl ActiveThread {
|
|||
save_thread_task: None,
|
||||
messages: Vec::new(),
|
||||
rendered_messages_by_id: HashMap::default(),
|
||||
rendered_tool_use_labels: HashMap::default(),
|
||||
rendered_tool_uses: HashMap::default(),
|
||||
expanded_tool_uses: HashMap::default(),
|
||||
expanded_thinking_segments: HashMap::default(),
|
||||
list_state: list_state.clone(),
|
||||
|
@ -393,9 +468,11 @@ impl ActiveThread {
|
|||
this.push_message(&message.id, &message.segments, window, cx);
|
||||
|
||||
for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
|
||||
this.render_tool_use_label_markdown(
|
||||
this.render_tool_use_markdown(
|
||||
tool_use.id.clone(),
|
||||
tool_use.ui_text.clone(),
|
||||
&tool_use.input,
|
||||
tool_use.status.text(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
@ -486,23 +563,44 @@ impl ActiveThread {
|
|||
self.rendered_messages_by_id.remove(id);
|
||||
}
|
||||
|
||||
fn render_tool_use_label_markdown(
|
||||
fn render_tool_use_markdown(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_label: impl Into<SharedString>,
|
||||
tool_input: &serde_json::Value,
|
||||
tool_output: SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.rendered_tool_use_labels.insert(
|
||||
tool_use_id,
|
||||
render_markdown(
|
||||
let rendered = RenderedToolUse {
|
||||
label: render_tool_use_markdown(
|
||||
tool_label.into(),
|
||||
self.language_registry.clone(),
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
);
|
||||
input: render_tool_use_markdown(
|
||||
format!(
|
||||
"```json\n{}\n```",
|
||||
serde_json::to_string_pretty(tool_input).unwrap_or_default()
|
||||
)
|
||||
.into(),
|
||||
self.language_registry.clone(),
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
output: render_tool_use_markdown(
|
||||
tool_output,
|
||||
self.language_registry.clone(),
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
};
|
||||
self.rendered_tool_uses
|
||||
.insert(tool_use_id.clone(), rendered);
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
|
@ -587,9 +685,11 @@ impl ActiveThread {
|
|||
.update(cx, |thread, cx| thread.use_pending_tools(cx));
|
||||
|
||||
for tool_use in tool_uses {
|
||||
self.render_tool_use_label_markdown(
|
||||
self.render_tool_use_markdown(
|
||||
tool_use.id.clone(),
|
||||
tool_use.ui_text.clone(),
|
||||
&tool_use.input,
|
||||
"".into(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
@ -602,9 +702,11 @@ impl ActiveThread {
|
|||
} => {
|
||||
let canceled = *canceled;
|
||||
if let Some(tool_use) = pending_tool_use {
|
||||
self.render_tool_use_label_markdown(
|
||||
self.render_tool_use_markdown(
|
||||
tool_use.id.clone(),
|
||||
SharedString::from(tool_use.ui_text.clone()),
|
||||
tool_use.ui_text.clone(),
|
||||
&tool_use.input,
|
||||
"".into(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
@ -1261,7 +1363,7 @@ impl ActiveThread {
|
|||
.pb_2p5()
|
||||
.when(!tool_uses.is_empty(), |parent| {
|
||||
parent.child(
|
||||
v_flex().children(
|
||||
div().children(
|
||||
tool_uses
|
||||
.into_iter()
|
||||
.map(|tool_use| self.render_tool_use(tool_use, cx)),
|
||||
|
@ -1695,11 +1797,13 @@ impl ActiveThread {
|
|||
}
|
||||
});
|
||||
|
||||
let content_container = || v_flex().py_1().gap_0p5().px_2p5();
|
||||
let rendered_tool_use = self.rendered_tool_uses.get(&tool_use.id).cloned();
|
||||
let results_content_container = || v_flex().p_2().gap_0p5();
|
||||
|
||||
let results_content = v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
content_container()
|
||||
results_content_container()
|
||||
.child(
|
||||
Label::new("Input")
|
||||
.size(LabelSize::XSmall)
|
||||
|
@ -1707,16 +1811,16 @@ impl ActiveThread {
|
|||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx),
|
||||
div().w_full().text_ui_sm(cx).children(
|
||||
rendered_tool_use
|
||||
.as_ref()
|
||||
.map(|rendered| rendered.input.clone()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.map(|container| match tool_use.status {
|
||||
ToolUseStatus::Finished(output) => container.child(
|
||||
content_container()
|
||||
ToolUseStatus::Finished(_) => container.child(
|
||||
results_content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
|
@ -1725,10 +1829,16 @@ impl ActiveThread {
|
|||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Label::new(output).size(LabelSize::Small).buffer_font(cx)),
|
||||
.child(
|
||||
div().w_full().text_ui_sm(cx).children(
|
||||
rendered_tool_use
|
||||
.as_ref()
|
||||
.map(|rendered| rendered.output.clone()),
|
||||
),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Running => container.child(
|
||||
content_container().child(
|
||||
results_content_container().child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.pb_1()
|
||||
|
@ -1756,8 +1866,8 @@ impl ActiveThread {
|
|||
),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Error(err) => container.child(
|
||||
content_container()
|
||||
ToolUseStatus::Error(_) => container.child(
|
||||
results_content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
|
@ -1766,11 +1876,17 @@ impl ActiveThread {
|
|||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Label::new(err).size(LabelSize::Small).buffer_font(cx)),
|
||||
.child(
|
||||
div().text_ui_sm(cx).children(
|
||||
rendered_tool_use
|
||||
.as_ref()
|
||||
.map(|rendered| rendered.output.clone()),
|
||||
),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Pending => container,
|
||||
ToolUseStatus::NeedsConfirmation => container.child(
|
||||
content_container()
|
||||
results_content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
|
@ -1786,13 +1902,13 @@ impl ActiveThread {
|
|||
div()
|
||||
.h_full()
|
||||
.absolute()
|
||||
.w_8()
|
||||
.w_12()
|
||||
.bottom_0()
|
||||
.map(|element| {
|
||||
if is_status_finished {
|
||||
element.right_7()
|
||||
element.right_6()
|
||||
} else {
|
||||
element.right(px(46.))
|
||||
element.right(px(44.))
|
||||
}
|
||||
})
|
||||
.bg(linear_gradient(
|
||||
|
@ -1828,9 +1944,7 @@ impl ActiveThread {
|
|||
)
|
||||
.child(
|
||||
h_flex().pr_8().text_ui_sm(cx).children(
|
||||
self.rendered_tool_use_labels
|
||||
.get(&tool_use.id)
|
||||
.cloned(),
|
||||
rendered_tool_use.map(|rendered| rendered.label)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -1918,9 +2032,7 @@ impl ActiveThread {
|
|||
)
|
||||
.child(
|
||||
h_flex().pr_8().text_ui_sm(cx).children(
|
||||
self.rendered_tool_use_labels
|
||||
.get(&tool_use.id)
|
||||
.cloned(),
|
||||
rendered_tool_use.map(|rendered| rendered.label)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -35,6 +35,18 @@ pub enum ToolUseStatus {
|
|||
Error(SharedString),
|
||||
}
|
||||
|
||||
impl ToolUseStatus {
|
||||
pub fn text(&self) -> SharedString {
|
||||
match self {
|
||||
ToolUseStatus::NeedsConfirmation => "".into(),
|
||||
ToolUseStatus::Pending => "".into(),
|
||||
ToolUseStatus::Running => "".into(),
|
||||
ToolUseStatus::Finished(out) => out.clone(),
|
||||
ToolUseStatus::Error(out) => out.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ToolUseState {
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue