aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-09 22:04:47 +0300
committerGitHub <noreply@github.com>2026-09-09 22:04:47 +0300
commit1682cab3263b27b296f477552a4e79ab54923091 (patch)
tree983b0b5492a66bb83cc068d1c0b64c674ffb9e77 /src/server
parent1f773dd6f44ab5a66371d4249c3d6f33a02d1069 (diff)
fix: restore plugin focus events for client navigation (#3850)
refs #3824
Diffstat (limited to 'src/server')
-rw-r--r--src/server/headless/client_views.rs8
-rw-r--r--src/server/headless/tests/mod.rs113
2 files changed, 120 insertions, 1 deletions
diff --git a/src/server/headless/client_views.rs b/src/server/headless/client_views.rs
index 2257a05e..333c94b4 100644
--- a/src/server/headless/client_views.rs
+++ b/src/server/headless/client_views.rs
@@ -895,10 +895,10 @@ impl HeadlessServer {
if reconcile || self.app.state.popup_pane.is_some() != popup_before {
self.reconcile_client_shell_locations();
}
+ let focus_after = self.shell_focus_target(client_id);
if let Some(all_focus_before) = all_focus_before {
self.finish_shell_location_reconciliation(all_focus_before, &focused_tabs_before);
} else {
- let focus_after = self.shell_focus_target(client_id);
let focused_tabs_after = self.focused_shell_tabs();
self.app.accept_current_focus_without_events();
self.send_shell_navigation_focus_events(
@@ -908,6 +908,12 @@ impl HeadlessServer {
&focused_tabs_after,
);
}
+ if focus_before != focus_after {
+ if let Some(target) = focus_after {
+ self.app
+ .emit_focus_api_events(target.workspace_index, target.pane_id);
+ }
+ }
let geometry_changed = method_claims_geometry
&& if reconcile {
self.reapply_controlled_shell_tab_geometry(false)
diff --git a/src/server/headless/tests/mod.rs b/src/server/headless/tests/mod.rs
index 41e08164..78949d6e 100644
--- a/src/server/headless/tests/mod.rs
+++ b/src/server/headless/tests/mod.rs
@@ -1595,6 +1595,119 @@ async fn client_local_navigation_does_not_emit_global_focus_transitions() {
}
#[tokio::test]
+async fn client_local_navigation_emits_pane_focused_only_when_that_client_moves() {
+ use api::schema::{EventData, Method, PaneTarget, TabTarget};
+
+ let event_hub = api::EventHub::default();
+ let mut server = test_headless_server_with_event_hub(event_hub.clone());
+ let mut workspace = crate::workspace::Workspace::test_new("plugin-focus-events");
+ let first_pane = workspace.tabs[0].root_pane;
+ let second_tab = workspace.test_add_tab(Some("second"));
+ let second_pane = workspace.tabs[second_tab].root_pane;
+ server.app.state.workspaces = vec![workspace];
+ server.app.state.ensure_test_terminals();
+ server.app.state.active = Some(0);
+ server.app.state.selected = 0;
+ server.app.state.mode = crate::app::Mode::Terminal;
+ let first_tab_id = server.app.public_tab_id(0, 0).unwrap();
+ let second_tab_id = server.app.public_tab_id(0, second_tab).unwrap();
+ let first_pane_id = server.app.public_pane_id(0, first_pane).unwrap();
+ let second_pane_id = server.app.public_pane_id(0, second_pane).unwrap();
+ let workspace_id = server.app.public_workspace_id(0);
+
+ let (first_control, _) = connect_matching_test_shell(&mut server, 61);
+ let (second_control, _) = connect_matching_test_shell(&mut server, 62);
+ let _ = first_control.recv().expect("first snapshot");
+ let _ = second_control.recv().expect("second snapshot");
+
+ let first_tab = TabTarget {
+ tab_id: first_tab_id,
+ };
+ let second_tab = TabTarget {
+ tab_id: second_tab_id,
+ };
+ let cases = [
+ (
+ 61,
+ Method::TabFocus(second_tab.clone()),
+ Some(&second_pane_id),
+ ),
+ // Both clients selecting the same destination must each notify plugins.
+ (
+ 62,
+ Method::TabFocus(second_tab.clone()),
+ Some(&second_pane_id),
+ ),
+ (61, Method::TabFocus(second_tab.clone()), None),
+ (61, Method::TabFocus(first_tab), Some(&first_pane_id)),
+ // Switching the server's default to this client's unchanged tab is not navigation.
+ (
+ 62,
+ Method::PaneFocus(PaneTarget {
+ pane_id: second_pane_id.clone(),
+ }),
+ None,
+ ),
+ (
+ 62,
+ Method::PaneFocus(PaneTarget {
+ pane_id: first_pane_id.clone(),
+ }),
+ Some(&first_pane_id),
+ ),
+ (
+ 61,
+ Method::TabFocus(second_tab.clone()),
+ Some(&second_pane_id),
+ ),
+ (61, Method::TabClose(second_tab), Some(&first_pane_id)),
+ ];
+ for (client_id, method, expected_pane) in cases {
+ let other_client = if client_id == 61 { 62 } else { 61 };
+ let other_focus = server.shell_focus_target(other_client);
+ let sequence = event_hub.current_sequence();
+ let (respond_to, response_rx) = std::sync::mpsc::channel();
+ server.handle_client_shell_api_request(
+ client_id,
+ api::ApiRequestMessage {
+ request: api::schema::Request {
+ id: "navigate".into(),
+ method,
+ },
+ respond_to,
+ response_write_complete: None,
+ stream_active: None,
+ },
+ );
+ let response = response_rx.recv().expect("navigation response");
+ assert!(
+ serde_json::from_str::<api::schema::SuccessResponse>(&response).is_ok(),
+ "{response}"
+ );
+ server.app.sync_focus_events();
+
+ let focused = event_hub
+ .events_after(sequence)
+ .into_iter()
+ .filter_map(|(_, event)| match event.data {
+ EventData::PaneFocused {
+ pane_id,
+ workspace_id,
+ } => Some((pane_id, workspace_id)),
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let expected = expected_pane
+ .map(|pane_id| (pane_id.clone(), workspace_id.clone()))
+ .into_iter()
+ .collect::<Vec<_>>();
+ assert_eq!(focused, expected, "client {client_id}");
+ assert_eq!(server.shell_focus_target(other_client), other_focus);
+ }
+ shutdown_test_runtimes(&mut server);
+}
+
+#[tokio::test]
async fn public_focus_moves_shell_focus_between_tabs() {
let mut server = test_headless_server();
let mut workspace = crate::workspace::Workspace::test_new("public-focus-events");