From bc5c1bdf616b9014c5b3b7534b4638b9a0d5d044 Mon Sep 17 00:00:00 2001 From: "Dhanji R. Prasanna" Date: Thu, 5 Feb 2026 14:38:18 +1100 Subject: [PATCH] Fix plan UI formatting to handle Vec and display elegantly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update ChecksCompact to use Vec for negative/boundary fields - Add progress bar visualization showing done/doing/blocked/todo counts - Show evidence for done items, checks for active items - Display all negative and boundary checks (not just first) - Add proper tree structure with └/├ prefixes - Truncate long descriptions and evidence paths - Add file path display with 📄 icon --- crates/g3-cli/src/ui_writer_impl.rs | 117 ++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 25 deletions(-) diff --git a/crates/g3-cli/src/ui_writer_impl.rs b/crates/g3-cli/src/ui_writer_impl.rs index ef9e299..fd78f3b 100644 --- a/crates/g3-cli/src/ui_writer_impl.rs +++ b/crates/g3-cli/src/ui_writer_impl.rs @@ -726,15 +726,22 @@ impl UiWriter for ConsoleUiWriter { description: String, state: String, touches: Vec, - checks: ChecksCompact, + #[serde(default)] + checks: Option, + #[serde(default)] + evidence: Vec, + #[serde(default)] + notes: Option, } #[derive(serde::Deserialize)] struct ChecksCompact { happy: CheckCompact, - negative: CheckCompact, - boundary: CheckCompact, + #[serde(default)] + negative: Vec, + #[serde(default)] + boundary: Vec, } - #[derive(serde::Deserialize)] + #[derive(serde::Deserialize, Clone)] struct CheckCompact { desc: String, #[allow(dead_code)] @@ -742,23 +749,35 @@ impl UiWriter for ConsoleUiWriter { } if let Ok(plan) = serde_yaml::from_str::(yaml) { - // Header with plan info + // Count items by state for summary + let done_count = plan.items.iter().filter(|i| i.state == "done").count(); + let doing_count = plan.items.iter().filter(|i| i.state == "doing").count(); + let blocked_count = plan.items.iter().filter(|i| i.state == "blocked").count(); + let todo_count = plan.items.iter().filter(|i| i.state == "todo").count(); + let total = plan.items.len(); + + // Header with plan info and progress let approved_str = if let Some(rev) = plan.approved_revision { format!(" \x1b[32m✓ approved@{}\x1b[0m", rev) } else { " \x1b[33m⚠ NOT APPROVED\x1b[0m".to_string() }; - println!(" \x1b[2m●\x1b[0m {}{: ("?", "\x1b[0m"), }; - // Item line: state icon, ID, description - let desc_style = if item.state == "done" { "\x1b[9m" } else { "" }; // strikethrough if done - println!(" \x1b[2m{}\x1b[0m {}{} \x1b[1m{}\x1b[0m {}{}\x1b[0m", - item_prefix, state_color, state_icon, item.id, desc_style, item.description); + // Item line with tree structure + let item_prefix = if is_last_item { "└" } else { "├" }; + let child_prefix = if is_last_item { " " } else { "│" }; - // Touches (dimmed) - let touches_str = item.touches.join(", "); - println!(" \x1b[2m{} → {}\x1b[0m", child_prefix, touches_str); + // Truncate description if too long + let max_desc_len = 70; + let desc_display = if item.description.chars().count() > max_desc_len { + let truncate_at = item.description + .char_indices() + .nth(max_desc_len - 3) + .map(|(i, _)| i) + .unwrap_or(item.description.len()); + format!("{}...", &item.description[..truncate_at]) + } else { + item.description.clone() + }; - // Checks (dimmed, compact) - println!(" \x1b[2m{} ✓ happy: {}\x1b[0m", child_prefix, item.checks.happy.desc); - println!(" \x1b[2m{} ✗ negative: {}\x1b[0m", child_prefix, item.checks.negative.desc); - println!(" \x1b[2m{} ◇ boundary: {}\x1b[0m", last_child_prefix, item.checks.boundary.desc); + // Item line: state icon, ID, description (strikethrough if done) + let desc_style = if item.state == "done" { "\x1b[9m\x1b[2m" } else { "" }; + let desc_reset = if item.state == "done" { "\x1b[0m" } else { "" }; + println!(" \x1b[2m{}\x1b[0m {}{}\x1b[0m \x1b[1m{}\x1b[0m {}{}{}", + item_prefix, state_color, state_icon, item.id, desc_style, desc_display, desc_reset); + + // For done items, show evidence compactly; for others show touches and checks + if item.state == "done" { + // Show evidence for done items + if !item.evidence.is_empty() { + let evidence_str = item.evidence.iter() + .map(|e| { + // Shorten long evidence paths + if e.len() > 40 { + let truncate_at = e.char_indices().nth(37).map(|(i, _)| i).unwrap_or(e.len()); + format!("{}...", &e[..truncate_at]) + } else { + e.clone() + } + }) + .collect::>() + .join(", "); + println!(" \x1b[2m{} 📎 {}\x1b[0m", child_prefix, evidence_str); + } + } else { + // Show touches for non-done items + let touches_str = item.touches.join(", "); + println!(" \x1b[2m{} → {}\x1b[0m", child_prefix, touches_str); + + // Show checks if present (compact format) + if let Some(ref checks) = item.checks { + // Happy check (always single) + println!(" \x1b[2m{} \x1b[32m✓\x1b[0m\x1b[2m {}\x1b[0m", child_prefix, checks.happy.desc); + + // Negative checks (can be multiple) + for neg in &checks.negative { + println!(" \x1b[2m{} \x1b[31m✗\x1b[0m\x1b[2m {}\x1b[0m", child_prefix, neg.desc); + } + + // Boundary checks (can be multiple) + for bnd in &checks.boundary { + println!(" \x1b[2m{} \x1b[33m◇\x1b[0m\x1b[2m {}\x1b[0m", child_prefix, bnd.desc); + } + } + } } // File path link at the end if let Some(path) = plan_file_path { - println!(); // Blank line gap - println!(" \x1b[2m-> {}\x1b[0m", path); + println!(" \x1b[2m📄 {}\x1b[0m", path); } // Add blank line after content for readability