From f8125c4129ce0a287ba5884b2bfc85cb2dd3ef35 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Sat, 15 Aug 2026 03:39:53 +0300 Subject: [PATCH] feat: make headless terminal size configurable refs #2828 --- docs/next/CHANGELOG.md | 1 + .../src/content/docs/configuration.mdx | 12 ++ .../website/src/data/config-reference.json | 18 +++ src/app/mod.rs | 12 +- src/app/state.rs | 16 +- src/config.rs | 20 +++ src/config/io.rs | 9 ++ src/config/model.rs | 58 +++++++ src/main.rs | 6 + src/server/headless.rs | 47 ++++-- tests/api_ping.rs | 2 +- tests/detach_reattach.rs | 145 +++++++++++++++++- 12 files changed, 328 insertions(+), 18 deletions(-) diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index cf3531e8ee..619f689407 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -16,6 +16,7 @@ - The plugin marketplace now discovers valid manifests at repository roots and subdirectories, groups multiple plugins under each repository, and publishes their versions and exact default-branch commits. ### Changed +- Headless servers now use a configurable 120×40 virtual terminal instead of 80×24 when no client is attached, giving newly created panes a practical default size. (#2828) - Desktop tab labels are now centered in their tabs, so the active-tab highlight has symmetric padding. - Bumped the client/server protocol version to 20 for pane terminal bell forwarding. - Experimental pane graphics now support bounded named layers, acknowledged full-RGBA primary-layer direct file frames on audited local terminals, owned BGRA fallback, exact pixel mouse input, and placement-only resize replay. diff --git a/docs/next/website/src/content/docs/configuration.mdx b/docs/next/website/src/content/docs/configuration.mdx index 4ba3a07aee..9f6a96f316 100644 --- a/docs/next/website/src/content/docs/configuration.mdx +++ b/docs/next/website/src/content/docs/configuration.mdx @@ -50,6 +50,18 @@ You can also open the global menu in Herdr and choose `reload config`. Reload applies most UI settings without restarting panes. Startup-only settings still need a restart. +## Headless terminal size + +When no client is attached, the server uses a 120×40 virtual terminal for layout and newly created panes. Change that fallback for headless orchestration with: + +```toml +[server] +headless_cols = 160 +headless_rows = 50 +``` + +An attached client remains authoritative for the shared runtime size. After it detaches, existing pane PTYs retain their last attached size while new headless layout uses the configured fallback. + ## Terminal defaults Set the executable Herdr uses for newly created interactive panes: diff --git a/docs/next/website/src/data/config-reference.json b/docs/next/website/src/data/config-reference.json index c16db7a193..a2f8d7a1ec 100644 --- a/docs/next/website/src/data/config-reference.json +++ b/docs/next/website/src/data/config-reference.json @@ -12,6 +12,24 @@ } ] }, + { + "id": "server", + "title": "Server", + "keys": [ + { + "key": "server.headless_cols", + "type": "integer", + "default": "120", + "description": "Virtual terminal width used for layout and newly created panes when no client is attached. Must be greater than zero." + }, + { + "key": "server.headless_rows", + "type": "integer", + "default": "40", + "description": "Virtual terminal height used for layout and newly created panes when no client is attached. Must be greater than zero." + } + ] + }, { "id": "theme", "title": "Theme", diff --git a/src/app/mod.rs b/src/app/mod.rs index 49a905ae32..3524411530 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -619,6 +619,7 @@ impl App { outer_terminal_focus: None, prefix_code, prefix_mods, + headless_size: config.headless_size(), default_sidebar_width: config.ui.sidebar_width, sidebar_width, sidebar_min_width, @@ -1539,6 +1540,14 @@ impl App { } } + if !invalid_section("server") { + if let Some(diagnostic) = config.invalid_headless_size_diagnostic() { + diagnostics.push(format!("{diagnostic}; keeping current [server] settings")); + } else { + self.state.headless_size = config.headless_size(); + } + } + if !invalid_section("advanced") { self.state.pane_scrollback_limit_bytes = config.advanced.scrollback_limit_bytes; } @@ -2974,7 +2983,7 @@ mod tests { std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write( &path, - "[terminal]\ndefault_shell = \"nu\"\nshell_mode = \"non_login\"\nnew_cwd = \"home\"\n[keys]\nnew_workspace = \"prefix+m\"\nprefix = \"ctrl+a\"\n[update]\nversion_check = false\nmanifest_check = false\n[ui]\nagent_panel_sort = \"priority\"\nredraw_on_focus_gained = false\ncopy_on_select = false\nright_click_passthrough_modifier = \"ctrl\"\nprompt_new_workspace_name = true\n[ui.toast]\ndelivery = \"herdr\"\n[experimental]\nswitch_ascii_input_source_in_prefix = true\n", + "[terminal]\ndefault_shell = \"nu\"\nshell_mode = \"non_login\"\nnew_cwd = \"home\"\n[keys]\nnew_workspace = \"prefix+m\"\nprefix = \"ctrl+a\"\n[update]\nversion_check = false\nmanifest_check = false\n[server]\nheadless_cols = 160\nheadless_rows = 50\n[ui]\nagent_panel_sort = \"priority\"\nredraw_on_focus_gained = false\ncopy_on_select = false\nright_click_passthrough_modifier = \"ctrl\"\nprompt_new_workspace_name = true\n[ui.toast]\ndelivery = \"herdr\"\n[experimental]\nswitch_ascii_input_source_in_prefix = true\n", ) .unwrap(); std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path); @@ -3008,6 +3017,7 @@ mod tests { let report = app.reload_config(); assert_eq!(report.status, crate::config::ConfigReloadStatus::Applied); + assert_eq!(app.state.headless_size, (160, 50)); assert_eq!(app.state.prefix_code, KeyCode::Char('a')); assert_eq!(app.state.prefix_mods, KeyModifiers::CONTROL); assert!(app diff --git a/src/app/state.rs b/src/app/state.rs index ea550472ac..e03d90a90d 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1428,6 +1428,8 @@ pub struct AppState { // Config pub prefix_code: KeyCode, pub prefix_mods: KeyModifiers, + /// Virtual terminal size (columns, rows) used when no client is attached. + pub(crate) headless_size: (u16, u16), pub default_sidebar_width: u16, pub sidebar_width: u16, pub sidebar_min_width: u16, @@ -1618,7 +1620,7 @@ impl AppState { if let Some(info) = self.view.pane_infos.first() { (info.rect.height, info.rect.width) } else { - (24, 80) + (self.headless_size.1, self.headless_size.0) } } @@ -1800,6 +1802,10 @@ impl AppState { outer_terminal_focus: None, prefix_code: KeyCode::Char('b'), prefix_mods: KeyModifiers::CONTROL, + headless_size: ( + crate::config::DEFAULT_HEADLESS_COLS, + crate::config::DEFAULT_HEADLESS_ROWS, + ), default_sidebar_width: 26, sidebar_width: 26, sidebar_min_width: 18, @@ -2242,6 +2248,14 @@ mod tests { use super::*; use crossterm::event::KeyEvent; + #[test] + fn pane_size_estimate_uses_headless_size_before_first_view() { + let mut state = AppState::test_new(); + state.headless_size = (132, 41); + + assert_eq!(state.estimate_pane_size(), (41, 132)); + } + #[test] fn agent_terminal_keeps_final_child_cursor_exposed() { let mut state = AppState::test_new(); diff --git a/src/config.rs b/src/config.rs index 7e7aecd215..0595ae2f76 100644 --- a/src/config.rs +++ b/src/config.rs @@ -53,6 +53,8 @@ pub const CONFIG_PATH_ENV_VAR: &str = "HERDR_CONFIG_PATH"; pub const DEFAULT_SCROLLBACK_LIMIT_BYTES: usize = 10_000_000; pub const DEFAULT_MOUSE_SCROLL_LINES: usize = 3; pub const DEFAULT_MOBILE_WIDTH_THRESHOLD: u16 = 64; +pub const DEFAULT_HEADLESS_COLS: u16 = 120; +pub const DEFAULT_HEADLESS_ROWS: u16 = 40; #[cfg(test)] pub(crate) fn app_dir_name() -> &'static str { @@ -90,9 +92,27 @@ impl Config { .chain(tab_bar_right_diagnostics(&self.ui.tab_bar_right)) .chain(window_title_diagnostics(&self.ui.window_title)) .chain(self.invalid_sidebar_bounds_diagnostic()) + .chain(self.invalid_headless_size_diagnostic()) .collect() } + pub(crate) fn headless_size(&self) -> (u16, u16) { + if self.invalid_headless_size_diagnostic().is_some() { + (DEFAULT_HEADLESS_COLS, DEFAULT_HEADLESS_ROWS) + } else { + (self.server.headless_cols, self.server.headless_rows) + } + } + + pub(crate) fn invalid_headless_size_diagnostic(&self) -> Option { + (self.server.headless_cols == 0 || self.server.headless_rows == 0).then(|| { + format!( + "server.headless_cols and server.headless_rows must be greater than zero (got {}x{})", + self.server.headless_cols, self.server.headless_rows + ) + }) + } + pub(crate) fn invalid_sidebar_bounds_diagnostic(&self) -> Option { validated_sidebar_bounds(self.ui.sidebar_min_width, self.ui.sidebar_max_width) .is_none() diff --git a/src/config/io.rs b/src/config/io.rs index 7ed82395a1..898cf07715 100644 --- a/src/config/io.rs +++ b/src/config/io.rs @@ -10,6 +10,7 @@ const KNOWN_TOP_LEVEL_CONFIG_KEYS: &[&str] = &[ "keys", "onboarding", "remote", + "server", "session", "terminal", "theme", @@ -292,6 +293,14 @@ fn load_live_config_from_str(content: &str) -> Result> &mut invalid_sections, |section| config.session = section, ); + load_live_section( + table, + "server", + "server config", + &mut diagnostics, + &mut invalid_sections, + |section| config.server = section, + ); load_live_section( table, "update", diff --git a/src/config/model.rs b/src/config/model.rs index 6d6083ad77..ad779d263b 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -315,6 +315,7 @@ pub struct Config { pub theme: ThemeConfig, pub terminal: TerminalConfig, pub session: SessionConfig, + pub server: ServerConfig, pub update: UpdateConfig, pub keys: KeysConfig, pub ui: UiConfig, @@ -938,6 +939,15 @@ impl ImeCursorShape { } } +#[derive(Debug, Deserialize)] +#[serde(default)] +pub struct ServerConfig { + /// Virtual terminal width used when no client is attached. Default: 120. + pub headless_cols: u16, + /// Virtual terminal height used when no client is attached. Default: 40. + pub headless_rows: u16, +} + #[derive(Debug, Deserialize)] #[serde(default)] pub struct AdvancedConfig { @@ -1200,6 +1210,15 @@ impl<'de> Deserialize<'de> for ToastConfig { } } +impl Default for ServerConfig { + fn default() -> Self { + Self { + headless_cols: crate::config::DEFAULT_HEADLESS_COLS, + headless_rows: crate::config::DEFAULT_HEADLESS_ROWS, + } + } +} + impl Default for AdvancedConfig { fn default() -> Self { Self { @@ -1817,6 +1836,45 @@ delay_seconds = {} assert!(!config.should_show_onboarding()); } + #[test] + fn server_headless_size_defaults_and_parses() { + let default_config = Config::default(); + assert_eq!( + default_config.server.headless_cols, + crate::config::DEFAULT_HEADLESS_COLS + ); + assert_eq!( + default_config.server.headless_rows, + crate::config::DEFAULT_HEADLESS_ROWS + ); + + let config: Config = toml::from_str( + r#"[server] +headless_cols = 160 +headless_rows = 50 +"#, + ) + .unwrap(); + assert_eq!(config.server.headless_cols, 160); + assert_eq!(config.server.headless_rows, 50); + + let invalid: Config = toml::from_str( + r#"[server] +headless_cols = 0 +headless_rows = 50 +"#, + ) + .unwrap(); + assert!(invalid.invalid_headless_size_diagnostic().is_some()); + assert_eq!( + invalid.headless_size(), + ( + crate::config::DEFAULT_HEADLESS_COLS, + crate::config::DEFAULT_HEADLESS_ROWS + ) + ); + } + #[test] fn advanced_defaults_include_scrollback_limit_bytes() { let config = Config::default(); diff --git a/src/main.rs b/src/main.rs index 8505e5cc28..c159db3f28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -251,6 +251,12 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration # workspaces = "" # e.g. "ctrl+shift" makes ctrl+shift+1..9 switch workspaces directly # agents = "" # e.g. "alt" makes alt+1..9 focus agent rows directly +# Size of the virtual terminal used when no client is attached. +# Attached clients always use their own terminal size. +[server] +# headless_cols = 120 +# headless_rows = 40 + # [worktrees] # directory = "~/.herdr/worktrees" diff --git a/src/server/headless.rs b/src/server/headless.rs index ac1e7db227..354b0008dd 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -252,10 +252,6 @@ fn dirty_patch_intersects_hyperlinks( // Constants // --------------------------------------------------------------------------- -/// Default shared runtime size (columns, rows) when no clients are attached. -const MIN_COLS: u16 = 80; -const MIN_ROWS: u16 = 24; - /// Timeout for in-flight API requests during shutdown. #[allow(dead_code)] const SHUTDOWN_API_TIMEOUT: Duration = Duration::from_secs(5); @@ -326,8 +322,10 @@ pub struct HeadlessServer { deferred_alt_screen_reads: Vec, /// Monotonic activity counter used to pick the most recently active client. next_activity_stamp: u64, - /// Shared pane runtime size derived from the foreground client, - /// or MIN_COLS × MIN_ROWS when no clients are connected. + /// Configured virtual terminal size used when no clients are connected. + headless_size: (u16, u16), + /// Shared pane runtime size derived from the foreground client, or the + /// configured headless size when no clients are connected. effective_size: (u16, u16), /// Flag set when shutdown is initiated. shutting_down: bool, @@ -495,6 +493,7 @@ impl HeadlessServer { spawn_windows_client_accept_thread(listener, should_quit.clone(), server_event_tx.clone()); let server_keybindings = app_keybindings(&app); + let headless_size = app.state.headless_size; let (server_config_diagnostic, server_config_diagnostic_without_keybindings) = server_config_diagnostic_summaries(config_diagnostics); #[cfg(not(unix))] @@ -521,7 +520,8 @@ impl HeadlessServer { pending_alt_screen_reads: Vec::new(), deferred_alt_screen_reads: Vec::new(), next_activity_stamp: 1, - effective_size: (MIN_COLS, MIN_ROWS), + headless_size, + effective_size: headless_size, shutting_down: false, handoff_in_progress: false, #[cfg(unix)] @@ -1139,6 +1139,14 @@ impl HeadlessServer { } } + fn sync_headless_view_geometry(&mut self) { + crate::ui::compute_view_without_resizing_panes( + &mut self.app.state, + &self.app.terminal_runtimes, + Rect::new(0, 0, self.headless_size.0, self.headless_size.1), + ); + } + fn sync_foreground_client_state(&mut self) { self.app.direct_graphics_available = self.direct_graphics_available(); self.app.pixel_mouse_available = self.foreground_client_id.is_some_and(|id| { @@ -1150,9 +1158,10 @@ impl HeadlessServer { self.retire_all_direct_graphics(); } let Some(client_id) = self.foreground_client_id else { - self.effective_size = (MIN_COLS, MIN_ROWS); + self.effective_size = self.headless_size; self.app.state.outer_terminal_focus = None; self.app.state.host_cell_size = crate::kitty_graphics::HostCellSize::default(); + self.sync_headless_view_geometry(); let server_keybindings = self.server_keybindings.clone(); apply_keybindings(&mut self.app, &server_keybindings); self.sync_visible_server_config_diagnostic(false); @@ -1160,9 +1169,10 @@ impl HeadlessServer { }; let Some(client) = self.clients.get(&client_id) else { self.foreground_client_id = None; - self.effective_size = (MIN_COLS, MIN_ROWS); + self.effective_size = self.headless_size; self.app.state.outer_terminal_focus = None; self.app.state.host_cell_size = crate::kitty_graphics::HostCellSize::default(); + self.sync_headless_view_geometry(); let server_keybindings = self.server_keybindings.clone(); apply_keybindings(&mut self.app, &server_keybindings); self.sync_visible_server_config_diagnostic(false); @@ -1507,6 +1517,7 @@ impl HeadlessServer { let report = self.app.apply_config_from_disk(notify_success); self.app.take_config_reloaded_from_disk(); self.server_keybindings = app_keybindings(&self.app); + self.headless_size = self.app.state.headless_size; let (server_config_diagnostic, server_config_diagnostic_without_keybindings) = server_config_diagnostic_summaries(&report.diagnostics); self.server_config_diagnostic = server_config_diagnostic; @@ -5306,6 +5317,7 @@ mod tests { #[cfg(windows)] spawn_windows_client_accept_thread(listener, should_quit.clone(), server_event_tx.clone()); let server_keybindings = app_keybindings(&app); + let headless_size = app.state.headless_size; HeadlessServer { app, @@ -5329,7 +5341,8 @@ mod tests { pending_alt_screen_reads: Vec::new(), deferred_alt_screen_reads: Vec::new(), next_activity_stamp: 1, - effective_size: (MIN_COLS, MIN_ROWS), + headless_size, + effective_size: headless_size, shutting_down: false, handoff_in_progress: false, #[cfg(unix)] @@ -5380,6 +5393,20 @@ mod tests { } } + #[test] + fn default_headless_size_is_effective_without_clients() { + let server = test_headless_server(); + + assert_eq!( + server.headless_size, + ( + crate::config::DEFAULT_HEADLESS_COLS, + crate::config::DEFAULT_HEADLESS_ROWS + ) + ); + assert_eq!(server.effective_size, server.headless_size); + } + #[tokio::test] async fn headless_api_reads_latest_title_without_spinner_event_flooding() { let event_hub = api::EventHub::default(); diff --git a/tests/api_ping.rs b/tests/api_ping.rs index f4f92da681..bfecc27af3 100644 --- a/tests/api_ping.rs +++ b/tests/api_ping.rs @@ -502,7 +502,7 @@ fn workspace_list_and_create_round_trip() { let recent = send_request( &socket_path, &format!( - r#"{{"id":"req_11","method":"pane.read","params":{{"pane_id":"{}","source":"recent","lines":20}}}}"#, + r#"{{"id":"req_11","method":"pane.read","params":{{"pane_id":"{}","source":"recent","lines":50}}}}"#, pane_id ), ); diff --git a/tests/detach_reattach.rs b/tests/detach_reattach.rs index 3abe1bec32..4461a17b24 100644 --- a/tests/detach_reattach.rs +++ b/tests/detach_reattach.rs @@ -19,6 +19,19 @@ use support::{ wait_for_disconnect, wait_for_message_variant, wait_for_socket, wait_until, CURRENT_PROTOCOL, }; +const CUSTOM_HEADLESS_SIZE_CONFIG: &str = r#"onboarding = false + +[server] +headless_cols = 132 +headless_rows = 41 + +[ui] +sidebar_start_collapsed = true +sidebar_collapsed_mode = "hidden" +hide_tab_bar_when_single_tab = true +pane_scrollbars = false +"#; + fn unique_test_dir() -> PathBuf { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -70,19 +83,31 @@ fn test_lock() -> MutexGuard<'static, ()> { } fn spawn_server( + config_home: &PathBuf, + runtime_dir: &PathBuf, + api_socket_path: &PathBuf, + client_socket_path: &PathBuf, +) -> SpawnedHerdr { + spawn_server_with_config( + config_home, + runtime_dir, + api_socket_path, + client_socket_path, + "onboarding = false\n", + ) +} + +fn spawn_server_with_config( config_home: &PathBuf, runtime_dir: &PathBuf, api_socket_path: &PathBuf, _client_socket_path: &PathBuf, + config: &str, ) -> SpawnedHerdr { fs::create_dir_all(config_home.join("herdr")).unwrap(); fs::create_dir_all(runtime_dir).unwrap(); register_runtime_dir(runtime_dir); - fs::write( - config_home.join("herdr/config.toml"), - "onboarding = false\n", - ) - .unwrap(); + fs::write(config_home.join("herdr/config.toml"), config).unwrap(); let pair = native_pty_system() .openpty(PtySize { @@ -99,6 +124,7 @@ fn spawn_server( cmd.env("XDG_RUNTIME_DIR", runtime_dir); cmd.env("HERDR_SOCKET_PATH", api_socket_path); cmd.env_remove("HERDR_CLIENT_SOCKET_PATH"); + cmd.env("HERDR_CONFIG_PATH", config_home.join("herdr/config.toml")); cmd.env("SHELL", "/bin/sh"); cmd.env_remove("HERDR_ENV"); @@ -638,6 +664,115 @@ fn server_persists_after_client_connection_drop() { cleanup_spawned_herdr(spawned, base); } +#[test] +fn pane_created_without_client_uses_configured_headless_size() { + let _lock = test_lock(); + let base = unique_test_dir(); + let config_home = base.join("config"); + let runtime_dir = base.join("runtime"); + let api_socket = runtime_dir.join("herdr.sock"); + let client_socket = runtime_dir.join("herdr-client.sock"); + + let spawned = spawn_server_with_config( + &config_home, + &runtime_dir, + &api_socket, + &client_socket, + CUSTOM_HEADLESS_SIZE_CONFIG, + ); + wait_for_socket(&api_socket, Duration::from_secs(10)); + + let create = workspace_create(&api_socket, "headless-size"); + let pane_id = create["result"]["root_pane"]["pane_id"] + .as_str() + .expect("root pane id") + .to_string(); + + // A second request cannot run until the full render triggered by workspace + // creation has applied the virtual headless geometry. + assert!(ping_socket(&api_socket).contains("pong")); + let size = read_pane_tty_size_after_marker( + &api_socket, + &pane_id, + "HEADLESS_SIZE", + Duration::from_secs(5), + ); + + assert_eq!(size, (41, 132)); + + cleanup_spawned_herdr(spawned, base); +} + +#[test] +fn pane_created_after_detach_uses_configured_headless_size() { + let _lock = test_lock(); + let base = unique_test_dir(); + let config_home = base.join("config"); + let runtime_dir = base.join("runtime"); + let api_socket = runtime_dir.join("herdr.sock"); + let client_socket = runtime_dir.join("herdr-client.sock"); + + let spawned = spawn_server_with_config( + &config_home, + &runtime_dir, + &api_socket, + &client_socket, + CUSTOM_HEADLESS_SIZE_CONFIG, + ); + wait_for_socket(&api_socket, Duration::from_secs(10)); + wait_for_socket(&client_socket, Duration::from_secs(10)); + + let mut stream = UnixStream::connect(&client_socket).expect("client should connect"); + let (version, error) = + client_handshake(&mut stream, CURRENT_PROTOCOL, 160, 50).expect("handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); + assert!(error.is_none(), "{error:?}"); + drain_messages(&mut stream); + + let first = workspace_create(&api_socket, "attached-size"); + let first_pane_id = first["result"]["root_pane"]["pane_id"] + .as_str() + .expect("first root pane id") + .to_string(); + let attached_size = read_pane_tty_size_after_marker( + &api_socket, + &first_pane_id, + "ATTACHED_SIZE", + Duration::from_secs(5), + ); + assert_eq!(attached_size, (50, 160)); + + send_detach(&mut stream).expect("send detach"); + assert!( + wait_for_disconnect(&mut stream, Duration::from_secs(2)).expect("wait for detach"), + "detached client connection should close" + ); + drop(stream); + + let second = workspace_create(&api_socket, "headless-size"); + let second_pane_id = second["result"]["root_pane"]["pane_id"] + .as_str() + .expect("second root pane id") + .to_string(); + let headless_size = read_pane_tty_size_after_marker( + &api_socket, + &second_pane_id, + "HEADLESS_SIZE_AFTER_DETACH", + Duration::from_secs(5), + ); + let preserved_size = read_pane_tty_size_after_marker( + &api_socket, + &first_pane_id, + "PRESERVED_SIZE_AFTER_DETACH", + Duration::from_secs(5), + ); + + assert_eq!(headless_size, (41, 132)); + assert_eq!(preserved_size, attached_size); + + cleanup_spawned_herdr(spawned, base); +} + #[test] fn detached_output_preserves_last_attached_pty_size() { let _lock = test_lock();