Fix plan UI formatting to handle Vec<Check> and display elegantly
- Update ChecksCompact to use Vec<CheckCompact> 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
This commit is contained in:
@@ -726,15 +726,22 @@ impl UiWriter for ConsoleUiWriter {
|
|||||||
description: String,
|
description: String,
|
||||||
state: String,
|
state: String,
|
||||||
touches: Vec<String>,
|
touches: Vec<String>,
|
||||||
checks: ChecksCompact,
|
#[serde(default)]
|
||||||
|
checks: Option<ChecksCompact>,
|
||||||
|
#[serde(default)]
|
||||||
|
evidence: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
notes: Option<String>,
|
||||||
}
|
}
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct ChecksCompact {
|
struct ChecksCompact {
|
||||||
happy: CheckCompact,
|
happy: CheckCompact,
|
||||||
negative: CheckCompact,
|
#[serde(default)]
|
||||||
boundary: CheckCompact,
|
negative: Vec<CheckCompact>,
|
||||||
|
#[serde(default)]
|
||||||
|
boundary: Vec<CheckCompact>,
|
||||||
}
|
}
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize, Clone)]
|
||||||
struct CheckCompact {
|
struct CheckCompact {
|
||||||
desc: String,
|
desc: String,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -742,23 +749,35 @@ impl UiWriter for ConsoleUiWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(plan) = serde_yaml::from_str::<PlanCompact>(yaml) {
|
if let Ok(plan) = serde_yaml::from_str::<PlanCompact>(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 {
|
let approved_str = if let Some(rev) = plan.approved_revision {
|
||||||
format!(" \x1b[32m✓ approved@{}\x1b[0m", rev)
|
format!(" \x1b[32m✓ approved@{}\x1b[0m", rev)
|
||||||
} else {
|
} else {
|
||||||
" \x1b[33m⚠ NOT APPROVED\x1b[0m".to_string()
|
" \x1b[33m⚠ NOT APPROVED\x1b[0m".to_string()
|
||||||
};
|
};
|
||||||
println!(" \x1b[2m●\x1b[0m {}{:<width$}\x1b[0m \x1b[2m|\x1b[0m \x1b[36m{}\x1b[0m rev {}{}",
|
|
||||||
tool_color, tool_name, plan.plan_id, plan.revision, approved_str, width = TOOL_NAME_PADDING);
|
// Progress bar visualization
|
||||||
|
let progress_bar = format!(
|
||||||
|
"\x1b[32m{}\x1b[33m{}\x1b[31m{}\x1b[2m{}\x1b[0m",
|
||||||
|
"■".repeat(done_count),
|
||||||
|
"■".repeat(doing_count),
|
||||||
|
"■".repeat(blocked_count),
|
||||||
|
"□".repeat(todo_count)
|
||||||
|
);
|
||||||
|
|
||||||
|
println!(" \x1b[2m●\x1b[0m {}{:<width$}\x1b[0m \x1b[2m|\x1b[0m \x1b[1;36m{}\x1b[0m{} \x1b[2m[{}/{}]\x1b[0m {}",
|
||||||
|
tool_color, tool_name, plan.plan_id, approved_str, done_count, total, progress_bar, width = TOOL_NAME_PADDING);
|
||||||
|
|
||||||
let items_len = plan.items.len();
|
let items_len = plan.items.len();
|
||||||
for (i, item) in plan.items.iter().enumerate() {
|
for (i, item) in plan.items.iter().enumerate() {
|
||||||
let is_last_item = i == items_len - 1;
|
let is_last_item = i == items_len - 1;
|
||||||
// All items use ├, sub-lines use │
|
|
||||||
// Only the very last sub-line (boundary of last item) uses └
|
|
||||||
let item_prefix = "├";
|
|
||||||
let child_prefix = "│";
|
|
||||||
let last_child_prefix = if is_last_item { "└" } else { "│" };
|
|
||||||
|
|
||||||
// State indicator: □ = todo, ◐ = doing, ■ = done, ⊘ = blocked
|
// State indicator: □ = todo, ◐ = doing, ■ = done, ⊘ = blocked
|
||||||
let (state_icon, state_color) = match item.state.as_str() {
|
let (state_icon, state_color) = match item.state.as_str() {
|
||||||
@@ -769,25 +788,73 @@ impl UiWriter for ConsoleUiWriter {
|
|||||||
_ => ("?", "\x1b[0m"),
|
_ => ("?", "\x1b[0m"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Item line: state icon, ID, description
|
// Item line with tree structure
|
||||||
let desc_style = if item.state == "done" { "\x1b[9m" } else { "" }; // strikethrough if done
|
let item_prefix = if is_last_item { "└" } else { "├" };
|
||||||
println!(" \x1b[2m{}\x1b[0m {}{} \x1b[1m{}\x1b[0m {}{}\x1b[0m",
|
let child_prefix = if is_last_item { " " } else { "│" };
|
||||||
item_prefix, state_color, state_icon, item.id, desc_style, item.description);
|
|
||||||
|
|
||||||
// Touches (dimmed)
|
// Truncate description if too long
|
||||||
let touches_str = item.touches.join(", ");
|
let max_desc_len = 70;
|
||||||
println!(" \x1b[2m{} → {}\x1b[0m", child_prefix, touches_str);
|
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)
|
// Item line: state icon, ID, description (strikethrough if done)
|
||||||
println!(" \x1b[2m{} ✓ happy: {}\x1b[0m", child_prefix, item.checks.happy.desc);
|
let desc_style = if item.state == "done" { "\x1b[9m\x1b[2m" } else { "" };
|
||||||
println!(" \x1b[2m{} ✗ negative: {}\x1b[0m", child_prefix, item.checks.negative.desc);
|
let desc_reset = if item.state == "done" { "\x1b[0m" } else { "" };
|
||||||
println!(" \x1b[2m{} ◇ boundary: {}\x1b[0m", last_child_prefix, item.checks.boundary.desc);
|
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::<Vec<_>>()
|
||||||
|
.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
|
// File path link at the end
|
||||||
if let Some(path) = plan_file_path {
|
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
|
// Add blank line after content for readability
|
||||||
|
|||||||
Reference in New Issue
Block a user