From 751ff943aa2c1dc6a081e482ffd0d6745441cd81 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Fri, 12 Jun 2026 22:55:11 -0700 Subject: [PATCH] fix(tui): group strategy context under work checklist --- crates/tui/src/tui/sidebar.rs | 129 ++++++++++++++++++++++++++++++++-- docs/TOOL_SURFACE.md | 6 ++ 2 files changed, 129 insertions(+), 6 deletions(-) diff --git a/crates/tui/src/tui/sidebar.rs b/crates/tui/src/tui/sidebar.rs index c09e40e4..d542af97 100644 --- a/crates/tui/src/tui/sidebar.rs +++ b/crates/tui/src/tui/sidebar.rs @@ -185,6 +185,10 @@ pub(crate) struct SidebarWorkSummary { } impl SidebarWorkSummary { + fn checklist_is_primary(&self) -> bool { + !self.checklist_items.is_empty() + } + fn has_strategy(&self) -> bool { self.strategy_explanation .as_deref() @@ -450,7 +454,7 @@ fn work_panel_hover_texts( } if summary.has_strategy() && texts.len() < max_rows { - if summary.checklist_items.is_empty() && !summary.strategy_steps.is_empty() { + if !summary.checklist_is_primary() && !summary.strategy_steps.is_empty() { let (pending, in_progress, completed) = summary.strategy_counts(); let total = pending + in_progress + completed; texts.push(format!( @@ -458,7 +462,7 @@ fn work_panel_hover_texts( summary.strategy_progress_percent() )); } else { - texts.push("Strategy metadata".to_string()); + texts.push(work_strategy_context_label(summary).to_string()); } if let Some(explanation) = summary.strategy_explanation.as_deref() @@ -476,7 +480,15 @@ fn work_panel_hover_texts( StepStatus::InProgress => "[~]", StepStatus::Completed => "[✓]", }; - let mut text = format!("{prefix} {}", step.text); + let mut text = if summary.checklist_is_primary() { + format!( + "{} {}", + strategy_context_step_prefix(&step.status), + step.text + ) + } else { + format!("{prefix} {}", step.text) + }; if !step.elapsed.is_empty() { let _ = write!(text, " ({})", step.elapsed); } @@ -670,7 +682,8 @@ fn push_work_strategy_lines( return; } - if summary.checklist_items.is_empty() && !summary.strategy_steps.is_empty() { + let checklist_is_primary = summary.checklist_is_primary(); + if !checklist_is_primary && !summary.strategy_steps.is_empty() { let (pending, in_progress, completed) = summary.strategy_counts(); let total = pending + in_progress + completed; lines.push(Line::from(vec![ @@ -689,7 +702,7 @@ fn push_work_strategy_lines( ])); } else { lines.push(Line::from(Span::styled( - "Strategy metadata", + work_strategy_context_label(summary), Style::default().fg(theme.plan_summary_color).bold(), ))); } @@ -712,7 +725,15 @@ fn push_work_strategy_lines( StepStatus::InProgress => ("[~]", theme.plan_in_progress_color), StepStatus::Completed => ("[✓]", theme.plan_completed_color), }; - let mut text = format!("{prefix} {}", step.text); + let (text_prefix, color) = if checklist_is_primary { + ( + strategy_context_step_prefix(&step.status), + strategy_context_step_color(&step.status, theme), + ) + } else { + (prefix, color) + }; + let mut text = format!("{text_prefix} {}", step.text); if !step.elapsed.is_empty() { let _ = write!(text, " ({})", step.elapsed); } @@ -731,6 +752,30 @@ fn push_work_strategy_lines( } } +fn work_strategy_context_label(summary: &SidebarWorkSummary) -> &'static str { + if summary.checklist_is_primary() { + "Strategy context" + } else { + "Strategy metadata" + } +} + +fn strategy_context_step_prefix(status: &StepStatus) -> &'static str { + match status { + StepStatus::Pending => "phase next:", + StepStatus::InProgress => "phase now:", + StepStatus::Completed => "phase done:", + } +} + +fn strategy_context_step_color(status: &StepStatus, theme: &Theme) -> ratatui::style::Color { + match status { + StepStatus::Pending => theme.plan_pending_color, + StepStatus::InProgress => theme.plan_in_progress_color, + StepStatus::Completed => theme.plan_summary_color, + } +} + #[must_use] fn work_panel_empty_hint(content_width: usize) -> String { truncate_line_to_width("No active work", content_width) @@ -2808,6 +2853,78 @@ mod tests { !text.iter().any(|line| line.contains("50% complete")), "strategy progress must not render as a second progress bar when checklist exists: {text:?}" ); + assert!( + text.iter().any(|line| line == "Strategy context"), + "strategy should be grouped as context for the checklist: {text:?}" + ); + assert!( + text.iter() + .any(|line| line.contains("phase done: Simplify sidebar")), + "completed strategy steps should render as phase context: {text:?}" + ); + assert!( + text.iter() + .any(|line| line.contains("phase next: Update prompts")), + "pending strategy steps should render as phase context: {text:?}" + ); + assert!( + !text + .iter() + .any(|line| line.contains("[✓] Simplify sidebar")) + && !text.iter().any(|line| line.contains("[ ] Update prompts")), + "strategy rows must not look like a second checklist when Work checklist exists: {text:?}" + ); + } + + #[test] + fn work_panel_hover_renders_strategy_as_context_when_checklist_exists() { + let summary = SidebarWorkSummary { + checklist_completion_pct: 0, + checklist_items: vec![SidebarWorkChecklistItem { + id: 1, + content: "Wire tool execution".to_string(), + status: TodoStatus::InProgress, + }], + strategy_explanation: Some("Keep strategy and checklist linked".to_string()), + strategy_steps: vec![ + SidebarWorkStrategyStep { + text: "Map phase boundaries".to_string(), + status: StepStatus::Completed, + elapsed: String::new(), + }, + SidebarWorkStrategyStep { + text: "Implement counted work".to_string(), + status: StepStatus::InProgress, + elapsed: String::new(), + }, + ], + ..SidebarWorkSummary::default() + }; + + let hover = work_panel_hover_texts(&summary, 80, 16); + + assert!( + hover.iter().any(|line| line == "Strategy context"), + "hover should name strategy as context when checklist exists: {hover:?}" + ); + assert!( + hover + .iter() + .any(|line| line.contains("phase done: Map phase boundaries")), + "hover strategy rows should be phase context: {hover:?}" + ); + assert!( + hover + .iter() + .any(|line| line.contains("phase now: Implement counted work")), + "hover should expose the active strategy phase without checklist markers: {hover:?}" + ); + assert!( + !hover + .iter() + .any(|line| line.contains("[✓] Map phase boundaries")), + "hover strategy rows must not look like a second checklist: {hover:?}" + ); } #[test] diff --git a/docs/TOOL_SURFACE.md b/docs/TOOL_SURFACE.md index a016cc7b..ace2bec8 100644 --- a/docs/TOOL_SURFACE.md +++ b/docs/TOOL_SURFACE.md @@ -128,6 +128,12 @@ confirmation prompt, `/relay`, and fork-state handoff all render the same artifact so a plan can be reviewed, accepted, revised, replayed, or delegated without losing its source context. +Strategy metadata and checklist work are one Work surface. Treat +`update_plan` as phase context and sequencing intent, while `checklist_*` +remains the counted task ledger. When both exist, UI projections should group +strategy around the checklist instead of showing two peer checklist/progress +systems for the same run. + ### Verification gates and artifacts | Tool | Niche |