From 2ab07c2343e86d230dedb1b839185e3234b97edb Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 25 Aug 2026 13:59:38 +0100 Subject: [PATCH] fix(maestro): harden grouped sidebar interactions --- crates/maestro-tui/src/app.rs | 40 +++++++++++++++++++++++++++---- crates/maestro-tui/src/sidebar.rs | 40 +++++++++++++++++++++++++------ 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/crates/maestro-tui/src/app.rs b/crates/maestro-tui/src/app.rs index b08547b..d4cf7bc 100644 --- a/crates/maestro-tui/src/app.rs +++ b/crates/maestro-tui/src/app.rs @@ -197,10 +197,6 @@ impl App { }; let key = self.clicks.promote(key, std::time::Instant::now()); - if self.handle_sidebar_mouse(&key) { - return Ok(false); - } - // Overlay intercept first. let (decision, effects) = self.handle_overlay_input(&key); self.perform(effects).await; @@ -208,6 +204,10 @@ impl App { return Ok(false); } + if self.handle_sidebar_mouse(&key) { + return Ok(false); + } + let known = self.cfg.keymap.known_bindings(self.state.mode); match self.chord.feed(&key, &known) { ChordResult::Pending => Ok(false), @@ -250,6 +250,7 @@ impl App { let Some(item) = self.region.sidebar_item_at(*row) else { return true; }; + self.state.focus = Focus::Sidebar; match item { SidebarItem::Group(key) => { self.state.sidebar_highlight = Some(SidebarItem::Group(key.clone())); @@ -257,7 +258,6 @@ impl App { } SidebarItem::Session(id) => { self.state.sidebar_highlight = Some(SidebarItem::Session(id)); - self.state.focus = Focus::Sidebar; } } true @@ -1296,6 +1296,7 @@ mod tests { .state .collapsed_sidebar_groups .contains(&PathBuf::from("/"))); + assert_eq!(app.state.focus, Focus::Sidebar); } #[test] @@ -1319,6 +1320,35 @@ mod tests { .state .collapsed_sidebar_groups .contains(&PathBuf::from("/"))); + assert_eq!(app.state.focus, Focus::Sidebar); + } + + #[tokio::test] + async fn sidebar_click_dismisses_a_toast_before_handling_the_click() { + use crossterm::event::{MouseButton, MouseEvent, MouseEventKind}; + + let sid = Uuid::from_u128(17); + let mut app = app_with_highlighted_session(sid); + app.state.toast("notice"); + app.region.sidebar = Some(ratatui::layout::Rect::new(0, 0, 28, 10)); + app.region.sidebar_items = vec![SidebarItem::Group(PathBuf::from("/"))]; + + let quit = app + .handle_event(CtEvent::Mouse(MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: 3, + row: 2, + modifiers: crossterm::event::KeyModifiers::NONE, + })) + .await + .unwrap(); + + assert!(!quit); + assert!(app.state.overlay().is_none()); + assert!(app + .state + .collapsed_sidebar_groups + .contains(&PathBuf::from("/"))); } #[test] diff --git a/crates/maestro-tui/src/sidebar.rs b/crates/maestro-tui/src/sidebar.rs index 77eb7a3..61a44b9 100644 --- a/crates/maestro-tui/src/sidebar.rs +++ b/crates/maestro-tui/src/sidebar.rs @@ -180,21 +180,27 @@ impl Sidebar { let path = abbreviate_dir(&group.key, home); let name = abbreviate_folder_name(&path); let count = format!(" ({})", group.sessions.len()); - let mut spans = vec![Span::styled( + let prefix = Span::styled( prefix, if edge { base.fg(palette.focus_accent) } else { base }, - )]; - spans.push(Span::styled(chevron, base)); + ); + let chevron = Span::styled(chevron, base); + let count = Span::styled(count, surface.muted_style()); + let label_budget = width.saturating_sub(prefix.width() + chevron.width() + count.width()); + let mut label = Vec::new(); if let Some(stem) = name.stem { - spans.push(Span::styled(format!("{stem}/"), surface.dim_style())); + label.push(Span::styled(format!("{stem}/"), surface.dim_style())); } - spans.push(Span::styled(name.last, base)); - spans.push(Span::styled(count, surface.muted_style())); - pad_to_width(fit_to_width(spans, width), width, base) + label.push(Span::styled(name.last, base)); + + let mut spans = vec![prefix, chevron]; + spans.extend(fit_to_width(label, label_budget)); + spans.push(count); + pad_to_width(spans, width, base) } fn name_row(&self, s: &Session, width: usize, focused: bool) -> Vec> { @@ -541,6 +547,26 @@ mod tests { assert!(!rendered.contains("claude")); } + #[test] + fn long_group_labels_yield_to_the_always_visible_count() { + let mut session = session_named(1, "claude"); + session.cwd = "/very/long/project/path/extraordinarily-long-project-name".into(); + let sidebar = Sidebar { + sessions: vec![session], + highlight: None, + attached: Default::default(), + }; + let key = PathBuf::from("/very/long/project/path/extraordinarily-long-project-name"); + let collapsed = HashSet::from([key]); + let rendered = render_to_string(&draw_collapsed(&sidebar, 28, 6, &collapsed)); + + assert!(rendered.contains('…')); + assert!( + rendered.contains("(1)"), + "group count was truncated: {rendered:?}" + ); + } + #[test] fn shared_display_helpers_abbreviate_group_labels() { let path = maestro_protocol::display::abbreviate_dir(