diff --git a/.gitignore b/.gitignore index db23daca..b326c650 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ apps/ # Claude Code runtime artifacts .claude/scheduled_tasks.lock .claude/worktrees/ +.worktrees/ diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index 2db1cb7d..ae68e81b 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -3298,12 +3298,11 @@ impl Engine { "tool_id": tool_id.clone(), "tool_name": tool_name.clone(), })); - let approval_key = - crate::tools::approval_cache::build_approval_key( - &tool_name, - &tool_input, - ) - .0; + let approval_key = crate::tools::approval_cache::build_approval_key( + &tool_name, + &tool_input, + ) + .0; let _ = self .tx_event .send(Event::ApprovalRequired { diff --git a/crates/tui/src/error_taxonomy.rs b/crates/tui/src/error_taxonomy.rs index 724078cd..16cd984b 100644 --- a/crates/tui/src/error_taxonomy.rs +++ b/crates/tui/src/error_taxonomy.rs @@ -319,9 +319,7 @@ pub enum ClientError { retryable: bool, }, /// Generic internal error that doesn't fit a provider taxonomy. - Internal { - envelope: ErrorEnvelope, - }, + Internal { envelope: ErrorEnvelope }, } impl ClientError { @@ -412,25 +410,15 @@ impl std::error::Error for ClientError {} #[derive(Debug, Clone)] pub enum StreamError { /// Stream stalled — no chunk received within the idle timeout. - Stall { - timeout_secs: u64, - }, + Stall { timeout_secs: u64 }, /// Chunk decode / JSON parse failure. - Decode { - message: String, - }, + Decode { message: String }, /// Stream exceeded content size limit. - Overflow { - limit_bytes: usize, - }, + Overflow { limit_bytes: usize }, /// Stream exceeded wall‑clock duration limit. - DurationLimit { - limit_secs: u64, - }, + DurationLimit { limit_secs: u64 }, /// Transport error from the underlying SSE connection. - Transport { - message: String, - }, + Transport { message: String }, } impl StreamError { @@ -439,29 +427,19 @@ impl StreamError { pub fn into_client_error(self) -> ClientError { match self { Self::Stall { timeout_secs } => { - ClientError::stream( - format!("Stream stalled after {timeout_secs}s idle"), - true, - ) + ClientError::stream(format!("Stream stalled after {timeout_secs}s idle"), true) } Self::Decode { message } => { ClientError::stream(format!("Stream decode error: {message}"), true) } Self::Overflow { limit_bytes } => { - ClientError::stream( - format!("Stream exceeded {limit_bytes} bytes limit"), - false, - ) - } - Self::DurationLimit { limit_secs } => { - ClientError::stream( - format!("Stream exceeded {limit_secs}s duration limit"), - false, - ) - } - Self::Transport { message } => { - ClientError::stream(message, true) + ClientError::stream(format!("Stream exceeded {limit_bytes} bytes limit"), false) } + Self::DurationLimit { limit_secs } => ClientError::stream( + format!("Stream exceeded {limit_secs}s duration limit"), + false, + ), + Self::Transport { message } => ClientError::stream(message, true), } } } diff --git a/crates/tui/src/prompts.rs b/crates/tui/src/prompts.rs index 3c61edba..cda8bff0 100644 --- a/crates/tui/src/prompts.rs +++ b/crates/tui/src/prompts.rs @@ -134,9 +134,8 @@ pub fn compose_prompt(mode: AppMode, personality: Personality) -> String { approval_prompt(mode).trim(), ]; - let mut out = String::with_capacity( - parts.iter().map(|p| p.len()).sum::() + (parts.len() - 1) * 2, - ); + let mut out = + String::with_capacity(parts.iter().map(|p| p.len()).sum::() + (parts.len() - 1) * 2); for (i, part) in parts.iter().enumerate() { if i > 0 { out.push('\n'); @@ -335,9 +334,13 @@ mod tests { #[test] fn each_mode_gets_correct_approval() { - assert!(compose_prompt(AppMode::Agent, Personality::Calm).contains("Approval Policy: Suggest")); + assert!( + compose_prompt(AppMode::Agent, Personality::Calm).contains("Approval Policy: Suggest") + ); assert!(compose_prompt(AppMode::Yolo, Personality::Calm).contains("Approval Policy: Auto")); - assert!(compose_prompt(AppMode::Plan, Personality::Calm).contains("Approval Policy: Never")); + assert!( + compose_prompt(AppMode::Plan, Personality::Calm).contains("Approval Policy: Never") + ); } #[test] diff --git a/crates/tui/src/runtime_threads.rs b/crates/tui/src/runtime_threads.rs index 4f46763b..3b4dfb54 100644 --- a/crates/tui/src/runtime_threads.rs +++ b/crates/tui/src/runtime_threads.rs @@ -3177,7 +3177,8 @@ mod tests { harness .tx_event - .send(EngineEvent::ApprovalRequired { approval_key: "test_key".to_string(), + .send(EngineEvent::ApprovalRequired { + approval_key: "test_key".to_string(), id: "tool_stale".to_string(), tool_name: "exec_command".to_string(), description: "stale approval".to_string(), diff --git a/crates/tui/src/tools/approval_cache.rs b/crates/tui/src/tools/approval_cache.rs index ad7ab2ba..2c2c7af3 100644 --- a/crates/tui/src/tools/approval_cache.rs +++ b/crates/tui/src/tools/approval_cache.rs @@ -121,8 +121,11 @@ pub fn build_approval_key(tool_name: &str, input: &serde_json::Value) -> Approva let paths_hash = hash_patch_paths(input); format!("patch:{paths_hash}") } - "exec_shell" | "exec_shell_wait" | "exec_shell_interact" - | "exec_wait" | "exec_interact" => { + "exec_shell" + | "exec_shell_wait" + | "exec_shell_interact" + | "exec_wait" + | "exec_interact" => { let prefix = command_prefix(input); format!("shell:{prefix}") } @@ -137,10 +140,7 @@ pub fn build_approval_key(tool_name: &str, input: &serde_json::Value) -> Approva /// Extract the first three non‑flag tokens from the command string. fn command_prefix(input: &serde_json::Value) -> String { - let cmd = input - .get("command") - .and_then(|v| v.as_str()) - .unwrap_or(""); + let cmd = input.get("command").and_then(|v| v.as_str()).unwrap_or(""); let tokens: Vec<&str> = cmd .split_whitespace() .filter(|t| !t.starts_with('-')) @@ -190,10 +190,7 @@ fn hash_patch_paths(input: &serde_json::Value) -> String { /// Parse the host portion from a URL input. fn parse_host(input: &serde_json::Value) -> String { - let url = input - .get("url") - .and_then(|v| v.as_str()) - .unwrap_or(""); + let url = input.get("url").and_then(|v| v.as_str()).unwrap_or(""); if let Ok(parsed) = reqwest::Url::parse(url) { parsed.host_str().unwrap_or(url).to_string() diff --git a/crates/tui/src/tools/registry.rs b/crates/tui/src/tools/registry.rs index 1903da49..76123411 100644 --- a/crates/tui/src/tools/registry.rs +++ b/crates/tui/src/tools/registry.rs @@ -607,10 +607,7 @@ impl ToolSpec for McpToolAdapter { fn description(&self) -> &str { // McpTool.description is Option; fall back to the // prefixed name when absent. - self.tool - .description - .as_deref() - .unwrap_or(&self.name) + self.tool.description.as_deref().unwrap_or(&self.name) } fn input_schema(&self) -> Value { @@ -645,18 +642,13 @@ impl ToolSpec for McpToolAdapter { !keep_loaded } - async fn execute( - &self, - input: Value, - _context: &ToolContext, - ) -> Result { + async fn execute(&self, input: Value, _context: &ToolContext) -> Result { let mut pool = self.pool.lock().await; let result = pool .call_tool(&self.name, input) .await .map_err(|e| ToolError::execution_failed(format!("MCP tool failed: {e}")))?; - let content = - serde_json::to_string_pretty(&result).unwrap_or_else(|_| result.to_string()); + let content = serde_json::to_string_pretty(&result).unwrap_or_else(|_| result.to_string()); Ok(ToolResult::success(content)) } } diff --git a/crates/tui/src/tui/approval.rs b/crates/tui/src/tui/approval.rs index 1a2d62da..885c04ba 100644 --- a/crates/tui/src/tui/approval.rs +++ b/crates/tui/src/tui/approval.rs @@ -668,8 +668,13 @@ mod tests { #[test] fn test_approval_request_new() { let params = json!({"path": "src/main.rs", "content": "test"}); - let request = - ApprovalRequest::new("test-id", "write_file", "Write a file to disk", ¶ms, "test_key"); + let request = ApprovalRequest::new( + "test-id", + "write_file", + "Write a file to disk", + ¶ms, + "test_key", + ); assert_eq!(request.id, "test-id"); assert_eq!(request.tool_name, "write_file"); @@ -682,8 +687,13 @@ mod tests { // Create params with a very long string let long_content = "x".repeat(300); let params = json!({"path": "src/main.rs", "content": long_content}); - let request = - ApprovalRequest::new("test-id", "write_file", "Write a file to disk", ¶ms, "test_key"); + let request = ApprovalRequest::new( + "test-id", + "write_file", + "Write a file to disk", + ¶ms, + "test_key", + ); let display = request.params_display(); // Should be truncated to around 200 chars @@ -694,8 +704,13 @@ mod tests { #[test] fn test_approval_request_params_display_short() { let params = json!({"path": "src/main.rs"}); - let request = - ApprovalRequest::new("test-id", "read_file", "Read a file from disk", ¶ms, "test_key"); + let request = ApprovalRequest::new( + "test-id", + "read_file", + "Read a file from disk", + ¶ms, + "test_key", + ); let display = request.params_display(); assert!(display.contains("src/main.rs")); @@ -704,7 +719,13 @@ mod tests { #[test] fn test_approval_request_derives_impact_summary() { let params = json!({"cmd": "cargo test", "workdir": "/tmp/project"}); - let request = ApprovalRequest::new("test-id", "exec_shell", "Run a shell command", ¶ms, "test_key"); + let request = ApprovalRequest::new( + "test-id", + "exec_shell", + "Run a shell command", + ¶ms, + "test_key", + ); assert_eq!(request.category, ToolCategory::Shell); assert!( @@ -728,8 +749,13 @@ mod tests { #[test] fn test_approval_view_initial_state() { let params = json!({"path": "src/main.rs"}); - let request = - ApprovalRequest::new("test-id", "read_file", "Read a file from disk", ¶ms, "test_key"); + let request = ApprovalRequest::new( + "test-id", + "read_file", + "Read a file from disk", + ¶ms, + "test_key", + ); let view = ApprovalView::new(request.clone()); assert_eq!(view.selected, 0); @@ -739,8 +765,13 @@ mod tests { #[test] fn test_approval_view_navigation() { let params = json!({"path": "src/main.rs"}); - let request = - ApprovalRequest::new("test-id", "read_file", "Read a file from disk", ¶ms, "test_key"); + let request = ApprovalRequest::new( + "test-id", + "read_file", + "Read a file from disk", + ¶ms, + "test_key", + ); let mut view = ApprovalView::new(request); // Initially at 0 @@ -768,8 +799,13 @@ mod tests { #[test] fn test_approval_view_keybindings_decisions() { let params = json!({"path": "src/main.rs"}); - let request = - ApprovalRequest::new("test-id", "read_file", "Read a file from disk", ¶ms, "test_key"); + let request = ApprovalRequest::new( + "test-id", + "read_file", + "Read a file from disk", + ¶ms, + "test_key", + ); let mut view = ApprovalView::new(request.clone()); // Test 'y' -> Approved @@ -819,8 +855,13 @@ mod tests { #[test] fn test_approval_view_enter_uses_selected_option() { let params = json!({"path": "src/main.rs"}); - let request = - ApprovalRequest::new("test-id", "read_file", "Read a file from disk", ¶ms, "test_key"); + let request = ApprovalRequest::new( + "test-id", + "read_file", + "Read a file from disk", + ¶ms, + "test_key", + ); let mut view = ApprovalView::new(request); // Navigate to index 2 (Denied) @@ -842,8 +883,13 @@ mod tests { #[test] fn test_approval_view_navigation_keys() { let params = json!({"path": "src/main.rs"}); - let request = - ApprovalRequest::new("test-id", "read_file", "Read a file from disk", ¶ms, "test_key"); + let request = ApprovalRequest::new( + "test-id", + "read_file", + "Read a file from disk", + ¶ms, + "test_key", + ); let mut view = ApprovalView::new(request); // Test Up arrow @@ -866,8 +912,13 @@ mod tests { #[test] fn test_approval_view_view_params() { let params = json!({"path": "src/main.rs", "content": "test"}); - let request = - ApprovalRequest::new("test-id", "read_file", "Read a file from disk", ¶ms, "test_key"); + let request = ApprovalRequest::new( + "test-id", + "read_file", + "Read a file from disk", + ¶ms, + "test_key", + ); let mut view = ApprovalView::new(request.clone()); // Test 'v' to view params @@ -889,8 +940,13 @@ mod tests { #[test] fn test_approval_view_current_decision_mapping() { let params = json!({"path": "src/main.rs"}); - let request = - ApprovalRequest::new("test-id", "read_file", "Read a file from disk", ¶ms, "test_key"); + let request = ApprovalRequest::new( + "test-id", + "read_file", + "Read a file from disk", + ¶ms, + "test_key", + ); let mut view = ApprovalView::new(request); // Index 0 -> Approved diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index c3558b19..aaf9cc1a 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -771,8 +771,9 @@ async fn run_event_loop( description, approval_key, } => { - let session_approved = app.approval_session_approved.contains(&approval_key) - || app.approval_session_approved.contains(&tool_name); + let session_approved = + app.approval_session_approved.contains(&approval_key) + || app.approval_session_approved.contains(&tool_name); if session_approved || app.approval_mode == ApprovalMode::Auto { log_sensitive_event( "tool.approval.auto_approve", @@ -809,8 +810,13 @@ async fn run_event_loop( } // Create approval request and show overlay - let request = - ApprovalRequest::new(&id, &tool_name, &description, &tool_input, &approval_key); + let request = ApprovalRequest::new( + &id, + &tool_name, + &description, + &tool_input, + &approval_key, + ); log_sensitive_event( "tool.approval.prompted", serde_json::json!({