mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-22 00:01:06 +00:00
fix: restore plugin focus events for client navigation (#3850)
refs #3824
This commit is contained in:
@@ -821,6 +821,9 @@ that workspace.
|
||||
Pane event subscriptions include `pane.created`, `pane.updated`, `pane.closed`, `pane.focused`,
|
||||
`pane.moved`, `pane.exited`, `pane.agent_detected`,
|
||||
`pane.output_matched`, `pane.agent_status_changed`, and `pane.scroll_changed`.
|
||||
`pane.focused` also reports manual pane selection changes from any client attached
|
||||
to this server. It does not move other clients' views, and its payload does not
|
||||
identify the client. Selecting an already-selected pane does not emit it again.
|
||||
Terminal-title changes can emit `pane.updated`, but spinner-only raw-title changes do not emit it when `terminal_title_stripped` is unchanged.
|
||||
`pane.scroll_changed` is scoped to one `pane_id` and emits `pane_id`,
|
||||
`workspace_id`, and the current `scroll` metrics whenever Herdr observes a
|
||||
|
||||
+1
-1
@@ -799,7 +799,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_focus_api_events(&mut self, ws_idx: usize, pane_id: crate::layout::PaneId) {
|
||||
pub(crate) fn emit_focus_api_events(&mut self, ws_idx: usize, pane_id: crate::layout::PaneId) {
|
||||
self.emit_event(crate::api::schema::EventEnvelope {
|
||||
event: crate::api::schema::EventKind::WorkspaceFocused,
|
||||
data: crate::api::schema::EventData::WorkspaceFocused {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1594,6 +1594,119 @@ async fn client_local_navigation_does_not_emit_global_focus_transitions() {
|
||||
shutdown_test_runtimes(&mut server);
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
Reference in New Issue
Block a user