From 9fa62b8118f4e798e3e84998d446cf160ec0a886 Mon Sep 17 00:00:00 2001 From: John Myers Date: Fri, 4 Sep 2026 09:08:03 -0700 Subject: [PATCH] fix(tui): keep sandbox actions visible Signed-off-by: John Myers --- crates/openshell-tui/src/ui/mod.rs | 29 ++++++++++++- crates/openshell-tui/src/ui/sandbox_detail.rs | 43 ++++++++++++++----- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/crates/openshell-tui/src/ui/mod.rs b/crates/openshell-tui/src/ui/mod.rs index 2521184320..cf5c187364 100644 --- a/crates/openshell-tui/src/ui/mod.rs +++ b/crates/openshell-tui/src/ui/mod.rs @@ -74,8 +74,8 @@ fn draw_sandbox_screen(frame: &mut Frame<'_>, app: &mut App, area: Rect) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Percentage(20), // metadata - Constraint::Percentage(80), // policy or logs + Constraint::Length(sandbox_detail::required_height(app)), // metadata + Constraint::Min(0), // policy or logs ]) .split(area); @@ -737,4 +737,29 @@ mod tests { assert_eq!(rendered, expected); assert!(!rendered.contains("ALPHA")); } + + #[tokio::test] + async fn sandbox_delete_confirmation_is_visible_in_standard_terminal() { + let mut app = test_app(); + app.screen = Screen::Sandbox; + app.focus = Focus::SandboxPolicy; + app.sandbox_names = vec!["test-sandbox".to_string()]; + app.sandbox_count = 1; + app.confirm_delete = true; + let mut terminal = Terminal::new(TestBackend::new(100, 24)).unwrap(); + + terminal.draw(|frame| draw(frame, &mut app)).unwrap(); + + let text: String = terminal + .backend() + .buffer() + .content() + .iter() + .map(ratatui::buffer::Cell::symbol) + .collect(); + assert!( + text.contains("Delete sandbox 'test-sandbox'?"), + "sandbox screen was: {text:?}" + ); + } } diff --git a/crates/openshell-tui/src/ui/sandbox_detail.rs b/crates/openshell-tui/src/ui/sandbox_detail.rs index 4ff78617f0..a398797e40 100644 --- a/crates/openshell-tui/src/ui/sandbox_detail.rs +++ b/crates/openshell-tui/src/ui/sandbox_detail.rs @@ -8,6 +8,37 @@ use ratatui::widgets::{Block, Borders, Padding, Paragraph}; use crate::app::App; +const BASE_CONTENT_ROWS: u16 = 6; +const BORDER_ROWS: u16 = 2; + +fn pending_draft_count(app: &App) -> usize { + let cached = app + .sandbox_draft_counts + .get(app.sandbox_selected) + .copied() + .unwrap_or(0); + if cached > 0 { + cached + } else { + app.draft_chunks + .iter() + .filter(|chunk| chunk.status == "pending") + .count() + } +} + +/// Return the rows needed to render every metadata line without clipping. +pub(super) fn required_height(app: &App) -> u16 { + let policy_rows = u16::from(app.sandbox_policy_is_global); + let action_rows = if app.confirm_delete { + 2 // spacer plus confirmation + } else { + u16::from(pending_draft_count(app) > 0) + }; + + BASE_CONTENT_ROWS + policy_rows + action_rows + BORDER_ROWS +} + /// Draw a compact metadata pane for the currently selected sandbox. /// /// This is non-interactive (no focus state) — always rendered with the @@ -37,16 +68,8 @@ pub fn draw(frame: &mut Frame<'_>, app: &App, area: Rect) { }; // Count pending draft recommendations for this sandbox. - let pending_count = app.sandbox_draft_counts.get(idx).copied().unwrap_or(0); - // Also check the live draft_chunks when on the sandbox screen (more up-to-date). - let pending_count = if pending_count > 0 { - pending_count - } else { - app.draft_chunks - .iter() - .filter(|c| c.status == "pending") - .count() - }; + // Fall back to the live chunks when the dashboard cache has no pending count. + let pending_count = pending_draft_count(app); // Row 1: Name + Status + optional draft badge let mut row1_spans = vec![