diff --git a/crates/codemark-tui/src/browser/bindings.rs b/crates/codemark-tui/src/browser/bindings.rs index da25d972..18443b9e 100644 --- a/crates/codemark-tui/src/browser/bindings.rs +++ b/crates/codemark-tui/src/browser/bindings.rs @@ -1,5 +1,5 @@ use crate::browser::right_pane::INFO_TAB_INDEX; -use crate::browser::{BrowserLayout, ContentTab, FocusArea}; +use crate::browser::{BrowserLayout, ContentTab, ContextTab, FocusArea}; use crate::ui::{HIDDEN_BINDING_PRIORITY, KeyBinding}; impl BrowserLayout { @@ -39,8 +39,19 @@ impl BrowserLayout { bindings.push(("Ctrl+S", "FTS / Sem")); } FocusArea::ContextPanel => { - bindings.push(("Enter", "Select repo")); - bindings.push(("Space", "Toggle repo")); + // Enter/Space act on whatever the active Context tab lists. The + // Auth tab is read-only, so it advertises neither. + match ContextTab::from_index(self.left_pane.context_panel.tabs.selected_index()) { + Some(ContextTab::Repos) => { + bindings.push(("Enter", "Select repo")); + bindings.push(("Space", "Toggle repo")); + } + Some(ContextTab::Owners) => { + bindings.push(("Enter", "Toggle owner")); + bindings.push(("Space", "Toggle owner")); + } + Some(ContextTab::Auth) | None => {} + } bindings.push(("+", "Increase pane")); bindings.push(("_", "Decrease pane")); } @@ -164,10 +175,27 @@ impl BrowserLayout { bindings.push(KeyBinding::new("Ctrl+S", "FTS / Sem").with_priority(90)); } FocusArea::ContextPanel => { - // Repos / Accounts - bindings.push( - KeyBinding::new("Enter", "Select").with_priority(HIDDEN_BINDING_PRIORITY), - ); + // Repos / Owners / Auth. Enter is hidden from the bar but kept for + // the help popup; the Auth tab is read-only so it advertises none. + match ContextTab::from_index(self.left_pane.context_panel.tabs.selected_index()) { + Some(ContextTab::Repos) => { + bindings.push( + KeyBinding::new("Enter", "Select") + .with_priority(HIDDEN_BINDING_PRIORITY), + ); + } + Some(ContextTab::Owners) => { + // Enter is universal (hidden from the bar); Space is the + // discoverable toggle affordance, so advertise it as the + // primary action here. + bindings.push( + KeyBinding::new("Enter", "Toggle owner") + .with_priority(HIDDEN_BINDING_PRIORITY), + ); + bindings.push(KeyBinding::new("Space", "Toggle owner").with_priority(85)); + } + Some(ContextTab::Auth) | None => {} + } bindings.push(KeyBinding::new("+/-", "Resize").with_priority(20)); } FocusArea::FiltersPanel => { diff --git a/crates/codemark-tui/tests/browser_e2e.rs b/crates/codemark-tui/tests/browser_e2e.rs index cdfb0c2e..feff540e 100644 --- a/crates/codemark-tui/tests/browser_e2e.rs +++ b/crates/codemark-tui/tests/browser_e2e.rs @@ -403,6 +403,80 @@ async fn search_bar_shows_esc_clear_hint() { assert!(has_esc_hint, "search-focus bindings should include 'Esc: Clear'; got: {bindings:?}"); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn context_panel_help_labels_track_active_tab() { + let sandbox = Sandbox::new(); + let (mut layout, _sandbox) = make_layout(sandbox); + + // An empty database starts focused on the Context panel's Repos tab. + assert_eq!(layout.focus(), FocusArea::ContextPanel); + + // The help popup (get_help_bindings) and the status bar (get_status_bindings, + // via the separate get_context_bindings path) must both track the active tab. + let repos = layout.get_help_bindings(); + assert!( + repos.contains(&("Enter", "Select repo")) && repos.contains(&("Space", "Toggle repo")), + "Repos tab should describe repo actions; got: {repos:?}" + ); + let repos_status = layout.get_status_bindings(); + assert!( + repos_status.iter().any(|b| b.key == "Enter" && b.description == "Select"), + "Repos status bar should carry the Enter Select action; got: {repos_status:?}" + ); + assert!( + repos_status.iter().any(|b| b.key == "+/-" && b.description == "Resize"), + "Repos status bar should keep the resize binding; got: {repos_status:?}" + ); + + // `]` cycles the focused panel's tab: Repos -> Owners. Now Enter/Space filter + // the repo list by owner, so the labels must say so. + key_char(&mut layout, ']'); + let owners = layout.get_help_bindings(); + assert!( + owners.contains(&("Enter", "Toggle owner")) && owners.contains(&("Space", "Toggle owner")), + "Owners tab should describe owner actions; got: {owners:?}" + ); + assert!( + !owners.iter().any(|b| b.1 == "Select repo" || b.1 == "Toggle repo"), + "Owners tab must not advertise repo actions; got: {owners:?}" + ); + let owners_status = layout.get_status_bindings(); + // Space is the discoverable toggle affordance and must be visible on the bar. + assert!( + owners_status + .iter() + .any(|b| { b.key == "Space" && b.description == "Toggle owner" && b.priority > 0 }), + "Owners status bar should show a visible Space toggle; got: {owners_status:?}" + ); + assert!( + !owners_status.iter().any(|b| b.description == "Select" || b.description == "Toggle repo"), + "Owners status bar must not advertise repo actions; got: {owners_status:?}" + ); + + // `]` again: Owners -> Auth. The Auth tab is read-only, so neither key acts. + key_char(&mut layout, ']'); + let auth = layout.get_help_bindings(); + assert!( + !auth.iter().any(|b| b.0 == "Enter" || b.0 == "Space"), + "Auth tab must not advertise Enter/Space bindings; got: {auth:?}" + ); + + // Resize bindings stay put regardless of tab. + assert!( + auth.contains(&("+", "Increase pane")) && auth.contains(&("_", "Decrease pane")), + "Resize bindings should persist across tabs; got: {auth:?}" + ); + let auth_status = layout.get_status_bindings(); + assert!( + !auth_status.iter().any(|b| b.key == "Enter" || b.key == "Space"), + "Auth status bar must not carry an Enter/Space action; got: {auth_status:?}" + ); + assert!( + auth_status.iter().any(|b| b.key == "+/-" && b.description == "Resize"), + "Auth status bar should keep the resize binding; got: {auth_status:?}" + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn search_results_reconcile_with_db_on_focus_regained() { // Two bookmarks match a "config" path search; one of them will be deleted