Compare commits

..
Author SHA1 Message Date
Miguel Palhas 2ab07c2343 fix(maestro): harden grouped sidebar interactions
ci / web (push) Successful in 1m29s
ci / rust (push) Successful in 5m25s
ci / flake (push) Successful in 7m56s
2026-08-25 13:59:38 +01:00
Miguel Palhas 342e732ef0 merge: TUI shell/124 — collapsible project groups 2026-08-25 13:54:09 +01:00
2 changed files with 68 additions and 12 deletions
+35 -5
View File
@@ -197,10 +197,6 @@ impl App {
}; };
let key = self.clicks.promote(key, std::time::Instant::now()); let key = self.clicks.promote(key, std::time::Instant::now());
if self.handle_sidebar_mouse(&key) {
return Ok(false);
}
// Overlay intercept first. // Overlay intercept first.
let (decision, effects) = self.handle_overlay_input(&key); let (decision, effects) = self.handle_overlay_input(&key);
self.perform(effects).await; self.perform(effects).await;
@@ -208,6 +204,10 @@ impl App {
return Ok(false); return Ok(false);
} }
if self.handle_sidebar_mouse(&key) {
return Ok(false);
}
let known = self.cfg.keymap.known_bindings(self.state.mode); let known = self.cfg.keymap.known_bindings(self.state.mode);
match self.chord.feed(&key, &known) { match self.chord.feed(&key, &known) {
ChordResult::Pending => Ok(false), ChordResult::Pending => Ok(false),
@@ -250,6 +250,7 @@ impl App {
let Some(item) = self.region.sidebar_item_at(*row) else { let Some(item) = self.region.sidebar_item_at(*row) else {
return true; return true;
}; };
self.state.focus = Focus::Sidebar;
match item { match item {
SidebarItem::Group(key) => { SidebarItem::Group(key) => {
self.state.sidebar_highlight = Some(SidebarItem::Group(key.clone())); self.state.sidebar_highlight = Some(SidebarItem::Group(key.clone()));
@@ -257,7 +258,6 @@ impl App {
} }
SidebarItem::Session(id) => { SidebarItem::Session(id) => {
self.state.sidebar_highlight = Some(SidebarItem::Session(id)); self.state.sidebar_highlight = Some(SidebarItem::Session(id));
self.state.focus = Focus::Sidebar;
} }
} }
true true
@@ -1296,6 +1296,7 @@ mod tests {
.state .state
.collapsed_sidebar_groups .collapsed_sidebar_groups
.contains(&PathBuf::from("/"))); .contains(&PathBuf::from("/")));
assert_eq!(app.state.focus, Focus::Sidebar);
} }
#[test] #[test]
@@ -1319,6 +1320,35 @@ mod tests {
.state .state
.collapsed_sidebar_groups .collapsed_sidebar_groups
.contains(&PathBuf::from("/"))); .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] #[test]
+33 -7
View File
@@ -180,21 +180,27 @@ impl Sidebar {
let path = abbreviate_dir(&group.key, home); let path = abbreviate_dir(&group.key, home);
let name = abbreviate_folder_name(&path); let name = abbreviate_folder_name(&path);
let count = format!(" ({})", group.sessions.len()); let count = format!(" ({})", group.sessions.len());
let mut spans = vec![Span::styled( let prefix = Span::styled(
prefix, prefix,
if edge { if edge {
base.fg(palette.focus_accent) base.fg(palette.focus_accent)
} else { } else {
base 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 { 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)); label.push(Span::styled(name.last, base));
spans.push(Span::styled(count, surface.muted_style()));
pad_to_width(fit_to_width(spans, width), width, 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<Span<'static>> { fn name_row(&self, s: &Session, width: usize, focused: bool) -> Vec<Span<'static>> {
@@ -541,6 +547,26 @@ mod tests {
assert!(!rendered.contains("claude")); 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] #[test]
fn shared_display_helpers_abbreviate_group_labels() { fn shared_display_helpers_abbreviate_group_labels() {
let path = maestro_protocol::display::abbreviate_dir( let path = maestro_protocol::display::abbreviate_dir(