fix: preserve pane size while detached

This commit is contained in:
Ogulcan Celik 2026-04-29 23:40:09 +03:00
parent c950670dca
commit d8bb2ebed9
2 changed files with 121 additions and 2 deletions

View File

@ -1529,10 +1529,11 @@ impl HeadlessServer {
if render_targets.is_empty() {
let (cols, rows) = self.effective_size;
let area = Rect::new(0, 0, cols, rows);
let _ = render_virtual(&mut self.app.state, area, true);
let resize_panes = self.app.state.view.pane_infos.is_empty();
let _ = render_virtual(&mut self.app.state, area, resize_panes);
debug!(
cols,
rows, "rendered virtual frame with no attached clients"
rows, resize_panes, "rendered virtual frame with no attached clients"
);
return;
}

View File

@ -168,6 +168,66 @@ fn pane_read_recent(socket_path: &PathBuf, pane_id: &str) -> Value {
)
}
fn pane_read_recent_text(socket_path: &PathBuf, pane_id: &str) -> String {
pane_read_recent(socket_path, pane_id)["result"]["read"]["text"]
.as_str()
.unwrap_or_default()
.to_string()
}
fn pane_send_text(socket_path: &PathBuf, pane_id: &str, text: &str) -> Value {
send_json_request(
socket_path,
&format!(
r#"{{"id":"pane_send_text","method":"pane.send_text","params":{{"pane_id":"{pane_id}","text":{}}}}}"#,
serde_json::to_string(text).unwrap()
),
)
}
fn parse_size_after_marker(text: &str, marker: &str) -> Option<(u16, u16)> {
let lines: Vec<&str> = text.lines().collect();
for (idx, line) in lines.iter().enumerate() {
if !line.contains(marker) {
continue;
}
for candidate in lines.iter().skip(idx + 1).take(6) {
let mut parts = candidate.split_whitespace();
let Some(rows) = parts.next().and_then(|part| part.parse::<u16>().ok()) else {
continue;
};
let Some(cols) = parts.next().and_then(|part| part.parse::<u16>().ok()) else {
continue;
};
if parts.next().is_none() {
return Some((rows, cols));
}
}
}
None
}
fn read_pane_tty_size_after_marker(
socket_path: &PathBuf,
pane_id: &str,
marker: &str,
timeout: Duration,
) -> (u16, u16) {
pane_send_text(socket_path, pane_id, &format!("echo {marker}; stty size\n"));
let deadline = Instant::now() + timeout;
let mut last_text = String::new();
while Instant::now() < deadline {
last_text = pane_read_recent_text(socket_path, pane_id);
if let Some(size) = parse_size_after_marker(&last_text, marker) {
return size;
}
thread::sleep(Duration::from_millis(50));
}
panic!("did not observe tty size after marker {marker}. pane output:\n{last_text}");
}
fn workspace_id_by_label(response: &Value, label: &str) -> String {
response["result"]["workspaces"]
.as_array()
@ -578,6 +638,64 @@ fn server_persists_after_client_connection_drop() {
cleanup_spawned_herdr(spawned, base);
}
#[test]
fn detached_output_preserves_last_attached_pty_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(&config_home, &runtime_dir, &api_socket, &client_socket);
wait_for_socket(&api_socket, Duration::from_secs(10));
wait_for_file(&client_socket, Duration::from_secs(10));
let mut stream = UnixStream::connect(&client_socket).expect("client should connect");
let (version, error) =
client_handshake(&mut stream, 2, 120, 40).expect("handshake should succeed");
assert_eq!(version, 2);
assert!(error.is_none(), "{:?}", error);
drain_messages(&mut stream);
let create = workspace_create(&api_socket, "detached-size");
let pane_id = create["result"]["root_pane"]["pane_id"]
.as_str()
.expect("root pane id")
.to_string();
let before = read_pane_tty_size_after_marker(
&api_socket,
&pane_id,
"SIZE_BEFORE_DETACH",
Duration::from_secs(5),
);
send_detach(&mut stream).expect("send detach");
drop(stream);
assert!(
wait_until(Duration::from_secs(2), Duration::from_millis(25), || {
ping_socket(&api_socket).contains("pong")
}),
"server should persist after detach"
);
let while_detached = read_pane_tty_size_after_marker(
&api_socket,
&pane_id,
"SIZE_WHILE_DETACHED",
Duration::from_secs(5),
);
assert_eq!(
while_detached, before,
"detached renders should not resize live pane PTYs to a fallback size"
);
cleanup_spawned_herdr(spawned, base);
}
#[test]
fn output_accumulated_while_detached_visible_on_reattach() {
// Output produced while detached is visible in the