agent_ui: Make tool call raw input visible (#45097)

Torstein Sørnes and Danilo Leal created

<img width="500" height="1246" alt="Screenshot 2025-12-17 at 9  28@2x"
src="https://github.com/user-attachments/assets/eddb290d-d4d0-4ab8-94b3-bcc50ad07157"
/>

Release Notes:

- agent: Made tool calls' raw input visible in the agent UI.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

crates/acp_thread/src/acp_thread.rs    |  9 +++++++++
crates/agent_ui/src/acp/thread_view.rs | 27 +++++++++++++++++++++++++--
2 files changed, 34 insertions(+), 2 deletions(-)

Detailed changes

crates/acp_thread/src/acp_thread.rs 🔗

@@ -192,6 +192,7 @@ pub struct ToolCall {
     pub locations: Vec<acp::ToolCallLocation>,
     pub resolved_locations: Vec<Option<AgentLocation>>,
     pub raw_input: Option<serde_json::Value>,
+    pub raw_input_markdown: Option<Entity<Markdown>>,
     pub raw_output: Option<serde_json::Value>,
 }
 
@@ -222,6 +223,11 @@ impl ToolCall {
             }
         }
 
+        let raw_input_markdown = tool_call
+            .raw_input
+            .as_ref()
+            .and_then(|input| markdown_for_raw_output(input, &language_registry, cx));
+
         let result = Self {
             id: tool_call.tool_call_id,
             label: cx
@@ -232,6 +238,7 @@ impl ToolCall {
             resolved_locations: Vec::default(),
             status,
             raw_input: tool_call.raw_input,
+            raw_input_markdown,
             raw_output: tool_call.raw_output,
         };
         Ok(result)
@@ -307,6 +314,7 @@ impl ToolCall {
         }
 
         if let Some(raw_input) = raw_input {
+            self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx);
             self.raw_input = Some(raw_input);
         }
 
@@ -1355,6 +1363,7 @@ impl AcpThread {
                     locations: Vec::new(),
                     resolved_locations: Vec::new(),
                     raw_input: None,
+                    raw_input_markdown: None,
                     raw_output: None,
                 };
                 self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -2440,6 +2440,12 @@ impl AcpThreadView {
         let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
 
         let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
+        let input_output_header = |label: SharedString| {
+            Label::new(label)
+                .size(LabelSize::XSmall)
+                .color(Color::Muted)
+                .buffer_font(cx)
+        };
 
         let tool_output_display =
             if is_open {
@@ -2481,7 +2487,25 @@ impl AcpThreadView {
                     | ToolCallStatus::Completed
                     | ToolCallStatus::Failed
                     | ToolCallStatus::Canceled => v_flex()
+                        .mt_1p5()
                         .w_full()
+                        .child(
+                            v_flex()
+                                .ml(rems(0.4))
+                                .px_3p5()
+                                .pb_1()
+                                .gap_1()
+                                .border_l_1()
+                                .border_color(self.tool_card_border_color(cx))
+                                .child(input_output_header("Raw Input".into()))
+                                .children(tool_call.raw_input_markdown.clone().map(|input| {
+                                    self.render_markdown(
+                                        input,
+                                        default_markdown_style(false, false, window, cx),
+                                    )
+                                }))
+                                .child(input_output_header("Output:".into())),
+                        )
                         .children(tool_call.content.iter().enumerate().map(
                             |(content_ix, content)| {
                                 div().child(self.render_tool_call_content(
@@ -2580,7 +2604,7 @@ impl AcpThreadView {
                                         .gap_px()
                                         .when(is_collapsible, |this| {
                                             this.child(
-                                            Disclosure::new(("expand", entry_ix), is_open)
+                                            Disclosure::new(("expand-output", entry_ix), is_open)
                                                 .opened_icon(IconName::ChevronUp)
                                                 .closed_icon(IconName::ChevronDown)
                                                 .visible_on_hover(&card_header_id)
@@ -2766,7 +2790,6 @@ impl AcpThreadView {
         let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
 
         v_flex()
-            .mt_1p5()
             .gap_2()
             .when(!card_layout, |this| {
                 this.ml(rems(0.4))