diff --git a/src/agent_ux.rs b/src/agent_ux.rs index e2c3376..430db85 100644 --- a/src/agent_ux.rs +++ b/src/agent_ux.rs @@ -1,5 +1,5 @@ use matrix_sdk::room::Room; -use ruma::events::relation::InReplyTo; +use ruma::events::relation::Thread; use ruma::events::room::message::{Relation, RoomMessageEventContent}; use ruma::OwnedEventId; use tracing::warn; @@ -47,25 +47,19 @@ impl AgentProgress { } } - /// Post a step update to the thread. Creates the thread on first call. + /// Post a step update to the thread on the user's message. pub async fn post_step(&mut self, text: &str) { - let content = if let Some(ref _root) = self.thread_root_id { - // Reply in existing thread - let mut msg = RoomMessageEventContent::text_markdown(text); - msg.relates_to = Some(Relation::Reply { - in_reply_to: InReplyTo::new(self.user_event_id.clone()), - }); - msg - } else { - // First message — starts the thread as a reply to the user's message - let mut msg = RoomMessageEventContent::text_markdown(text); - msg.relates_to = Some(Relation::Reply { - in_reply_to: InReplyTo::new(self.user_event_id.clone()), - }); - msg - }; + let latest = self + .thread_root_id + .as_ref() + .unwrap_or(&self.user_event_id) + .clone(); - match self.room.send(content).await { + let mut msg = RoomMessageEventContent::text_markdown(text); + let thread = Thread::plain(self.user_event_id.clone(), latest); + msg.relates_to = Some(Relation::Thread(thread)); + + match self.room.send(msg).await { Ok(response) => { if self.thread_root_id.is_none() { self.thread_root_id = Some(response.event_id); @@ -96,19 +90,51 @@ impl AgentProgress { .await; } - /// Format a tool call for the thread. + /// Format a tool call for the thread — concise, not raw args. pub fn format_tool_call(name: &str, args: &str) -> String { - format!("`{name}` → ```json\n{args}\n```") + // Extract just the key params, not the full JSON blob + let summary = match serde_json::from_str::(args) { + Ok(v) => { + let params: Vec = v + .as_object() + .map(|obj| { + obj.iter() + .filter(|(_, v)| !v.is_null() && v.as_str() != Some("")) + .map(|(k, v)| { + let val = match v { + serde_json::Value::String(s) => { + if s.len() > 40 { + format!("{}…", &s[..40]) + } else { + s.clone() + } + } + other => other.to_string(), + }; + format!("{k}={val}") + }) + .collect() + }) + .unwrap_or_default(); + if params.is_empty() { + String::new() + } else { + format!(" ({})", params.join(", ")) + } + } + Err(_) => String::new(), + }; + format!("🔧 `{name}`{summary}") } - /// Format a tool result for the thread. + /// Format a tool result for the thread — short summary only. pub fn format_tool_result(name: &str, result: &str) -> String { - let truncated = if result.len() > 500 { - format!("{}…", &result[..500]) + let truncated = if result.len() > 200 { + format!("{}…", &result[..200]) } else { result.to_string() }; - format!("`{name}` ← {truncated}") + format!("← `{name}`: {truncated}") } } @@ -117,23 +143,41 @@ mod tests { use super::*; #[test] - fn test_format_tool_call() { - let formatted = AgentProgress::format_tool_call("search_archive", r#"{"query":"test"}"#); + fn test_format_tool_call_with_params() { + let formatted = AgentProgress::format_tool_call("search_archive", r#"{"query":"test","room":"general"}"#); assert!(formatted.contains("search_archive")); - assert!(formatted.contains("test")); + assert!(formatted.contains("query=test")); + assert!(formatted.contains("room=general")); + assert!(formatted.starts_with("🔧")); + } + + #[test] + fn test_format_tool_call_no_params() { + let formatted = AgentProgress::format_tool_call("list_rooms", "{}"); + assert_eq!(formatted, "🔧 `list_rooms`"); + } + + #[test] + fn test_format_tool_call_truncates_long_values() { + let long_code = "x".repeat(100); + let args = format!(r#"{{"code":"{}"}}"#, long_code); + let formatted = AgentProgress::format_tool_call("run_script", &args); + assert!(formatted.contains("code=")); + assert!(formatted.contains("…")); + assert!(formatted.len() < 200); } #[test] fn test_format_tool_result_truncation() { - let long = "x".repeat(1000); + let long = "x".repeat(500); let formatted = AgentProgress::format_tool_result("search", &long); - assert!(formatted.len() < 600); + assert!(formatted.len() < 300); assert!(formatted.ends_with('…')); } #[test] fn test_format_tool_result_short() { let formatted = AgentProgress::format_tool_result("search", "3 results found"); - assert_eq!(formatted, "`search` ← 3 results found"); + assert_eq!(formatted, "← `search`: 3 results found"); } } diff --git a/src/matrix_utils.rs b/src/matrix_utils.rs index 2920429..f825c83 100644 --- a/src/matrix_utils.rs +++ b/src/matrix_utils.rs @@ -56,6 +56,19 @@ pub fn make_reply_content(body: &str, reply_to_event_id: OwnedEventId) -> RoomMe content } +/// Build a threaded reply — shows up in Matrix threads UI. +/// The thread root is the event being replied to (creates the thread on first use). +pub fn make_thread_reply( + body: &str, + thread_root_id: OwnedEventId, +) -> RoomMessageEventContent { + use ruma::events::relation::Thread; + let mut content = RoomMessageEventContent::text_markdown(body); + let thread = Thread::plain(thread_root_id.clone(), thread_root_id); + content.relates_to = Some(Relation::Thread(thread)); + content +} + /// Send an emoji reaction to a message. pub async fn send_reaction( room: &Room,