From 449312cf2b6ea542df2eec96f6485ec6af80d98f Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 3 May 2026 13:53:37 -0500 Subject: [PATCH] fix(sidebar): collapse empty Todos/Tasks/Agents panels in Auto layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-mode reserved 25% of the sidebar height for each of Plan / Todos / Tasks / Agents regardless of content, so on a typical 32-row sidebar each slot was ~8 rows. With Todos/Tasks/Agents empty (the common case when a goal is set but no checklist exists), Plan ended up with ~5 content rows of its 8-row slot consumed by header + token bar + separator, and steps got silently clipped — the user-reported "sidebar broken / Plan disappearing". Build the constraint list dynamically: include a slot only for panels that actually have content. Plan always renders (it owns the session-wide empty hint). Todos/Tasks/Agents collapse to zero rows when empty, letting the visible panels share the full height. --- crates/tui/src/tui/sidebar.rs | 85 ++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/crates/tui/src/tui/sidebar.rs b/crates/tui/src/tui/sidebar.rs index caf6a9ec..d07e399f 100644 --- a/crates/tui/src/tui/sidebar.rs +++ b/crates/tui/src/tui/sidebar.rs @@ -37,22 +37,7 @@ pub fn render_sidebar(f: &mut Frame, area: Rect, app: &App) { } match app.sidebar_focus { - SidebarFocus::Auto => { - let sections = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Min(6), - ]) - .split(area); - - render_sidebar_plan(f, sections[0], app); - render_sidebar_todos(f, sections[1], app); - render_sidebar_tasks(f, sections[2], app); - render_sidebar_subagents(f, sections[3], app); - } + SidebarFocus::Auto => render_sidebar_auto(f, area, app), SidebarFocus::Plan => render_sidebar_plan(f, area, app), SidebarFocus::Todos => render_sidebar_todos(f, area, app), SidebarFocus::Tasks => render_sidebar_tasks(f, area, app), @@ -60,6 +45,74 @@ pub fn render_sidebar(f: &mut Frame, area: Rect, app: &App) { } } +/// Build the Auto-mode panel stack. Empty panels collapse to zero height so +/// non-empty ones get the full sidebar real estate. Without this, Plan got +/// clipped because Todos/Tasks/Agents each reserved 25% of the height even +/// when they had nothing to show. Plan is always rendered (it owns the +/// session-wide empty-state hint). +fn render_sidebar_auto(f: &mut Frame, area: Rect, app: &App) { + #[derive(Clone, Copy)] + enum Panel { + Plan, + Todos, + Tasks, + Agents, + } + + let todos_empty = app + .todos + .try_lock() + .map(|todos| todos.snapshot().items.is_empty()) + .unwrap_or(false); // assume non-empty when locked so we don't hide updating data + let tasks_empty = app.runtime_turn_id.is_none() && app.task_panel.is_empty(); + let agents_empty = app.subagent_cache.is_empty() + && app.agent_progress.is_empty() + && active_fanout_counts(app).is_none() + && !foreground_rlm_running(app); + + let mut visible: Vec = Vec::with_capacity(4); + visible.push(Panel::Plan); + if !todos_empty { + visible.push(Panel::Todos); + } + if !tasks_empty { + visible.push(Panel::Tasks); + } + if !agents_empty { + visible.push(Panel::Agents); + } + + let constraints: Vec = match visible.len() { + 1 => vec![Constraint::Min(0)], + 2 => vec![Constraint::Percentage(50), Constraint::Min(0)], + 3 => vec![ + Constraint::Percentage(34), + Constraint::Percentage(33), + Constraint::Min(0), + ], + _ => vec![ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Min(6), + ], + }; + + let sections = Layout::default() + .direction(Direction::Vertical) + .constraints(constraints) + .split(area); + + for (panel, rect) in visible.iter().zip(sections.iter()) { + match panel { + Panel::Plan => render_sidebar_plan(f, *rect, app), + Panel::Todos => render_sidebar_todos(f, *rect, app), + Panel::Tasks => render_sidebar_tasks(f, *rect, app), + Panel::Agents => render_sidebar_subagents(f, *rect, app), + } + } +} + /// The Plan section is the **single source of truth for the /// `update_plan` tool's output** (#408). It is intentionally distinct /// from the Todos section: todos are checklist work items the user