diff --git a/src/bin/midtown/cli/response.rs b/src/bin/midtown/cli/response.rs index 4cd6f7c07..562a86ab5 100644 --- a/src/bin/midtown/cli/response.rs +++ b/src/bin/midtown/cli/response.rs @@ -1,3 +1,4 @@ +use midtown::ToolBlock; use serde::{Deserialize, Serialize}; /// Response wrapper for CLI output formatting @@ -110,6 +111,8 @@ pub struct ChannelMessage { pub from: String, pub message: String, pub timestamp: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tool_data: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -395,10 +398,27 @@ impl Response { } let mut out = String::new(); for msg in messages { - out.push_str(&format!( - "[{}] {}: {}\n", - msg.timestamp, msg.from, msg.message - )); + let text = if msg.message.is_empty() { + if let Some(ref blocks) = msg.tool_data { + if !blocks.is_empty() { + format!( + "[{}]", + blocks + .iter() + .map(|b| b.tool_name.as_str()) + .collect::>() + .join(", ") + ) + } else { + msg.message.clone() + } + } else { + msg.message.clone() + } + } else { + msg.message.clone() + }; + out.push_str(&format!("[{}] {}: {}\n", msg.timestamp, msg.from, text)); } out.trim_end().to_string() } diff --git a/src/bin/midtown/cli/response_tests.rs b/src/bin/midtown/cli/response_tests.rs index 2f5ac8837..ab3f3e250 100644 --- a/src/bin/midtown/cli/response_tests.rs +++ b/src/bin/midtown/cli/response_tests.rs @@ -1,3 +1,5 @@ +use midtown::ToolBlock; + use super::*; #[test] @@ -990,3 +992,95 @@ fn test_json_passthrough_to_json_does_not_double_wrap() { json_output ); } + +#[test] +fn test_messages_pretty_shows_tool_summary_for_tool_only_message() { + // A message with empty text but tool_data should show [ToolName, ...] in --text output. + let response = Response::Messages { + messages: vec![ChannelMessage { + from: "worker".to_string(), + message: String::new(), + timestamp: "12:00".to_string(), + tool_data: Some(vec![ + ToolBlock { + tool_name: "Bash".to_string(), + input: serde_json::json!({"command": "ls"}), + output: None, + error: false, + call_id: None, + parent_tool_use_id: None, + }, + ToolBlock { + tool_name: "Read".to_string(), + input: serde_json::json!({"file_path": "/foo"}), + output: None, + error: false, + call_id: None, + parent_tool_use_id: None, + }, + ]), + }], + }; + + let pretty = response.to_pretty(); + assert!( + pretty.contains("[Bash, Read]"), + "Tool-only message should show '[Bash, Read]' summary, got: {}", + pretty + ); +} + +#[test] +fn test_messages_pretty_shows_text_when_message_is_present() { + // A message with text content should show the text, not a tool summary. + let response = Response::Messages { + messages: vec![ChannelMessage { + from: "worker".to_string(), + message: "hello world".to_string(), + timestamp: "12:00".to_string(), + tool_data: Some(vec![ToolBlock { + tool_name: "Bash".to_string(), + input: serde_json::json!({}), + output: None, + error: false, + call_id: None, + parent_tool_use_id: None, + }]), + }], + }; + + let pretty = response.to_pretty(); + assert!( + pretty.contains("hello world"), + "Message with text should show the text, got: {}", + pretty + ); + assert!( + !pretty.contains("[Bash]"), + "Message with text should not show tool summary, got: {}", + pretty + ); +} + +#[test] +fn test_channel_message_deserializes_tool_data_from_json() { + // The daemon returns tool_data in the JSON — verify ChannelMessage captures it. + let messages_json = r#"{"messages": [ + {"from": "worker", "message": "", "timestamp": "12:00", + "tool_data": [{"tool_name": "Edit", "input": {}, "error": false}]} + ]}"#; + let response: Response = serde_json::from_str(messages_json).unwrap(); + + match response { + Response::Messages { messages } => { + assert_eq!(messages.len(), 1); + let tool_data = messages[0] + .tool_data + .as_ref() + .expect("tool_data should be present"); + assert_eq!(tool_data.len(), 1); + assert_eq!(tool_data[0].tool_name, "Edit"); + } + other => panic!("Expected Messages, got {:?}", other), + } +}