From bb641d28c22de67a20f00617c927a952f488b9ea Mon Sep 17 00:00:00 2001 From: Chummy Date: Tue, 17 Feb 2026 22:04:34 +0800 Subject: [PATCH] fix(approval): harden CLI approval flow and summaries --- src/agent/loop_.rs | 48 ++++++++++++++++++++++++++++++--------------- src/approval/mod.rs | 39 ++++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs index f2e7592..6ff27b4 100644 --- a/src/agent/loop_.rs +++ b/src/agent/loop_.rs @@ -1058,39 +1058,53 @@ pub async fn run( } else { println!("🦀 ZeroClaw Interactive Mode"); println!("Type /quit to exit.\n"); - - let (tx, mut rx) = tokio::sync::mpsc::channel(32); let cli = crate::channels::CliChannel::new(); - // Spawn listener - let listen_handle = tokio::spawn(async move { - let _ = crate::channels::Channel::listen(&cli, tx).await; - }); - // Persistent conversation history across turns let mut history = vec![ChatMessage::system(&system_prompt)]; - while let Some(msg) = rx.recv().await { + loop { + print!("> "); + let _ = std::io::stdout().flush(); + + let mut input = String::new(); + match std::io::stdin().read_line(&mut input) { + Ok(0) => break, + Ok(_) => {} + Err(e) => { + eprintln!("\nError reading input: {e}\n"); + break; + } + } + + let user_input = input.trim().to_string(); + if user_input.is_empty() { + continue; + } + if user_input == "/quit" || user_input == "/exit" { + break; + } + // Auto-save conversation turns if config.memory.auto_save { let user_key = autosave_memory_key("user_msg"); let _ = mem - .store(&user_key, &msg.content, MemoryCategory::Conversation, None) + .store(&user_key, &user_input, MemoryCategory::Conversation, None) .await; } // Inject memory + hardware RAG context into user message - let mem_context = build_context(mem.as_ref(), &msg.content).await; + let mem_context = build_context(mem.as_ref(), &user_input).await; let rag_limit = if config.agent.compact_context { 2 } else { 5 }; let hw_context = hardware_rag .as_ref() - .map(|r| build_hardware_context(r, &msg.content, &board_names, rag_limit)) + .map(|r| build_hardware_context(r, &user_input, &board_names, rag_limit)) .unwrap_or_default(); let context = format!("{mem_context}{hw_context}"); let enriched = if context.is_empty() { - msg.content.clone() + user_input.clone() } else { - format!("{context}{}", msg.content) + format!("{context}{user_input}") }; history.push(ChatMessage::user(&enriched)); @@ -1116,7 +1130,11 @@ pub async fn run( } }; final_output = response.clone(); - println!("\n{response}\n"); + if let Err(e) = + crate::channels::Channel::send(&cli, &format!("\n{response}\n"), "user").await + { + eprintln!("\nError sending CLI response: {e}\n"); + } observer.record_event(&ObserverEvent::TurnComplete); // Auto-compaction before hard trimming to preserve long-context signal. @@ -1139,8 +1157,6 @@ pub async fn run( .await; } } - - listen_handle.abort(); } let duration = start.elapsed(); diff --git a/src/approval/mod.rs b/src/approval/mod.rs index c673b46..5099d9b 100644 --- a/src/approval/mod.rs +++ b/src/approval/mod.rs @@ -201,20 +201,10 @@ fn summarize_args(args: &serde_json::Value) -> String { .iter() .map(|(k, v)| { let val = match v { - serde_json::Value::String(s) => { - if s.len() > 80 { - format!("{}…", &s[..77]) - } else { - s.clone() - } - } + serde_json::Value::String(s) => truncate_for_summary(s, 80), other => { let s = other.to_string(); - if s.len() > 80 { - format!("{}…", &s[..77]) - } else { - s - } + truncate_for_summary(&s, 80) } }; format!("{k}: {val}") @@ -224,15 +214,21 @@ fn summarize_args(args: &serde_json::Value) -> String { } other => { let s = other.to_string(); - if s.len() > 120 { - format!("{}…", &s[..117]) - } else { - s - } + truncate_for_summary(&s, 120) } } } +fn truncate_for_summary(input: &str, max_chars: usize) -> String { + let mut chars = input.chars(); + let truncated: String = chars.by_ref().take(max_chars).collect(); + if chars.next().is_some() { + format!("{truncated}…") + } else { + input.to_string() + } +} + // ── Tests ──────────────────────────────────────────────────────── #[cfg(test)] @@ -404,6 +400,15 @@ mod tests { assert!(summary.len() < 200); } + #[test] + fn summarize_args_unicode_safe_truncation() { + let long_val = "🦀".repeat(120); + let args = serde_json::json!({"content": long_val}); + let summary = summarize_args(&args); + assert!(summary.contains("content:")); + assert!(summary.contains('…')); + } + #[test] fn summarize_args_non_object() { let args = serde_json::json!("just a string");