diff --git a/src/client/shell/endpoint_agents.rs b/src/client/shell/endpoint_agents.rs index 3cc106b3..f7c85e31 100644 --- a/src/client/shell/endpoint_agents.rs +++ b/src/client/shell/endpoint_agents.rs @@ -87,6 +87,36 @@ pub(super) fn render_expanded( ); } +impl ClientShellState { + pub(super) fn reveal_endpoint_agent(&mut self, endpoint_id: &ClientEndpointId, pane_id: &str) { + if self.hits.agent_body.is_empty() { + return; + } + let rows = agent_rows(&self.endpoints, &self.active_endpoint_id, &self.config); + let Some(target) = rows + .iter() + .position(|row| &row.endpoint_id == endpoint_id && row.agent.pane_id == pane_id) + else { + return; + }; + let heights = rows + .iter() + .map(|row| row.agent.rows.len().max(1).min(u16::MAX as usize) as u16) + .collect::>(); + let mut gaps = vec![self.config.agents.row_gap; rows.len()]; + if let Some(last) = gaps.last_mut() { + *last = 0; + } + self.agent_scroll = super::scroll::list_scroll_start_to_reveal( + &heights, + &gaps, + self.hits.agent_body.height, + self.agent_scroll, + target, + ); + } +} + struct EndpointAgentRow { endpoint_id: ClientEndpointId, machine_label: String, diff --git a/src/client/shell/endpoint_navigation.rs b/src/client/shell/endpoint_navigation.rs index 6cb8c2c4..26b6a032 100644 --- a/src/client/shell/endpoint_navigation.rs +++ b/src/client/shell/endpoint_navigation.rs @@ -203,6 +203,7 @@ impl ClientShellState { _ => unreachable!("endpoint agent navigation"), }; let target = &agents[next]; + self.reveal_endpoint_agent(&target.endpoint_id, &target.pane_id); self.focus_or_activate( target.endpoint_id.clone(), ClientEndpointFocusTarget::Pane(target.pane_id.clone()), diff --git a/src/client/shell/tests/endpoints.rs b/src/client/shell/tests/endpoints.rs index 846806f1..fd4751ef 100644 --- a/src/client/shell/tests/endpoints.rs +++ b/src/client/shell/tests/endpoints.rs @@ -109,6 +109,72 @@ fn state_with_scrollable_agents() -> (ClientShellState, ClientEndpointId) { (state, remote) } +#[test] +fn agent_navigation_reveals_offscreen_targets() { + use crate::input::KeybindAction; + + for action in [ + KeybindAction::NextAgent, + KeybindAction::PreviousAgent, + KeybindAction::FocusAgent(0), + ] { + let (mut state, remote) = state_with_scrollable_agents(); + let (endpoint_id, pane_id) = match action { + KeybindAction::NextAgent => (ClientEndpointId::Local, "pane_2"), + KeybindAction::PreviousAgent => (remote, "pane_8"), + _ => (ClientEndpointId::Local, "pane_1"), + }; + state.agent_scroll = if action == KeybindAction::PreviousAgent { + 0 + } else { + state.hits.agent_max_scroll + }; + state.compose(100, 28).unwrap(); + assert!(!state + .hits + .endpoint_agents + .iter() + .any(|(_, endpoint, pane)| { endpoint == &endpoint_id && pane == pane_id })); + + let mut outcome = ClientShellInput::default(); + assert!(state.handle_endpoint_navigation(action, &mut outcome)); + if endpoint_id != state.active_endpoint_id { + assert!(state.activate_endpoint_projection(&endpoint_id)); + } + state.compose(100, 28).unwrap(); + assert!( + state + .hits + .endpoint_agents + .iter() + .any(|(_, endpoint, pane)| { endpoint == &endpoint_id && pane == pane_id }), + "{action:?} must reveal the selected agent" + ); + } +} + +#[test] +fn agent_navigation_keeps_scroll_when_target_is_visible() { + let (mut state, _) = state_with_scrollable_agents(); + let (_, endpoint_id, pane_id) = state.hits.endpoint_agents[1].clone(); + let targets = super::super::aggregate_navigation::online_agent_targets( + &state.endpoints, + &state.active_endpoint_id, + state.config.agent_panel_sort, + ); + let index = targets + .iter() + .position(|target| target.endpoint_id == endpoint_id && target.pane_id == pane_id) + .unwrap(); + let scroll = state.agent_scroll; + assert!(state.handle_endpoint_navigation( + crate::input::KeybindAction::FocusAgent(index), + &mut ClientShellInput::default(), + )); + state.compose(100, 28).unwrap(); + assert_eq!(state.agent_scroll, scroll); +} + #[test] fn switching_machines_preserves_aggregate_agent_scroll_and_visible_rows() { let (mut state, remote) = state_with_scrollable_agents();