From 0434095da21b991310448daf35fc0bbbd5e221b7 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Fri, 18 Sep 2026 18:34:38 +0300 Subject: [PATCH] fix: reveal agent after endpoint activation --- src/client/shell/endpoint_agents.rs | 11 +++- src/client/shell/endpoint_navigation.rs | 19 +++++- src/client/shell/endpoints.rs | 7 +++ src/client/shell/state.rs | 2 + src/client/shell/tests/endpoints.rs | 82 +++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 6 deletions(-) diff --git a/src/client/shell/endpoint_agents.rs b/src/client/shell/endpoint_agents.rs index f7c85e31..b56b41fa 100644 --- a/src/client/shell/endpoint_agents.rs +++ b/src/client/shell/endpoint_agents.rs @@ -88,8 +88,13 @@ 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() { + pub(super) fn reveal_endpoint_agent( + &mut self, + endpoint_id: &ClientEndpointId, + pane_id: &str, + body_height: u16, + ) { + if body_height == 0 { return; } let rows = agent_rows(&self.endpoints, &self.active_endpoint_id, &self.config); @@ -110,7 +115,7 @@ impl ClientShellState { self.agent_scroll = super::scroll::list_scroll_start_to_reveal( &heights, &gaps, - self.hits.agent_body.height, + body_height, self.agent_scroll, target, ); diff --git a/src/client/shell/endpoint_navigation.rs b/src/client/shell/endpoint_navigation.rs index 26b6a032..c15fd434 100644 --- a/src/client/shell/endpoint_navigation.rs +++ b/src/client/shell/endpoint_navigation.rs @@ -203,12 +203,23 @@ impl ClientShellState { _ => unreachable!("endpoint agent navigation"), }; let target = &agents[next]; - self.reveal_endpoint_agent(&target.endpoint_id, &target.pane_id); - self.focus_or_activate( + if self.focus_or_activate( target.endpoint_id.clone(), ClientEndpointFocusTarget::Pane(target.pane_id.clone()), outcome, - ); + ) { + if target.endpoint_id == self.active_endpoint_id { + self.reveal_endpoint_agent( + &target.endpoint_id, + &target.pane_id, + self.hits.agent_body.height, + ); + } else { + self.pending_agent_reveal = + Some((target.endpoint_id.clone(), target.pane_id.clone())); + } + outcome.repaint = true; + } return true; } false @@ -219,6 +230,7 @@ impl ClientShellState { endpoint_id: ClientEndpointId, outcome: &mut ClientShellInput, ) -> bool { + self.pending_agent_reveal = None; let online = self.endpoint_is_online(&endpoint_id); if !online && !endpoint_id.is_local() { let label = self.endpoint_label(&endpoint_id).to_owned(); @@ -243,6 +255,7 @@ impl ClientShellState { target: ClientEndpointFocusTarget, outcome: &mut ClientShellInput, ) -> bool { + self.pending_agent_reveal = None; let online = self.endpoint_is_online(&endpoint_id); if !online && !endpoint_id.is_local() { let label = self.endpoint_label(&endpoint_id).to_owned(); diff --git a/src/client/shell/endpoints.rs b/src/client/shell/endpoints.rs index c27358e5..3d1d419b 100644 --- a/src/client/shell/endpoints.rs +++ b/src/client/shell/endpoints.rs @@ -195,6 +195,10 @@ impl ClientShellState { } pub(crate) fn activate_endpoint_projection(&mut self, endpoint_id: &ClientEndpointId) -> bool { + let pending_agent_reveal = self + .pending_agent_reveal + .take_if(|(target_endpoint, _)| target_endpoint == endpoint_id); + let agent_body_height = self.hits.agent_body.height; let Some(endpoint) = self .endpoints .iter() @@ -221,6 +225,9 @@ impl ClientShellState { // The aggregate agent list belongs to the client, not one endpoint. self.agent_scroll = agent_scroll; } + if let Some((_, pane_id)) = pending_agent_reveal { + self.reveal_endpoint_agent(endpoint_id, &pane_id, agent_body_height); + } true } diff --git a/src/client/shell/state.rs b/src/client/shell/state.rs index 901c5a28..b975b2f6 100644 --- a/src/client/shell/state.rs +++ b/src/client/shell/state.rs @@ -861,6 +861,7 @@ pub(crate) struct ClientShellState { pub(super) remote_collapsed_groups: HashMap>, pub(super) workspace_scroll: usize, pub(super) agent_scroll: usize, + pub(super) pending_agent_reveal: Option<(ClientEndpointId, String)>, pub(super) tab_scroll: usize, pub(super) mobile_switcher_scroll: usize, pub(super) reveal_focused_workspace: bool, @@ -1023,6 +1024,7 @@ impl ClientShellState { remote_collapsed_groups, workspace_scroll: 0, agent_scroll: 0, + pending_agent_reveal: None, tab_scroll: 0, mobile_switcher_scroll: 0, reveal_focused_workspace: true, diff --git a/src/client/shell/tests/endpoints.rs b/src/client/shell/tests/endpoints.rs index fd4751ef..e4914971 100644 --- a/src/client/shell/tests/endpoints.rs +++ b/src/client/shell/tests/endpoints.rs @@ -138,6 +138,7 @@ fn agent_navigation_reveals_offscreen_targets() { let mut outcome = ClientShellInput::default(); assert!(state.handle_endpoint_navigation(action, &mut outcome)); + assert!(outcome.repaint, "agent navigation must request a frame"); if endpoint_id != state.active_endpoint_id { assert!(state.activate_endpoint_projection(&endpoint_id)); } @@ -153,6 +154,87 @@ fn agent_navigation_reveals_offscreen_targets() { } } +#[test] +fn agent_navigation_reveals_target_using_destination_sort() { + use crate::api::schema::{ + AgentViewBuiltinSortField, AgentViewSort, AgentViewSortField, AgentViewSortOrder, + }; + + let (mut state, remote) = state_with_scrollable_agents(); + for (endpoint_id, base) in [(ClientEndpointId::Local, 0), (remote.clone(), 8)] { + let mut projection = state + .endpoints + .iter() + .find(|endpoint| endpoint.endpoint_id == endpoint_id) + .unwrap() + .snapshot + .clone() + .unwrap(); + for (index, agent) in projection.agents.iter_mut().enumerate() { + agent.state_change_seq = base + index as u64; + } + if endpoint_id == remote { + projection.agent_view_label = Some("recent".into()); + } + state.set_endpoint_snapshot(&endpoint_id, projection); + } + state.set_test_endpoint_agent_view(&ClientEndpointId::Local, None); + let mut view = current_workspace_view(); + view.label = Some("recent".into()); + view.filter = None; + view.sort = vec![AgentViewSort { + field: AgentViewSortField::Builtin(AgentViewBuiltinSortField::StateChangeSeq), + order: AgentViewSortOrder::Desc, + }]; + state.set_test_endpoint_agent_view(&remote, Some(view)); + state.compose(100, 28).unwrap(); + + let mut outcome = ClientShellInput::default(); + assert!(state + .handle_endpoint_navigation(crate::input::KeybindAction::FocusAgent(15), &mut outcome,)); + assert!(matches!( + outcome.actions.as_slice(), + [ClientShellAction::ActivateEndpoint { + endpoint_id, + target: Some(ClientEndpointFocusTarget::Pane(pane_id)), + }] if endpoint_id == &remote && pane_id == "pane_8" + )); + // A superseded handoff restores its source before activating the new target. + assert!(state.activate_endpoint_projection(&ClientEndpointId::Local)); + state.compose(100, 28).unwrap(); + assert!(state.activate_endpoint_projection(&remote)); + state.compose(100, 28).unwrap(); + assert!(state + .hits + .endpoint_agents + .iter() + .any(|(_, endpoint, pane)| { endpoint == &remote && pane == "pane_8" })); +} + +#[test] +fn agent_navigation_reveal_is_cancelled_by_another_selection() { + for select_pane in [false, true] { + let (mut state, remote) = state_with_scrollable_agents(); + let scroll = state.agent_scroll; + let mut outcome = ClientShellInput::default(); + assert!(state + .handle_endpoint_navigation(crate::input::KeybindAction::PreviousAgent, &mut outcome,)); + assert_eq!(state.agent_scroll, scroll); + if select_pane { + assert!(state.focus_or_activate( + remote.clone(), + ClientEndpointFocusTarget::Pane("pane_1".into()), + &mut outcome, + )); + } else { + assert!(state.activate_endpoint(remote.clone(), &mut outcome)); + } + assert!(state.activate_endpoint_projection(&remote)); + state.compose(100, 28).unwrap(); + assert_eq!(state.agent_scroll, scroll); + } +} + #[test] fn agent_navigation_keeps_scroll_when_target_is_visible() { let (mut state, _) = state_with_scrollable_agents();