From 2dee4072a30e9456ec4727a10113c28e1213f614 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Wed, 8 Apr 2026 14:10:24 -0700 Subject: [PATCH] feat: add ecc2 worktree patch previews --- ecc2/src/main.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/ecc2/src/main.rs b/ecc2/src/main.rs index 6dd04d79..dee546b2 100644 --- a/ecc2/src/main.rs +++ b/ecc2/src/main.rs @@ -193,6 +193,9 @@ enum Commands { /// Emit machine-readable JSON instead of the human summary #[arg(long)] json: bool, + /// Include a bounded patch preview when a worktree is attached + #[arg(long)] + patch: bool, /// Return a non-zero exit code when the worktree needs attention #[arg(long)] check: bool, @@ -643,6 +646,7 @@ async fn main() -> Result<()> { Some(Commands::WorktreeStatus { session_id, json, + patch, check, }) => { let id = session_id.unwrap_or_else(|| "latest".to_string()); @@ -650,7 +654,7 @@ async fn main() -> Result<()> { let session = db .get_session(&resolved_id)? .ok_or_else(|| anyhow::anyhow!("Session not found: {resolved_id}"))?; - let report = build_worktree_status_report(&session)?; + let report = build_worktree_status_report(&session, patch)?; if json { println!("{}", serde_json::to_string_pretty(&report)?); } else { @@ -890,16 +894,18 @@ struct WorktreeStatusReport { session_state: String, health: String, check_exit_code: i32, + patch_included: bool, attached: bool, path: Option, branch: Option, base_branch: Option, diff_summary: Option, file_preview: Vec, + patch_preview: Option, merge_readiness: Option, } -fn build_worktree_status_report(session: &session::Session) -> Result { +fn build_worktree_status_report(session: &session::Session, include_patch: bool) -> Result { let Some(worktree) = session.worktree.as_ref() else { return Ok(WorktreeStatusReport { session_id: session.id.clone(), @@ -907,18 +913,25 @@ fn build_worktree_status_report(session: &session::Session) -> Result ("conflicted".to_string(), 2), @@ -932,12 +945,14 @@ fn build_worktree_status_report(session: &session::Session) -> Result "ready".to_string(), @@ -984,6 +999,14 @@ fn format_worktree_status_human(report: &WorktreeStatusReport) -> String { lines.push(format!("- conflict {conflict}")); } } + if report.patch_included { + if let Some(patch_preview) = report.patch_preview.as_ref() { + lines.push("Patch preview".to_string()); + lines.push(patch_preview.clone()); + } else { + lines.push("Patch preview unavailable".to_string()); + } + } lines.join("\n") } @@ -1228,10 +1251,12 @@ mod tests { Some(Commands::WorktreeStatus { session_id, json, + patch, check, }) => { assert_eq!(session_id.as_deref(), Some("planner")); assert!(!json); + assert!(!patch); assert!(!check); } _ => panic!("expected worktree-status subcommand"), @@ -1247,10 +1272,33 @@ mod tests { Some(Commands::WorktreeStatus { session_id, json, + patch, check, }) => { assert_eq!(session_id, None); assert!(json); + assert!(!patch); + assert!(!check); + } + _ => panic!("expected worktree-status subcommand"), + } + } + + #[test] + fn cli_parses_worktree_status_patch_flag() { + let cli = Cli::try_parse_from(["ecc", "worktree-status", "--patch"]) + .expect("worktree-status --patch should parse"); + + match cli.command { + Some(Commands::WorktreeStatus { + session_id, + json, + patch, + check, + }) => { + assert_eq!(session_id, None); + assert!(!json); + assert!(patch); assert!(!check); } _ => panic!("expected worktree-status subcommand"), @@ -1266,10 +1314,12 @@ mod tests { Some(Commands::WorktreeStatus { session_id, json, + patch, check, }) => { assert_eq!(session_id, None); assert!(!json); + assert!(!patch); assert!(check); } _ => panic!("expected worktree-status subcommand"), @@ -1284,12 +1334,14 @@ mod tests { session_state: "running".to_string(), health: "conflicted".to_string(), check_exit_code: 2, + patch_included: true, attached: true, path: Some("/tmp/ecc/wt-1".to_string()), branch: Some("ecc/deadbeefcafefeed".to_string()), base_branch: Some("main".to_string()), diff_summary: Some("Branch 1 file changed, 2 insertions(+)".to_string()), file_preview: vec!["Branch M README.md".to_string()], + patch_preview: Some("--- Branch diff vs main ---\n+hello".to_string()), merge_readiness: Some(WorktreeMergeReadinessReport { status: "conflicted".to_string(), summary: "Merge blocked by 1 conflict(s): README.md".to_string(), @@ -1304,6 +1356,8 @@ mod tests { assert!(text.contains("Branch M README.md")); assert!(text.contains("Merge blocked by 1 conflict(s): README.md")); assert!(text.contains("- conflict README.md")); + assert!(text.contains("Patch preview")); + assert!(text.contains("--- Branch diff vs main ---")); } #[test] @@ -1314,12 +1368,14 @@ mod tests { session_state: "stopped".to_string(), health: "clear".to_string(), check_exit_code: 0, + patch_included: true, attached: false, path: None, branch: None, base_branch: None, diff_summary: None, file_preview: Vec::new(), + patch_preview: None, merge_readiness: None, }; @@ -1338,12 +1394,14 @@ mod tests { session_state: "idle".to_string(), health: "clear".to_string(), check_exit_code: 0, + patch_included: false, attached: false, path: None, branch: None, base_branch: None, diff_summary: None, file_preview: Vec::new(), + patch_preview: None, merge_readiness: None, }; let in_progress = WorktreeStatusReport { @@ -1352,12 +1410,14 @@ mod tests { session_state: "running".to_string(), health: "in_progress".to_string(), check_exit_code: 1, + patch_included: false, attached: true, path: Some("/tmp/ecc/wt-2".to_string()), branch: Some("ecc/b".to_string()), base_branch: Some("main".to_string()), diff_summary: Some("Branch 1 file changed".to_string()), file_preview: vec!["Branch M README.md".to_string()], + patch_preview: None, merge_readiness: Some(WorktreeMergeReadinessReport { status: "ready".to_string(), summary: "Merge ready into main".to_string(), @@ -1370,12 +1430,14 @@ mod tests { session_state: "running".to_string(), health: "conflicted".to_string(), check_exit_code: 2, + patch_included: false, attached: true, path: Some("/tmp/ecc/wt-3".to_string()), branch: Some("ecc/c".to_string()), base_branch: Some("main".to_string()), diff_summary: Some("Branch 1 file changed".to_string()), file_preview: vec!["Branch M README.md".to_string()], + patch_preview: None, merge_readiness: Some(WorktreeMergeReadinessReport { status: "conflicted".to_string(), summary: "Merge blocked by 1 conflict(s): README.md".to_string(),