aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/headless/client_views.rs113
-rw-r--r--src/server/headless/notifications.rs2
-rw-r--r--src/server/headless/tests/mod.rs160
3 files changed, 269 insertions, 6 deletions
diff --git a/src/server/headless/client_views.rs b/src/server/headless/client_views.rs
index 3a0f895c..2d49a50a 100644
--- a/src/server/headless/client_views.rs
+++ b/src/server/headless/client_views.rs
@@ -281,6 +281,33 @@ impl HeadlessServer {
)
}
+ fn public_request_may_change_geometry(method: &api::schema::Method) -> bool {
+ use api::schema::Method;
+
+ matches!(
+ method,
+ Method::CommandInvoke(_)
+ | Method::LayoutSetSplitRatio(_)
+ | Method::PaneClose(_)
+ | Method::PaneEditScrollback(_)
+ | Method::PaneFocus(_)
+ | Method::PaneFocusDirection(_)
+ | Method::PaneResize(_)
+ | Method::PaneSplit(_)
+ | Method::PaneSwap(_)
+ | Method::PaneZoom(_)
+ | Method::TabClose(_)
+ | Method::TabCreate(_)
+ | Method::TabFocus(_)
+ | Method::WorkspaceClose(_)
+ | Method::WorkspaceCreate(_)
+ | Method::WorkspaceFocus(_)
+ | Method::WorktreeCreate(_)
+ | Method::WorktreeOpen(_)
+ | Method::WorktreeRemove(_)
+ )
+ }
+
pub(super) fn deferred_endpoint_navigation_tab_id(response: &[u8]) -> Option<String> {
let response = serde_json::from_slice::<serde_json::Value>(response).ok()?;
if response.pointer("/result/type")?.as_str()? != "worktree_created" {
@@ -522,6 +549,18 @@ impl HeadlessServer {
target: crate::ui::TabSurfaceTarget,
start_pending_agent_resumes: bool,
) -> bool {
+ if !self.resize_shell_tab_geometry_to_target(client_id, target) {
+ return false;
+ }
+ self.finish_shell_tab_geometry_change(start_pending_agent_resumes);
+ true
+ }
+
+ fn resize_shell_tab_geometry_to_target(
+ &mut self,
+ client_id: u64,
+ target: crate::ui::TabSurfaceTarget,
+ ) -> bool {
let Some(client) = self.clients.get(&client_id) else {
return false;
};
@@ -562,7 +601,6 @@ impl HeadlessServer {
{
let _ = resize_popup_runtime(&self.app, Rect::new(0, 0, cols, rows), cell_size);
}
- self.finish_shell_tab_geometry_change(start_pending_agent_resumes);
true
}
@@ -581,6 +619,66 @@ impl HeadlessServer {
self.apply_shell_tab_geometry(client_id, start_pending_agent_resumes)
}
+ pub(super) fn reapply_controlled_shell_tab_geometry(
+ &mut self,
+ start_pending_agent_resumes: bool,
+ ) -> bool {
+ let mut viewed_tabs = HashMap::<String, Vec<u64>>::new();
+ for (&client_id, client) in &self.clients {
+ if !client.is_shell_client() || client.writer.is_none() {
+ continue;
+ }
+ let Some(tab_id) = self.shell_tab_id_for_client(client_id) else {
+ continue;
+ };
+ viewed_tabs.entry(tab_id).or_default().push(client_id);
+ }
+ for viewers in viewed_tabs.values_mut() {
+ viewers.sort_unstable();
+ }
+ for (tab_id, viewers) in viewed_tabs {
+ let controller_is_viewing = self
+ .tab_geometry_controllers
+ .get(&tab_id)
+ .is_some_and(|controller| viewers.contains(controller));
+ if !controller_is_viewing {
+ self.tab_geometry_controllers.insert(tab_id, viewers[0]);
+ }
+ }
+
+ if self.resize_tabs_for_only_shell_client(start_pending_agent_resumes) {
+ return true;
+ }
+
+ let mut controlled_tabs = self
+ .tab_geometry_controllers
+ .iter()
+ .filter_map(|(tab_id, &client_id)| {
+ self.app
+ .parse_tab_id(tab_id)
+ .map(|(workspace_index, tab_index)| {
+ (
+ tab_id.clone(),
+ client_id,
+ crate::ui::TabSurfaceTarget {
+ workspace_index,
+ tab_index,
+ },
+ )
+ })
+ })
+ .collect::<Vec<_>>();
+ controlled_tabs.sort_unstable_by(|left, right| left.0.cmp(&right.0));
+ let mut reapplied = false;
+ for (_, client_id, target) in controlled_tabs {
+ reapplied |= self.resize_shell_tab_geometry_to_target(client_id, target);
+ }
+ if reapplied {
+ self.finish_shell_tab_geometry_change(start_pending_agent_resumes);
+ }
+ reapplied
+ }
+
pub(super) fn claim_shell_tab_geometry(
&mut self,
client_id: u64,
@@ -683,6 +781,7 @@ impl HeadlessServer {
) -> bool {
let target_before = self.default_shell_target();
let popup_before = self.app.state.popup_pane.is_some();
+ let method_claims_geometry = Self::public_request_may_change_geometry(&msg.request.method);
let explicit_public_focus_target = match &msg.request.method {
api::schema::Method::WorkspaceFocus(params) => self
.app
@@ -742,7 +841,9 @@ impl HeadlessServer {
if reconcile || target_changed || self.app.state.popup_pane.is_some() != popup_before {
self.reconcile_client_shell_locations();
}
- changed
+ let geometry_changed =
+ method_claims_geometry && self.reapply_controlled_shell_tab_geometry(false);
+ changed | geometry_changed
}
pub(super) fn handle_client_shell_api_request(
@@ -782,8 +883,12 @@ impl HeadlessServer {
);
}
let geometry_changed = method_claims_geometry
- && (self.claim_shell_tab_geometry(client_id, false)
- || self.resize_shell_tab_if_controller(client_id, false));
+ && if reconcile {
+ self.reapply_controlled_shell_tab_geometry(false)
+ } else {
+ self.claim_shell_tab_geometry(client_id, false)
+ || self.resize_shell_tab_if_controller(client_id, false)
+ };
changed | navigation_changed | geometry_changed
}
}
diff --git a/src/server/headless/notifications.rs b/src/server/headless/notifications.rs
index 6764dd2d..dd661809 100644
--- a/src/server/headless/notifications.rs
+++ b/src/server/headless/notifications.rs
@@ -652,6 +652,7 @@ impl HeadlessServer {
}
self.reconcile_client_shell_locations();
self.finish_shell_location_reconciliation(focus_before, &focused_tabs_before);
+ self.reapply_controlled_shell_tab_geometry(false);
for (pane_id, terminal_id) in shutdown_terminals {
if self.app.find_pane(pane_id).is_none() {
self.shutdown_terminal_stream_clients(
@@ -698,6 +699,7 @@ impl HeadlessServer {
}
self.reconcile_client_shell_locations();
self.finish_shell_location_reconciliation(focus_before, &focused_tabs_before);
+ self.reapply_controlled_shell_tab_geometry(false);
if self.app.find_pane(pane_id_val).is_none() {
if let Some(terminal_id) = terminal_id {
diff --git a/src/server/headless/tests/mod.rs b/src/server/headless/tests/mod.rs
index 29a4cfbf..77a16fc7 100644
--- a/src/server/headless/tests/mod.rs
+++ b/src/server/headless/tests/mod.rs
@@ -1578,6 +1578,108 @@ async fn repeated_layout_action_reapplies_controller_geometry() {
}
#[tokio::test]
+async fn public_close_reapplies_controller_geometry() {
+ let mut server = test_headless_server();
+ let mut workspace = crate::workspace::Workspace::test_new("public-close-geometry");
+ let first_pane = workspace.tabs[0].root_pane;
+ let second_pane = workspace.test_split(ratatui::layout::Direction::Vertical);
+ workspace.insert_test_runtime(
+ first_pane,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(80, 24, b""),
+ );
+ workspace.insert_test_runtime(
+ second_pane,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(80, 24, b""),
+ );
+ server.app.state.workspaces = vec![workspace];
+ server.app.state.active = Some(0);
+ server.app.state.selected = 0;
+ server.app.state.mode = crate::app::Mode::Terminal;
+ let second_pane_id = server.app.public_pane_id(0, second_pane).unwrap();
+
+ let (control, _) = connect_test_shell(&mut server, 66, 100, 30);
+ let _ = control.recv().expect("snapshot");
+ let shrunk = server.app.state.workspaces[0].test_runtimes[&first_pane].current_size();
+ assert!(shrunk.0 < 30);
+
+ let (respond_to, _response_rx) = std::sync::mpsc::channel();
+ assert!(
+ server.handle_api_request_with_shutdown_check(crate::api::ApiRequestMessage {
+ request: crate::api::schema::Request {
+ id: "public-close-geometry".into(),
+ method: crate::api::schema::Method::PaneClose(crate::api::schema::PaneTarget {
+ pane_id: second_pane_id,
+ }),
+ },
+ respond_to,
+ response_write_complete: None,
+ stream_active: None,
+ })
+ );
+
+ let runtime = &server.app.state.workspaces[0].test_runtimes[&first_pane];
+ let grown = runtime.current_size();
+ assert!(grown.0 > shrunk.0);
+ assert_eq!(runtime.terminal_dimensions(), Some((grown.1, grown.0)));
+ assert_eq!(
+ runtime.scroll_metrics().unwrap().viewport_rows,
+ grown.0 as usize
+ );
+ shutdown_test_runtimes(&mut server);
+}
+
+#[tokio::test]
+async fn geometry_reapply_replaces_a_controller_that_left_the_tab() {
+ let mut server = test_headless_server();
+ let mut workspace = crate::workspace::Workspace::test_new("geometry-controller-viewer");
+ 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;
+ let third_tab = workspace.test_add_tab(Some("third"));
+ let third_pane = workspace.tabs[third_tab].root_pane;
+ for pane_id in [first_pane, second_pane, third_pane] {
+ workspace.insert_test_runtime(
+ pane_id,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(80, 24, b""),
+ );
+ }
+ server.app.state.workspaces = vec![workspace];
+ server.app.state.active = Some(0);
+ server.app.state.selected = 0;
+ server.app.state.mode = crate::app::Mode::Terminal;
+ let second_tab_id = server.app.public_tab_id(0, second_tab).unwrap();
+ let third_tab_id = server.app.public_tab_id(0, third_tab).unwrap();
+
+ let (first_control, _) = connect_test_shell(&mut server, 67, 100, 30);
+ let (second_control, _) = connect_test_shell(&mut server, 68, 70, 20);
+ let _ = first_control.recv().expect("first snapshot");
+ let _ = second_control.recv().expect("second snapshot");
+
+ assert!(server.focus_shell_client_on_tab(67, &second_tab_id));
+ assert!(server.claim_shell_tab_geometry(67, false));
+ assert!(server.focus_shell_client_on_tab(67, &third_tab_id));
+ assert!(server.claim_shell_tab_geometry(67, false));
+ assert!(server.focus_shell_client_on_tab(68, &second_tab_id));
+ assert_eq!(
+ server.tab_geometry_controllers.get(&second_tab_id),
+ Some(&67)
+ );
+ let stale_size = server.app.state.workspaces[0].test_runtimes[&second_pane].current_size();
+
+ assert!(server.reapply_controlled_shell_tab_geometry(false));
+
+ assert_eq!(
+ server.tab_geometry_controllers.get(&second_tab_id),
+ Some(&68)
+ );
+ assert_ne!(
+ server.app.state.workspaces[0].test_runtimes[&second_pane].current_size(),
+ stale_size
+ );
+ shutdown_test_runtimes(&mut server);
+}
+
+#[tokio::test]
async fn client_shell_tabs_render_accept_input_and_resize_independently() {
let mut server = test_headless_server();
let mut workspace = crate::workspace::Workspace::test_new("independent-geometry");
@@ -3436,8 +3538,8 @@ async fn pane_death_reconciles_each_client_view_and_focus() {
.public_tab_id(0, second_tab)
.expect("second tab id");
- let (first_control, _) = connect_matching_test_shell(&mut server, 71);
- let (second_control, _) = connect_matching_test_shell(&mut server, 72);
+ let (first_control, _) = connect_test_shell(&mut server, 71, 100, 30);
+ let (second_control, _) = connect_test_shell(&mut server, 72, 70, 20);
let _ = first_control.recv().expect("first snapshot");
let _ = second_control.recv().expect("second snapshot");
assert!(server.focus_shell_client_on_tab(72, &second_tab_id));
@@ -3464,6 +3566,60 @@ async fn pane_death_reconciles_each_client_view_and_focus() {
second_input.try_recv().is_err(),
"focus gain was duplicated"
);
+ assert_eq!(
+ server.tab_geometry_controllers.get(&second_tab_id),
+ Some(&71)
+ );
+ let before_resize = server.app.state.workspaces[0].test_runtimes[&second_pane].current_size();
+ assert!(server.handle_server_event(ServerEvent::ClientShellResize {
+ client_id: 71,
+ surface_cols: 90,
+ surface_rows: 25,
+ cell_width_px: 0,
+ cell_height_px: 0,
+ pixel_mouse: false,
+ }));
+ assert_ne!(
+ server.app.state.workspaces[0].test_runtimes[&second_pane].current_size(),
+ before_resize
+ );
+ shutdown_test_runtimes(&mut server);
+}
+
+#[tokio::test]
+async fn pane_death_reapplies_controller_geometry() {
+ let mut server = test_headless_server();
+ let mut workspace = crate::workspace::Workspace::test_new("pane-death-geometry");
+ let first_pane = workspace.tabs[0].root_pane;
+ let dead_pane = workspace.test_split(ratatui::layout::Direction::Vertical);
+ workspace.insert_test_runtime(
+ first_pane,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(80, 24, b""),
+ );
+ workspace.insert_test_runtime(
+ dead_pane,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(80, 24, b""),
+ );
+ server.app.state.workspaces = vec![workspace];
+ server.app.state.active = Some(0);
+ server.app.state.selected = 0;
+ server.app.state.mode = crate::app::Mode::Terminal;
+
+ let (control, _) = connect_test_shell(&mut server, 73, 185, 46);
+ let _ = control.recv().expect("snapshot");
+ let shrunk = server.app.state.workspaces[0].test_runtimes[&first_pane].current_size();
+ assert!(shrunk.0 < 46);
+
+ assert!(server.handle_internal_event_with_forwarding(AppEvent::PaneDied { pane_id: dead_pane }));
+
+ let runtime = &server.app.state.workspaces[0].test_runtimes[&first_pane];
+ let grown = runtime.current_size();
+ assert!(grown.0 > shrunk.0);
+ assert_eq!(runtime.terminal_dimensions(), Some((grown.1, grown.0)));
+ assert_eq!(
+ runtime.scroll_metrics().unwrap().viewport_rows,
+ grown.0 as usize
+ );
shutdown_test_runtimes(&mut server);
}