From 781d30495d4880578a517c10d1902524ac218eb1 Mon Sep 17 00:00:00 2001 From: Val Alexander <68980965+BunsDev@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:23:00 -0500 Subject: [PATCH] fix(tui): surface hidden keybindings Fixes #56 --- src-rust/crates/tui/src/onboarding_dialog.rs | 9 +++++ src-rust/crates/tui/src/overlays.rs | 31 ++++++++++++++++ src-rust/crates/tui/src/render.rs | 37 ++++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/src-rust/crates/tui/src/onboarding_dialog.rs b/src-rust/crates/tui/src/onboarding_dialog.rs index 93d9e86f..cb16f00f 100644 --- a/src-rust/crates/tui/src/onboarding_dialog.rs +++ b/src-rust/crates/tui/src/onboarding_dialog.rs @@ -467,6 +467,9 @@ fn render_keybindings_page(frame: &mut Frame, area: Rect) { kb("PgUp/PgDn", "scroll transcript"), kb("Ctrl+K", "command palette"), kb("Ctrl+Shift+A", "model picker"), + kb("F2", "switch familiar"), + kb("Alt+H", "open help"), + kb("Ctrl+B", "create / switch branch"), Line::from(""), Line::from(Span::styled( " Permissions", @@ -584,6 +587,12 @@ mod tests { .map(|c| c.symbol().chars().next().unwrap_or(' ')) .collect(); assert!(content.contains("Keyboard") || content.contains("Enter")); + for expected in ["F2", "Alt+H", "Ctrl+B", "Tab", "build/plan/explore"] { + assert!( + content.contains(expected), + "onboarding keybindings should mention {expected}, got {content:?}" + ); + } } #[test] diff --git a/src-rust/crates/tui/src/overlays.rs b/src-rust/crates/tui/src/overlays.rs index 5d08c246..11866f75 100644 --- a/src-rust/crates/tui/src/overlays.rs +++ b/src-rust/crates/tui/src/overlays.rs @@ -440,6 +440,10 @@ pub fn render_help_overlay(frame: &mut Frame, overlay: &HelpOverlay, area: Rect) ))); for (key, desc) in &[ ("F1 / ?", "Toggle help"), + ("Alt+H", "Toggle help"), + ("F2", "Switch familiar"), + ("Ctrl+B", "Create / switch branch"), + ("Tab", "Cycle mode: build / plan / explore"), ("Ctrl+Shift+A", "Model picker"), ("Ctrl+K", "Command palette"), ("Ctrl+C", "Cancel / quit"), @@ -1954,6 +1958,8 @@ pub fn render_global_search( #[cfg(test)] mod tests { use super::*; + use ratatui::backend::TestBackend; + use ratatui::Terminal; // --- HelpOverlay --------------------------------------------------- @@ -1989,6 +1995,31 @@ mod tests { assert_eq!(h.filter, "h"); } + #[test] + fn help_overlay_lists_hidden_keybinding_hints() { + let mut terminal = Terminal::new(TestBackend::new(120, 40)).expect("terminal"); + let mut overlay = HelpOverlay::new(); + overlay.visible = true; + terminal + .draw(|frame| render_help_overlay(frame, &overlay, frame.area())) + .expect("draw help overlay"); + + let content: String = terminal + .backend() + .buffer() + .content() + .iter() + .map(|cell| cell.symbol()) + .collect(); + + for expected in ["F2", "Alt+H", "Ctrl+B", "Tab"] { + assert!( + content.contains(expected), + "help overlay should mention {expected}, got {content:?}" + ); + } + } + #[test] fn modal_search_line_separates_leading_space_from_cursor() { let line = modal_search_line("", "Search", COVEN_CODE_MUTED, COVEN_CODE_TEXT); diff --git a/src-rust/crates/tui/src/render.rs b/src-rust/crates/tui/src/render.rs index 6defb61b..0c458a4f 100644 --- a/src-rust/crates/tui/src/render.rs +++ b/src-rust/crates/tui/src/render.rs @@ -2386,6 +2386,13 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) { )); } + if app.prompt_input.text.is_empty() && !app.is_streaming { + spans.push(Span::styled( + "F2 familiar Alt+H help Ctrl+B branch Tab mode", + Style::default().fg(Color::DarkGray), + )); + } + // Agent type badge (shown when running as subagent / coordinator) if let Some(ref badge) = app.agent_type_badge { spans.push(Span::styled( @@ -2958,6 +2965,10 @@ fn render_simple_help_overlay(frame: &mut Frame, area: Rect) { kb_line("Ctrl+R", "Search input history"), kb_line("PageUp / PageDown", "Scroll messages"), kb_line("F1 / ?", "Toggle this help"), + kb_line("Alt+H", "Toggle this help"), + kb_line("F2", "Switch familiar"), + kb_line("Ctrl+B", "Create / switch branch"), + kb_line("Tab", "Cycle mode (build/plan/explore)"), Line::from(""), Line::from(vec![Span::styled( " Permission Dialog", @@ -3448,6 +3459,8 @@ fn render_familiar_switcher(frame: &mut Frame, app: &App, area: Rect) { mod welcome_tests { use super::*; use crate::app::test_env::EnvGuard; + use ratatui::backend::TestBackend; + use ratatui::Terminal; fn make_test_app_with_model_and_familiar( model: Option<&str>, @@ -3602,6 +3615,30 @@ mod welcome_tests { assert_ne!(spinner_char(1), spinner_char(2)); } + #[test] + fn footer_exposes_hidden_keybinding_hints() { + let mut terminal = Terminal::new(TestBackend::new(180, 1)).expect("terminal"); + let app = make_test_app_with_model_and_familiar(None, None, None, None); + terminal + .draw(|frame| render_footer(frame, &app, frame.area())) + .expect("draw footer"); + + let content: String = terminal + .backend() + .buffer() + .content() + .iter() + .map(|cell| cell.symbol()) + .collect(); + + for expected in ["F2", "Alt+H", "Ctrl+B", "Tab"] { + assert!( + content.contains(expected), + "footer should mention {expected}, got {content:?}" + ); + } + } + #[test] fn welcome_daemon_label_is_one_of_two_strings() { // Either string is acceptable — the test machine may or may not have