From ca08b990d80928feddb055b40b5e864180180044 Mon Sep 17 00:00:00 2001 From: Hikari Date: Tue, 24 Feb 2026 19:49:21 -0800 Subject: [PATCH] fix: extract last_assistant_message from ToolResult content in JSON stream Previously the feature relied on SubagentStop hook events via stderr, but Claude Code writes nothing to stderr. Instead, extract the agent's final output from the ToolResult content block in the stdout JSON stream, which contains the actual task result text. Handles both plain string and array-of-content-block formats. --- src-tauri/src/wsl_bridge.rs | 101 ++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs index 1b14dd7..c3e5fc8 100644 --- a/src-tauri/src/wsl_bridge.rs +++ b/src-tauri/src/wsl_bridge.rs @@ -875,6 +875,32 @@ fn parse_subagent_stop_hook(line: &str) -> Option { }) } +/// Extract text content from a ToolResult's `content` field. +/// The content may be a JSON string or an array of typed content blocks. +fn extract_tool_result_text(content: &serde_json::Value) -> Option { + match content { + serde_json::Value::String(s) if !s.is_empty() => Some(s.clone()), + serde_json::Value::Array(blocks) => { + let texts: Vec = blocks + .iter() + .filter_map(|block| { + if block.get("type")?.as_str()? == "text" { + block.get("text")?.as_str().map(String::from) + } else { + None + } + }) + .collect(); + if texts.is_empty() { + None + } else { + Some(texts.join("\n")) + } + } + _ => None, + } +} + fn process_json_line( line: &str, app: &AppHandle, @@ -1176,8 +1202,8 @@ fn process_json_line( } ContentBlock::ToolResult { tool_use_id, + content, is_error, - .. } => { // Emit agent-end for all tool results // The frontend will ignore IDs that don't match known agents @@ -1195,7 +1221,7 @@ fn process_json_line( conversation_id: conversation_id.clone(), duration_ms: None, num_turns: None, - last_assistant_message: None, + last_assistant_message: extract_tool_result_text(content), }, ); } @@ -1613,8 +1639,8 @@ fn process_json_line( for block in &message.content { if let ContentBlock::ToolResult { tool_use_id, + content, is_error, - .. } = block { let now = SystemTime::now() @@ -1631,7 +1657,7 @@ fn process_json_line( conversation_id: conversation_id.clone(), duration_ms: None, num_turns: None, - last_assistant_message: None, + last_assistant_message: extract_tool_result_text(content), }, ); } @@ -2252,4 +2278,71 @@ mod tests { Some("Found 3 files, all passing.".to_string()) ); } + + // extract_tool_result_text tests + #[test] + fn test_extract_tool_result_text_plain_string() { + let content = serde_json::json!("Hello from agent"); + assert_eq!( + extract_tool_result_text(&content), + Some("Hello from agent".to_string()) + ); + } + + #[test] + fn test_extract_tool_result_text_empty_string() { + let content = serde_json::json!(""); + assert_eq!(extract_tool_result_text(&content), None); + } + + #[test] + fn test_extract_tool_result_text_array_single_text_block() { + let content = serde_json::json!([{"type": "text", "text": "Agent completed the task."}]); + assert_eq!( + extract_tool_result_text(&content), + Some("Agent completed the task.".to_string()) + ); + } + + #[test] + fn test_extract_tool_result_text_array_multiple_text_blocks() { + let content = serde_json::json!([ + {"type": "text", "text": "First part."}, + {"type": "text", "text": "Second part."} + ]); + assert_eq!( + extract_tool_result_text(&content), + Some("First part.\nSecond part.".to_string()) + ); + } + + #[test] + fn test_extract_tool_result_text_array_non_text_block() { + let content = serde_json::json!([{"type": "image", "source": {"type": "base64"}}]); + assert_eq!(extract_tool_result_text(&content), None); + } + + #[test] + fn test_extract_tool_result_text_array_mixed_blocks() { + let content = serde_json::json!([ + {"type": "image", "source": {}}, + {"type": "text", "text": "Found results."} + ]); + assert_eq!( + extract_tool_result_text(&content), + Some("Found results.".to_string()) + ); + } + + #[test] + fn test_extract_tool_result_text_null() { + let content = serde_json::Value::Null; + assert_eq!(extract_tool_result_text(&content), None); + } + + #[test] + fn test_extract_tool_result_text_empty_array() { + let content = serde_json::json!([]); + assert_eq!(extract_tool_result_text(&content), None); + } }