pub(crate) mod helpers;
pub(crate) mod options;
pub(crate) mod option_catalog;
mod connection;

use std::io::{self, Write};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
use std::env;
use std::net::TcpListener;

use portable_pty::native_pty_system;
use ratatui::prelude::Rect;

use crate::types::{AppState, CtrlReq, Mode, FocusDir, LayoutKind, PipePaneState, VERSION,
    WaitChannel, WaitForOp, Node, Action, Bind};
use crate::platform::install_console_ctrl_handler;
use crate::pane::{create_window, create_window_raw, split_active_with_command, kill_active_pane, kill_pane_by_id, spawn_warm_pane};
use crate::tree::{self, active_pane, active_pane_mut, resize_all_panes, kill_all_children,
    find_window_index_by_id, focus_pane_by_id, focus_pane_by_id_no_mru, focus_pane_by_index, get_active_pane_id,
    get_split_mut, path_exists};

use helpers::{collect_pane_paths_server, serialize_bindings_json, json_escape_string,
    list_windows_json_with_tabs, combined_data_version, TMUX_COMMANDS};
use options::{get_option_value, get_window_option_value, render_window_options, apply_set_option};

use crate::input::{send_text_to_active, send_key_to_active, send_paste_to_active, move_focus, move_focus_preserving_zoom, find_best_pane_in_direction, find_wrap_target};
use crate::copy_mode::{enter_copy_mode, exit_copy_mode, move_copy_cursor, current_prompt_pos,
    yank_selection, scroll_copy_up, scroll_copy_down, switch_with_copy_save,
    capture_active_pane_text, capture_active_pane_range, capture_active_pane_styled};
use crate::layout::{dump_layout_json, dump_layout_json_fast, apply_layout, cycle_layout,
    cycle_layout_reverse};
use crate::window_ops::{toggle_zoom, remote_mouse_down, remote_mouse_drag, remote_mouse_up,
    remote_mouse_button, remote_mouse_motion, remote_scroll_up, remote_scroll_down,
    swap_pane, break_pane_to_window, unzoom_if_zoomed, resize_pane_vertical,
    resize_pane_horizontal, resize_pane_absolute, rotate_panes, respawn_active_pane,
    handle_pane_mouse, handle_pane_scroll, handle_split_set_sizes, handle_split_resize_done};
use crate::config::{load_config, parse_key_string, format_key_binding, normalize_key_for_binding,
    parse_config_content};
use crate::commands::{parse_command_to_action, format_action, parse_menu_definition, execute_command_string};
use crate::util::{list_windows_json, list_tree_json, list_windows_tmux, base64_encode};
use crate::control;
use crate::format::{expand_format, format_list_windows, format_list_panes, set_buffer_idx_override, set_named_buffer_override};
use crate::help;

/// Build a JSON fragment with overlay state (popup, menu, confirm, display_panes).
/// Delegates popup-specific serialization to the popup module.
fn serialize_overlay_json(app: &AppState) -> String {
    use crate::server::helpers::json_escape_string;

    // Popup overlay handles PopupMode, MenuMode, ConfirmMode, PaneChooser, and default
    let mut out = crate::popup::serialize_popup_overlay(app);

    // Include status_message for display-message without -p (#110)
    if let Some((ref msg, since, per_msg_duration)) = app.status_message {
        let elapsed = since.elapsed().as_millis() as u64;
        let display_time = per_msg_duration.unwrap_or(app.display_time_ms);
        if elapsed < display_time {
            out.push_str(",\"status_message\":\"");
            out.push_str(&json_escape_string(msg));
            out.push('"');
        }
    }
    out
}

fn should_spawn_warm_server(app: &AppState) -> bool {
    app.warm_enabled && app.session_name != "__warm__" && !app.destroy_unattached
}

/// Check if the active pane is currently squelched (hiding injected cd+cls).
/// Uses the non-consuming `squelch_cleared()` so the layout serialiser can
/// still properly consume the sentinel via `take_squelch_cleared()`.
fn is_active_pane_squelched(app: &AppState) -> bool {
    if app.windows.is_empty() { return false; }
    let win = &app.windows[app.active_idx];
    if let Some(p) = active_pane(&win.root, &win.active_path) {
        if let Some(deadline) = p.squelch_until {
            let sentinel = p.term.lock()
                .map(|parser| parser.screen().squelch_cleared())
                .unwrap_or(false);
            !sentinel && Instant::now() < deadline
        } else { false }
    } else { false }
}

/// Spawn a standby "warm server" process that pre-loads config + shell.
/// When `psmux new-session` is run later, the CLI claims this warm server
/// via `claim-session` instead of cold-spawning, making session creation
/// nearly instant.  The warm server uses session name `__warm__`.
fn spawn_warm_server(app: &AppState) {
    // destroy-unattached means the user expects the session to be torn down
    // when the last client leaves; keeping a hidden warm server alive breaks
    // that expectation and makes exit-empty appear ineffective.
    if !should_spawn_warm_server(app) {
        return;
    }
    // Skip if a warm server already exists
    let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
    let warm_base = if let Some(ref sn) = app.socket_name {
        format!("{}____warm__", sn)
    } else {
        "__warm__".to_string()
    };
    let warm_port_path = format!("{}\\.psmux\\{}.port", home, warm_base);
    if std::path::Path::new(&warm_port_path).exists() {
        // Check if it's actually alive
        if let Ok(port_str) = std::fs::read_to_string(&warm_port_path) {
            if let Ok(port) = port_str.trim().parse::<u16>() {
                let addr = format!("127.0.0.1:{}", port);
                if std::net::TcpStream::connect_timeout(
                    &addr.parse().unwrap(),
                    Duration::from_millis(100),
                ).is_ok() {
                    return; // warm server already running
                }
            }
        }
        // Stale port file — remove it (and matching key file)
        let _ = std::fs::remove_file(&warm_port_path);
        let warm_key_path = format!("{}\\.psmux\\{}.key", home, warm_base);
        let _ = std::fs::remove_file(&warm_key_path);
    }
    let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("psmux"));
    let mut args: Vec<String> = vec!["server".into(), "-s".into(), "__warm__".into()];
    if let Some(ref sn) = app.socket_name {
        args.push("-L".into());
        args.push(sn.clone());
    }
    // Pass current terminal dimensions so the warm server's first window
    // and warm pane are spawned at the right size.
    let area = app.last_window_area;
    if area.width > 1 && area.height > 1 {
        args.push("-x".into());
        args.push(area.width.to_string());
        args.push("-y".into());
        args.push(area.height.to_string());
    }
    #[cfg(windows)]
    { let _ = crate::platform::spawn_server_hidden(&exe, &args); }
    #[cfg(not(windows))]
    {
        let mut cmd = std::process::Command::new(&exe);
        for a in &args { cmd.arg(a); }
        cmd.stdin(std::process::Stdio::null());
        cmd.stdout(std::process::Stdio::null());
        cmd.stderr(std::process::Stdio::null());
        let _ = cmd.spawn();
    }
}

/// Parse a popup dimension spec: "80" (absolute) or "95%" (percentage of term_dim).
fn parse_popup_dim(spec: &str, term_dim: u16, default: u16) -> u16 {
    if let Some(pct_str) = spec.strip_suffix('%') {
        if let Ok(pct) = pct_str.parse::<u16>() {
            let pct = pct.min(100);
            (term_dim as u32 * pct as u32 / 100) as u16
        } else {
            default
        }
    } else {
        spec.parse().unwrap_or(default)
    }
}

/// Compute the effective display size from all connected clients' terminal sizes.
/// Returns None if no clients have reported sizes.
fn compute_effective_client_size(app: &AppState) -> Option<(u16, u16)> {
    if app.client_sizes.is_empty() { return None; }
    match app.window_size.as_str() {
        "smallest" => Some((
            app.client_sizes.values().map(|s| s.0).min().unwrap(),
            app.client_sizes.values().map(|s| s.1).min().unwrap(),
        )),
        "largest" => Some((
            app.client_sizes.values().map(|s| s.0).max().unwrap(),
            app.client_sizes.values().map(|s| s.1).max().unwrap(),
        )),
        _ => {
            // "latest" — use latest client's size, fall back to smallest
            if let Some(cid) = app.latest_client_id {
                if let Some(&size) = app.client_sizes.get(&cid) {
                    return Some(size);
                }
            }
            Some((
                app.client_sizes.values().map(|s| s.0).min().unwrap(),
                app.client_sizes.values().map(|s| s.1).min().unwrap(),
            ))
        }
    }
}

/// Process a single CtrlReq during the post-config plugin drain loop.
/// Handles the subset of requests that plugin scripts send (set, show, bind,
/// source-file) and silently drops others.
fn drain_plugin_req(
    app: &mut AppState,
    req: CtrlReq,
    shared_aliases: &std::sync::Arc<std::sync::RwLock<std::collections::HashMap<String, String>>>,
) {
    match req {
        CtrlReq::SetOption(option, value) => {
            apply_set_option(app, &option, &value, false);
            app.user_set_options.insert(option.clone());
            if option == "command-alias" {
                if let Ok(mut map) = shared_aliases.write() {
                    *map = app.command_aliases.clone();
                }
            }
        }
        CtrlReq::SetOptionQuiet(option, value, quiet) => {
            apply_set_option(app, &option, &value, quiet);
            app.user_set_options.insert(option.clone());
            if option == "command-alias" {
                if let Ok(mut map) = shared_aliases.write() {
                    *map = app.command_aliases.clone();
                }
            }
        }
        CtrlReq::SetOptionAppend(option, value) => {
            if option.starts_with('@') {
                let existing = app.user_options.get(&option).cloned().unwrap_or_default();
                app.user_options.insert(option, format!("{}{}", existing, value));
            } else {
                match option.as_str() {
                    "status-left" => app.status_left.push_str(&value),
                    "status-right" => app.status_right.push_str(&value),
                    "status-style" => app.status_style.push_str(&value),
                    _ => {}
                }
            }
        }
        CtrlReq::SetOptionUnset(option) => {
            if option.starts_with('@') {
                app.user_options.remove(&option);
            }
        }
        CtrlReq::SetOptionOnlyIfUnset(option, value) => {
            // Only set if the option hasn't been explicitly set by user/config.
            // For @-prefixed user options, check if the key exists.
            // For built-in options, check the user_set_options tracker.
            let already_set = if option.starts_with('@') {
                app.user_options.contains_key(&option)
            } else {
                app.user_set_options.contains(&option)
            };
            if !already_set {
                apply_set_option(app, &option, &value, false);
                app.user_set_options.insert(option.clone());
                if option == "command-alias" {
                    if let Ok(mut map) = shared_aliases.write() {
                        *map = app.command_aliases.clone();
                    }
                }
            }
        }
        CtrlReq::ShowOptionValue(resp, name) => {
            let val = get_option_value(app, &name);
            let _ = resp.send(val);
        }
        CtrlReq::ShowWindowOptionValue(resp, name) => {
            let val = get_window_option_value(app, &name);
            let _ = resp.send(val);
        }
        CtrlReq::ShowOptions(resp) => {
            // Minimal: just send empty to unblock the caller
            let _ = resp.send(String::new());
        }
        CtrlReq::ShowWindowOptions(resp) => {
            let _ = resp.send(render_window_options(app));
        }
        CtrlReq::BindKey(table_name, key, command, repeat) => {
            if let Some(kc) = parse_key_string(&key) {
                let kc = normalize_key_for_binding(kc);
                let sub_cmds = crate::config::split_chained_commands_pub(&command);
                let action = if sub_cmds.len() > 1 {
                    Some(Action::CommandChain(sub_cmds))
                } else {
                    parse_command_to_action(&command)
                };
                if let Some(act) = action {
                    let table = app.key_tables.entry(table_name).or_default();
                    table.retain(|b| b.key != kc);
                    table.push(Bind { key: kc, action: act, repeat });
                }
            }
        }
        CtrlReq::SourceFile(path) => {
            app.defaults_suppressed = false;
            app.key_tables.clear();
            crate::config::populate_default_bindings(app);
            crate::config::source_file(app, &path);
        }
        CtrlReq::UnbindAll => {
            app.key_tables.clear();
            app.defaults_suppressed = true;
        }
        CtrlReq::UnbindAllInTable(table) => {
            if let Some(binds) = app.key_tables.get_mut(&table) {
                binds.clear();
            }
        }
        CtrlReq::UnbindKey(key, table) => {
            if let Some(kc) = parse_key_string(&key) {
                let kc = normalize_key_for_binding(kc);
                let target = table.unwrap_or_else(|| "prefix".to_string());
                if let Some(binds) = app.key_tables.get_mut(&target) {
                    binds.retain(|b| b.key != kc);
                }
            }
        }
        // Ignore other request types during plugin drain
        _ => {}
    }
}

pub fn run_server(session_name: String, socket_name: Option<String>, initial_command: Option<String>, raw_command: Option<Vec<String>>, start_dir: Option<String>, window_name: Option<String>, init_size: Option<(u16, u16)>, group_target: Option<String>, env_vars: Vec<(String, String)>) -> io::Result<()> {
    // Write crash info to a log file when stderr is unavailable (detached server)
    // and clean up port/key files so stale entries do not linger (issue #204).
    let panic_session_name = session_name.clone();
    let panic_socket_name = socket_name.clone();
    std::panic::set_hook(Box::new(move |info| {
        let home = std::env::var("USERPROFILE").or_else(|_| std::env::var("HOME")).unwrap_or_default();
        let path = format!("{}\\.psmux\\crash.log", home);
        let bt = std::backtrace::Backtrace::force_capture();
        let _ = std::fs::write(&path, format!("{info}\n\nBacktrace:\n{bt}"));
        // Remove port/key files to prevent stale entries after a panic
        let base = if let Some(ref sn) = panic_socket_name {
            format!("{}__{}", sn, panic_session_name)
        } else {
            panic_session_name.clone()
        };
        let _ = std::fs::remove_file(format!("{}\\.psmux\\{}.port", home, base));
        let _ = std::fs::remove_file(format!("{}\\.psmux\\{}.key", home, base));
    }));
    // Install console control handler to prevent termination on client detach
    install_console_ctrl_handler();

    let pty_system = native_pty_system();

    let mut app = AppState::new(session_name);
    app.socket_name = socket_name;
    app.session_group = group_target;
    // Server starts detached with a reasonable default window size
    app.attached_clients = 0;

    // Bind the control listener BEFORE loading config so that run-shell
    // commands spawned by load_config can connect back to the server.
    let (tx, rx) = mpsc::channel::<CtrlReq>();
    app.control_rx = Some(rx);
    let listener = TcpListener::bind(("127.0.0.1", 0))?;
    let port = listener.local_addr()?.port();
    app.control_port = Some(port);

    // Write port and key files IMMEDIATELY after binding, BEFORE loading
    // config or creating windows.  run-shell scripts (e.g. PPM) need the
    // port file to discover the server, and the client polls for it to know
    // the server is ready.
    let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
    let dir = format!("{}\\.psmux", home);
    let _ = std::fs::create_dir_all(&dir);

    // Generate a random session key for security
    let session_key: String = {
        use std::collections::hash_map::RandomState;
        use std::hash::{BuildHasher, Hasher};
        let s = RandomState::new();
        let mut h = s.build_hasher();
        h.write_u64(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos() as u64);
        h.write_u64(std::process::id() as u64);
        format!("{:016x}", h.finish())
    };

    app.session_key = session_key.clone();

    let regpath = format!("{}\\{}.port", dir, app.port_file_base());
    let _ = std::fs::write(&regpath, port.to_string());
    let keypath = format!("{}\\{}.key", dir, app.port_file_base());
    let _ = std::fs::write(&keypath, &session_key);

    // Expose the server identity via env var so that child processes spawned
    // by run-shell (from hooks, keybindings, etc.) can find this server when
    // they call `psmux set -g ...` or other CLI commands.
    env::set_var("PSMUX_TARGET_SESSION", app.port_file_base());

    // Try to set file permissions to user-only (Windows)
    #[cfg(windows)]
    {
        // Recreate key file with restricted permissions
        let _ = std::fs::OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .open(&keypath)
            .map(|mut f| std::io::Write::write_all(&mut f, session_key.as_bytes()));
    }

    // Start accept thread BEFORE load_config so that run-shell commands
    // (e.g. PPM plugin manager) spawned during config parsing can connect
    // to the server.  Without this, run-shell scripts fail silently because
    // there is no TCP listener accepting connections yet.
    // Initialize shared aliases empty — will be populated after load_config.
    let shared_aliases: std::sync::Arc<std::sync::RwLock<std::collections::HashMap<String, String>>> =
        std::sync::Arc::new(std::sync::RwLock::new(std::collections::HashMap::new()));
    let shared_aliases_main = shared_aliases.clone();

    thread::spawn(move || {
        for conn in listener.incoming() {
            if let Ok(stream) = conn {
                let tx = tx.clone();
                let session_key_clone = session_key.clone();
                let aliases = shared_aliases.clone();
                thread::spawn(move || {
                    connection::handle_connection(stream, tx, &session_key_clone, aliases);
                }); // end per-connection thread
            }
        }
    });

    // Load config AFTER the TCP listener is bound, port/key files are written,
    // and the accept thread is running.  This ensures that run-shell commands
    // in the config (e.g. `run '~/.psmux/plugins/ppm/ppm.ps1'`) can connect
    // back to the server to apply settings.

    // Apply initial dimensions BEFORE warm pane spawn so spawn_warm_pane()
    // uses the correct terminal size.
    if let Some((w, h)) = init_size {
        app.last_window_area = ratatui::layout::Rect { x: 0, y: 0, width: w, height: h };
    }

    // Apply -e environment variables BEFORE pane spawn so the first pane
    // inherits them via apply_user_environment().
    crate::util::merge_session_env_into_app(&mut app, &env_vars);

    // Pre-spawn a warm pane BEFORE loading config: the shell (pwsh) starts
    // loading immediately and runs in parallel with config parsing / plugin
    // initialization.  By the time create_window() consumes it, the shell
    // has had the full config-load duration (~100-500ms) as a head start.
    // Only when using default shell (no custom command).
    // For detached sessions without -x/-y, last_window_area defaults to
    // 120x30 which is fine for the warm pane (resized later on first attach).
    let early_warm = if initial_command.is_none() && raw_command.is_none() && start_dir.is_none() {
        match spawn_warm_pane(&*pty_system, &mut app) {
            Ok(wp) => Some(wp),
            Err(_) => None,
        }
    } else { None };

    crate::config::populate_default_bindings(&mut app);
    load_config(&mut app);

    // Execute queued plugin .ps1 scripts (e.g. theme plugins that use
    // PowerShell variables and call back to psmux via CLI).  We spawn
    // them async and then drain the CtrlReq channel in a mini-loop so
    // show-options / set requests from the scripts are handled before
    // the main UI starts.
    if !app.pending_plugin_scripts.is_empty() {
        let scripts: Vec<String> = app.pending_plugin_scripts.drain(..).collect();
        let target_session = app.port_file_base();
        let mut children: Vec<std::process::Child> = Vec::new();
        for ps1 in &scripts {
            // Resolve shell: pwsh (PS7) preferred, fall back to powershell.exe (Windows PS)
            let shell = if which::which("pwsh").is_ok() { "pwsh" } else { "powershell" };
            let mut cmd = std::process::Command::new(shell);
            cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ps1]);
            if !target_session.is_empty() {
                cmd.env("PSMUX_TARGET_SESSION", &target_session);
            }
            cmd.stdout(std::process::Stdio::null());
            cmd.stderr(std::process::Stdio::null());
            { use crate::platform::HideWindowCommandExt; cmd.hide_window(); }
            if let Ok(child) = cmd.spawn() {
                children.push(child);
            }
        }

        // Drain CtrlReq messages until all scripts finish (max 5s).
        if !children.is_empty() {
            let deadline = Instant::now() + Duration::from_secs(5);
            // Temporarily take rx out of app to avoid borrow conflict
            if let Some(rx) = app.control_rx.take() {
                loop {
                    let all_done = children.iter_mut().all(|c| {
                        matches!(c.try_wait(), Ok(Some(_)))
                    });
                    let remaining = deadline.saturating_duration_since(Instant::now());
                    if all_done || remaining.is_zero() {
                        while let Ok(req) = rx.try_recv() {
                            drain_plugin_req(&mut app, req, &shared_aliases_main);
                        }
                        break;
                    }
                    match rx.recv_timeout(Duration::from_millis(50).min(remaining)) {
                        Ok(req) => drain_plugin_req(&mut app, req, &shared_aliases_main),
                        Err(mpsc::RecvTimeoutError::Timeout) => {}
                        Err(_) => break,
                    }
                }
                app.control_rx = Some(rx);
            }
        }
    }

    // If the user configured a custom default-shell in their config, the
    // early warm pane has the wrong shell — kill it so create_window falls
    // through to a cold spawn with the correct shell.
    if let Some(wp) = early_warm {
        if !app.warm_enabled {
            // Config disabled warm panes — kill the early pre-spawned pane.
            let mut wp = wp;
            wp.child.kill().ok();
        } else if app.default_shell.is_empty() {
            // No custom shell — the pre-spawned default (pwsh) is correct.
            // Check if config loaded env vars that the early warm pane is missing.
            let needs_env = app.environment.iter().any(|(k, _)| {
                !k.starts_with("PSMUX_TARGET_SESSION") && k != "TMUX" && k != "TMUX_PANE"
            });
            // The early warm pane was spawned before load_config with
            // allow_predictions=false (the default).  If the config set
            // allow-predictions on, the warm pane has the wrong PSReadLine
            // init string (PSRL_FIX instead of PSRL_CRASH_GUARD), so it
            // must be respawned (#165).
            let needs_predictions_fix = app.allow_predictions;
            if needs_env || needs_predictions_fix {
                // The early warm pane was spawned before config, so it lacks
                // config-defined env vars (e.g. TERM from default-terminal)
                // or has the wrong PSReadLine prediction init (#165).
                // Kill it and respawn with the correct settings.
                let mut wp = wp;
                wp.child.kill().ok();
                match spawn_warm_pane(&*pty_system, &mut app) {
                    Ok(new_wp) => { app.warm_pane = Some(new_wp); }
                    Err(e) => { eprintln!("psmux: warm pane respawn failed: {e}"); }
                }
            } else {
                app.warm_pane = Some(wp);
            }
        } else {
            // Custom shell set by config — wrong warm pane, kill it
            let mut wp = wp;
            wp.child.kill().ok();
        }
    }

    // Update shared aliases now that config has been loaded
    if let Ok(mut w) = shared_aliases_main.write() {
        *w = app.command_aliases.clone();
    }

    // Create initial window — if a warm pane was pre-spawned above,
    // create_window's fast path transplants it instantly.
    let saved_dir = if start_dir.is_some() { env::current_dir().ok() } else { None };
    if let Some(ref dir) = start_dir { env::set_current_dir(dir).ok(); }
    let create_result = if let Some(ref raw_args) = raw_command {
        create_window_raw(&*pty_system, &mut app, raw_args)
    } else {
        create_window(&*pty_system, &mut app, initial_command.as_deref(), None)
    };
    if let Err(e) = create_result {
        // Clean up port and key files so stale entries are not left
        // behind when the pane command fails to spawn (issue #204).
        let _ = std::fs::remove_file(&regpath);
        let _ = std::fs::remove_file(&keypath);
        // Kill warm pane if one was pre-spawned
        if let Some(mut wp) = app.warm_pane.take() { wp.child.kill().ok(); }
        return Err(e);
    }
    if let Some(prev) = saved_dir { env::set_current_dir(prev).ok(); }
    // Apply window name if specified via -n
    if let Some(n) = window_name { app.windows.last_mut().map(|w| w.name = n); }
    // Replenish: spawn a warm pane for the NEXT new-window / split.
    // Always replenish when no warm pane is available.
    if app.warm_pane.is_none() {
        match spawn_warm_pane(&*pty_system, &mut app) {
            Ok(wp) => { app.warm_pane = Some(wp); }
            Err(e) => { eprintln!("psmux: warm pane pre-spawn failed: {e}"); }
        }
    }
    // Fire client-attached hooks once at startup so plugins populate initial
    // data (e.g. CPU/battery) even for detached sessions (tppanel previews).
    crate::commands::fire_hooks(&mut app, "client-attached");
    // Fire session-created hook at startup
    crate::commands::fire_hooks(&mut app, "session-created");
    // Spawn a warm server for the NEXT new-session when the current session
    // is allowed to keep background state alive.
    if should_spawn_warm_server(&app) {
        spawn_warm_server(&app);
    }
    let mut state_dirty = true;
    let mut cached_dump_state = String::new();
    let mut cached_data_version: u64 = 0;
    // Cached metadata JSON — windows/tree/prefix change only on structural
    // mutations, so we rebuild them lazily via `meta_dirty`.
    let mut meta_dirty = true;
    let mut cached_windows_json = String::new();
    let mut cached_tree_json = String::new();
    let mut cached_prefix_str = String::new();
    let mut cached_prefix2_str = String::new();
    let mut cached_base_index: usize = 0;
    let mut cached_pred_dim: bool = false;
    let mut cached_status_style = String::new();
    let mut cached_bindings_json = String::from("[]");
    // Reusable buffer for building the combined JSON envelope.
    let mut combined_buf = String::with_capacity(32768);


    // Track when we recently sent keystrokes to the PTY.  While waiting
    // for the echo to appear we use a much shorter recv_timeout (1ms vs 5ms)
    // so that dump-state requests are served with minimal delay.  This is
    // critical for nested-shell latency (e.g. WSL inside pwsh) where the
    // echo path goes through ConPTY → pwsh → WSL → echo → ConPTY and can
    // take 10-30ms.  Without this, each "no-change" polling cycle costs up
    // to 5ms, adding cumulative latency visible as heavy input lag.
    let mut echo_pending_until: Option<Instant> = None;

    // Track when any client last requested a dump or sent input.
    // Used to ramp down the server loop frequency when truly idle.
    let mut last_client_activity = Instant::now();

    // Throttle reap_children: only check for exited processes every 250ms.
    // With hundreds of windows, calling try_wait() on every process each
    // loop iteration wastes CPU.  Exited processes are still reaped promptly
    // (250ms is imperceptible to users).
    let mut last_reap = Instant::now();

    // Persist temp_focus_restore across batch boundaries so that a
    // FocusWindowTemp/FocusPaneByIndexTemp in one batch plus the actual
    // command (e.g. CapturePane) in the next batch still works correctly.
    let mut temp_focus_restore: Option<(usize, usize)> = None;

    loop {
        // Adaptive timeout: ramps from 1ms (active typing/echo) through
        // 5ms (client recently active) up to 50ms (fully idle).  This
        // dramatically reduces CPU usage when the session is idle while
        // keeping responsiveness high during interaction.
        let data_ready = crate::types::PTY_DATA_READY.swap(false, std::sync::atomic::Ordering::AcqRel);
        if data_ready {
            state_dirty = true;
            // Drain output ring buffers and send %output notifications to control clients
            if !app.control_clients.is_empty() {
                // Collect output from all panes first, then dispatch to clients
                let mut pane_outputs: Vec<(usize, String)> = Vec::new();
                for win in &app.windows {
                    crate::tree::for_each_pane(&win.root, &mut |pane: &crate::types::Pane| {
                        if let Ok(mut ring) = pane.output_ring.lock() {
                            if !ring.is_empty() {
                                let bytes: Vec<u8> = ring.drain(..).collect();
                                let data = String::from_utf8_lossy(&bytes).to_string();
                                pane_outputs.push((pane.id, data));
                            }
                        }
                    });
                }
                // Dispatch to each control client with pause-after logic
                let now = std::time::Instant::now();
                for (pane_id, data) in &pane_outputs {
                    for client in app.control_clients.values_mut() {
                        if client.paused_panes.contains(pane_id) {
                            continue;
                        }
                        if client.output_paused_panes.contains(pane_id) {
                            // Pane is paused for this client; drop output
                            continue;
                        }
                        if let Some(pause_secs) = client.pause_after_secs {
                            // Track output timing per pane
                            let last = client.pane_last_output.entry(*pane_id).or_insert(now);
                            let age = now.duration_since(*last);
                            *last = now;
                            if age.as_secs() >= pause_secs {
                                // Client fell behind: pause this pane
                                client.output_paused_panes.insert(*pane_id);
                                let _ = client.notification_tx.try_send(
                                    crate::types::ControlNotification::Pause { pane_id: *pane_id }
                                );
                                continue;
                            }
                            // Send as extended-output with age
                            let age_ms = age.as_millis() as u64;
                            let _ = client.notification_tx.try_send(
                                crate::types::ControlNotification::ExtendedOutput {
                                    pane_id: *pane_id,
                                    age_ms,
                                    data: data.clone(),
                                }
                            );
                        } else {
                            // No pause-after: send normal %output
                            let _ = client.notification_tx.try_send(
                                crate::types::ControlNotification::Output {
                                    pane_id: *pane_id,
                                    data: data.clone(),
                                }
                            );
                        }
                    }
                }
            }
        }
        // When a popup PTY is active, always push frames so interactive
        // content (e.g. fzf, shell prompts) updates in real-time.
        if matches!(app.mode, Mode::PopupMode { .. }) {
            state_dirty = true;
        }
        let echo_active = echo_pending_until.map_or(false, |t| t.elapsed().as_millis() < 50);
        let idle_secs = last_client_activity.elapsed().as_secs();
        let timeout_ms: u64 = if echo_active || data_ready {
            1      // Active echo/data: 1ms for maximum responsiveness
        } else if idle_secs < 2 {
            5      // Recently active: 5ms (200 Hz)
        } else if crate::types::has_frame_receivers() {
            16     // Push clients attached: 16ms (~60 Hz) so PTY data
                   // is detected and pushed within one vsync period.
        } else {
            50     // No clients: 50ms (20 Hz) — saves CPU
        };
        if let Some(rx) = app.control_rx.as_ref() {
            if let Ok(req) = rx.recv_timeout(Duration::from_millis(timeout_ms)) {
                last_client_activity = Instant::now();
                let mut pending = vec![req];
                // Drain any additional queued messages without blocking
                while let Ok(r) = rx.try_recv() {
                    pending.push(r);
                }
                // Also check if fresh PTY output arrived while we were
                // waiting – mark state dirty so DumpState produces a full
                // frame instead of "NC".
                if crate::types::PTY_DATA_READY.swap(false, std::sync::atomic::Ordering::AcqRel) {
                    state_dirty = true;
                }
                // Process key/command inputs BEFORE dump-state requests.
                // This ensures ConPTY receives keystrokes before we serialize
                // the screen, reducing stale-frame responses.
                pending.sort_by_key(|r| match r {
                    CtrlReq::DumpState(..) => 1,
                    CtrlReq::DumpLayout(_) => 1,
                    CtrlReq::WindowDump(..) => 1,
                    _ => 0,
                });
                // Track temporary -t focus: save (active_idx, pane_id) when
                // FocusWindowTemp/FocusPaneTemp is seen, restore after next
                // non-temp command so the user's view doesn't jump.
                // We store the pane ID (not path) because kill-pane
                // restructures the tree, invalidating saved paths (#71).
                // NOTE: temp_focus_restore lives outside the loop so it
                // persists across batch boundaries (prevents race where
                // FocusWindowTemp and the actual command land in different
                // batches).
                for req in pending {
                    let mutates_state = !matches!(&req,
                        CtrlReq::DumpState(..)
                        | CtrlReq::SendText(_)
                        | CtrlReq::SendKey(_)
                        | CtrlReq::SendPaste(_)
                        | CtrlReq::WindowDump(..)
                        | CtrlReq::WindowLayout(..)
                    );
                    let is_temp_focus = matches!(&req,
                        CtrlReq::FocusWindowTemp(_) | CtrlReq::FocusWindowByIdTemp(_) | CtrlReq::FocusWindowByNameTemp(_) | CtrlReq::FocusPaneTemp(_) | CtrlReq::FocusPaneByIndexTemp(_));
                    let mut hook_event: Option<&str> = None;
                    // Track active_idx changes for debugging window-switch issues
                    let _prev_active_idx = app.active_idx;
                    let _req_tag: &str = match &req {
                        CtrlReq::NextWindow => "NextWindow",
                        CtrlReq::PrevWindow => "PrevWindow",
                        CtrlReq::SelectWindow(_) => "SelectWindow",
                        CtrlReq::FocusWindow(_) => "FocusWindow",
                        CtrlReq::FocusWindowById(_) => "FocusWindowById",
                        CtrlReq::FocusWindowByName(_) => "FocusWindowByName",
                        CtrlReq::FocusWindowTemp(_) => "FocusWindowTemp",
                        CtrlReq::FocusWindowByIdTemp(_) => "FocusWindowByIdTemp",
                        CtrlReq::FocusWindowByNameTemp(_) => "FocusWindowByNameTemp",
                        CtrlReq::FocusWindowCmd(_) => "FocusWindowCmd",
                        CtrlReq::LastWindow => "LastWindow",
                        CtrlReq::MouseDown(..) => "MouseDown",
                        CtrlReq::MouseDownRight(..) => "MouseDownRight",
                        CtrlReq::MouseDownMiddle(..) => "MouseDownMiddle",
                        CtrlReq::FocusPane(_) => "FocusPane",
                        CtrlReq::FocusPaneTemp(_) => "FocusPaneTemp",
                        CtrlReq::NewWindow(..) => "NewWindow",
                        CtrlReq::KillWindow => "KillWindow",
                        CtrlReq::KillPane => "KillPane",
                        CtrlReq::KillPaneById(_) => "KillPaneById",
                        CtrlReq::BreakPane => "BreakPane",
                        CtrlReq::JoinPane { .. } => "JoinPane",
                        CtrlReq::MovePane { .. } => "MovePane",
                        CtrlReq::PaneForwardExtract(..) => "PaneForwardExtract",
                        CtrlReq::PaneForwardInject { .. } => "PaneForwardInject",
                        CtrlReq::PaneForwardResize(..) => "PaneForwardResize",
                        CtrlReq::PaneForwardStatus(..) => "PaneForwardStatus",
                        CtrlReq::PaneForwardKill(..) => "PaneForwardKill",
                        CtrlReq::MoveWindow(..) => "MoveWindow",
                        CtrlReq::SwapWindow(_) => "SwapWindow",
                        _ => "",
                    };
                    match req {
                CtrlReq::NewWindow(cmd, name, detached, start_dir) => {
                    if let Some(cmds) = app.hooks.get("before-new-window") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    let prev_idx = app.active_idx;
                    // Expand format variables like #{pane_current_path} (#111)
                    let start_dir = start_dir.map(|d| expand_format(&d, &app)).filter(|d| !d.is_empty());
                    let saved_dir = if start_dir.is_some() { env::current_dir().ok() } else { None };
                    if let Some(dir) = &start_dir { env::set_current_dir(dir).ok(); }
                    // Hide the warm pane when an explicit start dir is requested
                    // so create_window spawns a fresh shell in the correct CWD.
                    let stashed_warm = if start_dir.is_some() { app.warm_pane.take() } else { None };
                    if let Err(e) = create_window(&*pty_system, &mut app, cmd.as_deref(), start_dir.as_deref()) {
                        eprintln!("psmux: new-window error: {e}");
                    }
                    if let Some(wp) = stashed_warm { app.warm_pane = Some(wp); }
                    if let Some(prev) = saved_dir { env::set_current_dir(prev).ok(); }
                    if let Some(n) = name { app.windows.last_mut().map(|w| { w.name = n; w.manual_rename = true; }); }
                    if detached { app.active_idx = prev_idx; }
                    // Replenish warm pane pool for next new-window
                    if app.warm_pane.is_none() {
                        match spawn_warm_pane(&*pty_system, &mut app) {
                            Ok(wp) => { app.warm_pane = Some(wp); }
                            Err(_) => {}
                        }
                    }
                    resize_all_panes(&mut app); meta_dirty = true; hook_event = Some("after-new-window");
                }
                CtrlReq::NewWindowPrint(cmd, name, detached, start_dir, format_str, resp) => {
                    if let Some(cmds) = app.hooks.get("before-new-window") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    let prev_idx = app.active_idx;
                    let start_dir = start_dir.map(|d| expand_format(&d, &app)).filter(|d| !d.is_empty());
                    let saved_dir = if start_dir.is_some() { env::current_dir().ok() } else { None };
                    if let Some(dir) = &start_dir { env::set_current_dir(dir).ok(); }
                    let stashed_warm = if start_dir.is_some() { app.warm_pane.take() } else { None };
                    if let Err(e) = create_window(&*pty_system, &mut app, cmd.as_deref(), start_dir.as_deref()) {
                        eprintln!("psmux: new-window error: {e}");
                    }
                    if let Some(wp) = stashed_warm { app.warm_pane = Some(wp); }
                    if let Some(prev) = saved_dir { env::set_current_dir(prev).ok(); }
                    if let Some(n) = name { app.windows.last_mut().map(|w| { w.name = n; w.manual_rename = true; }); }
                    // Use full format engine for -P output (tmux compatible)
                    let new_win_idx = app.windows.len() - 1;
                    let fmt = format_str.as_deref().unwrap_or("#{session_name}:#{window_index}");
                    let pane_info = crate::format::expand_format_for_window(fmt, &app, new_win_idx);
                    if detached { app.active_idx = prev_idx; }
                    let _ = resp.send(pane_info);
                    // Replenish warm pane pool for next new-window
                    if app.warm_pane.is_none() {
                        match spawn_warm_pane(&*pty_system, &mut app) {
                            Ok(wp) => { app.warm_pane = Some(wp); }
                            Err(_) => {}
                        }
                    }
                    resize_all_panes(&mut app); meta_dirty = true; hook_event = Some("after-new-window");
                }
                CtrlReq::SplitWindow(k, cmd, detached, start_dir, split_size, resp) => {
                    if let Some(cmds) = app.hooks.get("before-split-window") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    // tmux: split-window without -Z permanently unzooms (#82)
                    unzoom_if_zoomed(&mut app);
                    let start_dir = start_dir.map(|d| expand_format(&d, &app)).filter(|d| !d.is_empty());
                    let saved_dir = if start_dir.is_some() { env::current_dir().ok() } else { None };
                    if let Some(dir) = &start_dir { env::set_current_dir(dir).ok(); }
                    let prev_path = app.windows[app.active_idx].active_path.clone();
                    // Hide warm pane when explicit start_dir is given (wrong CWD)
                    let stashed_warm = if start_dir.is_some() { app.warm_pane.take() } else { None };
                    if let Err(e) = split_active_with_command(&mut app, k, cmd.as_deref(), Some(&*pty_system), start_dir.as_deref()) {
                        let _ = resp.send(format!("psmux: split-window: {e}"));
                    } else {
                        let _ = resp.send(String::new());
                    }
                    if let Some(wp) = stashed_warm { app.warm_pane = Some(wp); }
                    // Apply size if specified: (value, true) = percentage, (value, false) = cell count
                    if let Some((val, is_pct)) = split_size {
                        let pct = if is_pct {
                            val.clamp(1, 99)
                        } else {
                            // Convert cell count to percentage based on split direction
                            let area = app.last_window_area;
                            let total = if k == LayoutKind::Horizontal { area.width } else { area.height };
                            if total > 0 { ((val as u32 * 100) / total as u32).clamp(1, 99) as u16 } else { 50 }
                        };
                        let win = &mut app.windows[app.active_idx];
                        if let Some(Node::Split { sizes, .. }) = get_split_mut(&mut win.root, &prev_path) {
                            sizes[0] = 100 - pct;
                            sizes[1] = pct;
                        }
                    }
                    if detached {
                        // Capture new pane ID before reverting focus
                        let new_pane_id = crate::tree::get_active_pane_id(
                            &app.windows[app.active_idx].root,
                            &app.windows[app.active_idx].active_path,
                        );
                        // Revert focus to the previously active pane.
                        // After split, prev_path now points to a Split node;
                        // the original pane is child [0] of that Split.
                        let mut revert_path = prev_path;
                        revert_path.push(0);
                        app.windows[app.active_idx].active_path = revert_path;
                        // Detached splits never focus the new pane — remove
                        // from MRU entirely so directional nav tie-breaks by
                        // pane_index among equally-unvisited candidates (#70).
                        if let Some(nid) = new_pane_id {
                            let win = &mut app.windows[app.active_idx];
                            win.pane_mru.retain(|&id| id != nid);
                        }
                    } else {
                        // Non-detached: new pane keeps focus.
                        // Cancel temp_focus_restore so -t doesn't revert (#112).
                        temp_focus_restore = None;
                    }
                    if let Some(prev) = saved_dir { env::set_current_dir(prev).ok(); }
                    // Replenish warm pane for the next new-window/split
                    if app.warm_pane.is_none() {
                        match spawn_warm_pane(&*pty_system, &mut app) {
                            Ok(wp) => { app.warm_pane = Some(wp); }
                            Err(_) => {}
                        }
                    }
                    resize_all_panes(&mut app); meta_dirty = true; hook_event = Some("after-split-window");
                }
                CtrlReq::SplitWindowPrint(k, cmd, detached, start_dir, split_size, format_str, resp) => {
                    if let Some(cmds) = app.hooks.get("before-split-window") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    unzoom_if_zoomed(&mut app);
                    let start_dir = start_dir.map(|d| expand_format(&d, &app)).filter(|d| !d.is_empty());
                    let saved_dir = if start_dir.is_some() { env::current_dir().ok() } else { None };
                    if let Some(dir) = &start_dir { env::set_current_dir(dir).ok(); }
                    let prev_path = app.windows[app.active_idx].active_path.clone();
                    let stashed_warm = if start_dir.is_some() { app.warm_pane.take() } else { None };
                    if let Err(e) = split_active_with_command(&mut app, k, cmd.as_deref(), Some(&*pty_system), start_dir.as_deref()) {
                        eprintln!("psmux: split-window error: {e}");
                    }
                    if let Some(wp) = stashed_warm { app.warm_pane = Some(wp); }
                    // Apply size if specified: (value, true) = percentage, (value, false) = cell count
                    if let Some((val, is_pct)) = split_size {
                        let pct = if is_pct {
                            val.clamp(1, 99)
                        } else {
                            let area = app.last_window_area;
                            let total = if k == LayoutKind::Horizontal { area.width } else { area.height };
                            if total > 0 { ((val as u32 * 100) / total as u32).clamp(1, 99) as u16 } else { 50 }
                        };
                        let win = &mut app.windows[app.active_idx];
                        if let Some(Node::Split { sizes, .. }) = get_split_mut(&mut win.root, &prev_path) {
                            sizes[0] = 100 - pct;
                            sizes[1] = pct;
                        }
                    }
                    // Use full format engine for -P output (tmux compatible)
                    let fmt = format_str.as_deref().unwrap_or("#{session_name}:#{window_index}.#{pane_index}");
                    let pane_info = crate::format::expand_format_for_window(fmt, &app, app.active_idx);
                    if detached {
                        // Capture new pane ID before reverting focus
                        let new_pane_id = crate::tree::get_active_pane_id(
                            &app.windows[app.active_idx].root,
                            &app.windows[app.active_idx].active_path,
                        );
                        let mut revert_path = prev_path;
                        revert_path.push(0);
                        app.windows[app.active_idx].active_path = revert_path;
                        // Detached splits: remove from MRU (#70 pane_index tie-break)
                        if let Some(nid) = new_pane_id {
                            let win = &mut app.windows[app.active_idx];
                            win.pane_mru.retain(|&id| id != nid);
                        }
                    } else {
                        temp_focus_restore = None;
                    }
                    let _ = resp.send(pane_info);
                    if let Some(prev) = saved_dir { env::set_current_dir(prev).ok(); }
                    // Replenish warm pane
                    if app.warm_pane.is_none() {
                        match spawn_warm_pane(&*pty_system, &mut app) {
                            Ok(wp) => { app.warm_pane = Some(wp); }
                            Err(_) => {}
                        }
                    }
                    resize_all_panes(&mut app); meta_dirty = true; hook_event = Some("after-split-window");
                }
                CtrlReq::KillPane => {
                    if let Some(cmds) = app.hooks.get("before-kill-pane") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    unzoom_if_zoomed(&mut app); let _ = kill_active_pane(&mut app); resize_all_panes(&mut app); meta_dirty = true; hook_event = Some("after-kill-pane");
                }
                CtrlReq::KillPaneById(pid) => {
                    if let Some(cmds) = app.hooks.get("before-kill-pane") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    unzoom_if_zoomed(&mut app); let _ = kill_pane_by_id(&mut app, pid); resize_all_panes(&mut app); meta_dirty = true; hook_event = Some("after-kill-pane");
                }
                CtrlReq::CapturePane(resp) => {
                    if is_active_pane_squelched(&app) {
                        let _ = resp.send(String::new());
                    } else if let Some(text) = capture_active_pane_text(&mut app)? { let _ = resp.send(text); } else { let _ = resp.send(String::new()); }
                }
                CtrlReq::CapturePaneStyled(resp, s, e) => {
                    if is_active_pane_squelched(&app) {
                        let _ = resp.send(String::new());
                    } else if let Some(text) = capture_active_pane_styled(&mut app, s, e)? { let _ = resp.send(text); } else { let _ = resp.send(String::new()); }
                }
                CtrlReq::CapturePaneRange(resp, s, e) => {
                    if is_active_pane_squelched(&app) {
                        let _ = resp.send(String::new());
                    } else if let Some(text) = capture_active_pane_range(&mut app, s, e)? { let _ = resp.send(text); } else { let _ = resp.send(String::new()); }
                }
                CtrlReq::FocusWindow(wid) => {
                    // wid is a display index (same as tmux window number), convert to internal array index
                    if wid >= app.window_base_index {
                        let internal_idx = wid - app.window_base_index;
                        if internal_idx < app.windows.len() && internal_idx != app.active_idx {
                            switch_with_copy_save(&mut app, |app| {
                                app.last_window_idx = app.active_idx;
                                app.active_idx = internal_idx;
                            });
                            // Clear activity/bell/silence flags on the newly-focused window
                            if let Some(win) = app.windows.get_mut(internal_idx) {
                                win.activity_flag = false;
                                win.bell_flag = false;
                                win.silence_flag = false;
                            }
                            // Lazily resize panes in the newly-focused window
                            resize_all_panes(&mut app);
                        }
                    }
                    meta_dirty = true;
                    hook_event = Some("after-select-window");
                }
                CtrlReq::FocusWindowByName(ref name) => {
                    if let Some(internal_idx) = app.windows.iter().position(|w| w.name == *name) {
                        if internal_idx != app.active_idx {
                            switch_with_copy_save(&mut app, |app| {
                                app.last_window_idx = app.active_idx;
                                app.active_idx = internal_idx;
                            });
                            if let Some(win) = app.windows.get_mut(internal_idx) {
                                win.activity_flag = false;
                                win.bell_flag = false;
                                win.silence_flag = false;
                            }
                            resize_all_panes(&mut app);
                        }
                    }
                    meta_dirty = true;
                    hook_event = Some("after-select-window");
                }
                CtrlReq::FocusWindowById(id) => {
                    if let Some(internal_idx) = app.windows.iter().position(|w| w.id == id) {
                        if internal_idx != app.active_idx {
                            switch_with_copy_save(&mut app, |app| {
                                app.last_window_idx = app.active_idx;
                                app.active_idx = internal_idx;
                            });
                            if let Some(win) = app.windows.get_mut(internal_idx) {
                                win.activity_flag = false;
                                win.bell_flag = false;
                                win.silence_flag = false;
                            }
                            resize_all_panes(&mut app);
                        }
                    }
                    meta_dirty = true;
                    hook_event = Some("after-select-window");
                }
                CtrlReq::FocusPane(pid) => {
                    let old_path = app.windows[app.active_idx].active_path.clone();
                    switch_with_copy_save(&mut app, |app| { focus_pane_by_id(app, pid); });
                    if app.windows[app.active_idx].active_path != old_path { unzoom_if_zoomed(&mut app); }
                    meta_dirty = true;
                }
                CtrlReq::FocusPaneByIndex(idx) => {
                    let old_path = app.windows[app.active_idx].active_path.clone();
                    switch_with_copy_save(&mut app, |app| { focus_pane_by_index(app, idx); });
                    if app.windows[app.active_idx].active_path != old_path { unzoom_if_zoomed(&mut app); }
                    // Update MRU so directional navigation remembers this focus change
                    let win = &mut app.windows[app.active_idx];
                    if let Some(pid) = crate::tree::get_active_pane_id(&win.root, &win.active_path) {
                        crate::tree::touch_mru(&mut win.pane_mru, pid);
                    }
                    meta_dirty = true;
                }
                // ── Temporary focus variants for -t targeting ────────────
                // These switch active_idx/active_path so the NEXT command
                // in the batch operates on the correct window/pane.
                // After the entire pending batch is processed, we restore
                // the original focus (see temp_focus_restore below).
                CtrlReq::FocusWindowTemp(wid) => {
                    if temp_focus_restore.is_none() {
                        let pane_id = crate::tree::get_active_pane_id(
                            &app.windows[app.active_idx].root,
                            &app.windows[app.active_idx].active_path,
                        ).unwrap_or(usize::MAX);
                        temp_focus_restore = Some((app.active_idx, pane_id));
                    }
                    if wid >= app.window_base_index {
                        let internal_idx = wid - app.window_base_index;
                        if internal_idx < app.windows.len() {
                            app.active_idx = internal_idx;
                        }
                    }
                }
                CtrlReq::FocusWindowByNameTemp(ref name) => {
                    if temp_focus_restore.is_none() {
                        let pane_id = crate::tree::get_active_pane_id(
                            &app.windows[app.active_idx].root,
                            &app.windows[app.active_idx].active_path,
                        ).unwrap_or(usize::MAX);
                        temp_focus_restore = Some((app.active_idx, pane_id));
                    }
                    if let Some(internal_idx) = app.windows.iter().position(|w| w.name == *name) {
                        app.active_idx = internal_idx;
                    }
                }
                CtrlReq::FocusWindowByIdTemp(id) => {
                    if temp_focus_restore.is_none() {
                        let pane_id = crate::tree::get_active_pane_id(
                            &app.windows[app.active_idx].root,
                            &app.windows[app.active_idx].active_path,
                        ).unwrap_or(usize::MAX);
                        temp_focus_restore = Some((app.active_idx, pane_id));
                    }
                    if let Some(internal_idx) = app.windows.iter().position(|w| w.id == id) {
                        app.active_idx = internal_idx;
                    }
                }
                CtrlReq::FocusPaneTemp(pid) => {
                    if temp_focus_restore.is_none() {
                        let pane_id = crate::tree::get_active_pane_id(
                            &app.windows[app.active_idx].root,
                            &app.windows[app.active_idx].active_path,
                        ).unwrap_or(usize::MAX);
                        temp_focus_restore = Some((app.active_idx, pane_id));
                    }
                    // Use no-MRU variant: temporary -t targeting should not
                    // pollute the recency list (#71 — split-window -t was
                    // incorrectly touching the target pane's MRU rank).
                    focus_pane_by_id_no_mru(&mut app, pid);
                }
                CtrlReq::FocusPaneByIndexTemp(idx) => {
                    if temp_focus_restore.is_none() {
                        let pane_id = crate::tree::get_active_pane_id(
                            &app.windows[app.active_idx].root,
                            &app.windows[app.active_idx].active_path,
                        ).unwrap_or(usize::MAX);
                        temp_focus_restore = Some((app.active_idx, pane_id));
                    }
                    focus_pane_by_index(&mut app, idx);
                }
                CtrlReq::SessionInfo(resp) => {
                    let num_attached = app.client_registry.len();
                    let attached = if num_attached > 0 { " (attached)" } else { "" };
                    let group = if let Some(ref g) = app.session_group {
                        format!(" (group {})", g)
                    } else {
                        String::new()
                    };
                    let windows = app.windows.len();
                    let created = app.created_at.format("%a %b %e %H:%M:%S %Y");
                    let line = format!("{}: {} windows (created {}){}{}\n", app.session_name, windows, created, group, attached);
                    let _ = resp.send(line);
                }
                CtrlReq::SessionInfoFormat(resp, fmt) => {
                    let line = crate::format::format_list_sessions(&app, &fmt);
                    let _ = resp.send(format!("{}\n", line));
                }
                CtrlReq::ClientAttach(cid) => {
                    app.attached_clients = app.attached_clients.saturating_add(1);
                    app.latest_client_id = Some(cid);
                    // Register in client registry if not already present
                    app.client_registry.entry(cid).or_insert_with(|| {
                        let tty = format!("/dev/pts/{}", cid);
                        crate::types::ClientInfo {
                            id: cid,
                            width: app.last_window_area.width,
                            height: app.last_window_area.height,
                            connected_at: std::time::Instant::now(),
                            last_activity: std::time::Instant::now(),
                            tty_name: tty,
                            is_control: false,
                        }
                    });
                    hook_event = Some("client-attached");
                    // update-environment: refresh env vars from the attaching client's environment
                    let update_vars = app.update_environment.clone();
                    for var_spec in &update_vars {
                        let remove = var_spec.starts_with('-');
                        let name = if remove { &var_spec[1..] } else { var_spec.as_str() };
                        if remove {
                            app.environment.remove(name);
                        } else if let Ok(val) = std::env::var(name) {
                            app.environment.insert(name.to_string(), val);
                        } else {
                            app.environment.remove(name);
                        }
                    }
                }
                CtrlReq::ClientDetach(cid) => {
                    app.attached_clients = app.attached_clients.saturating_sub(1);
                    app.client_sizes.remove(&cid);
                    app.client_registry.remove(&cid);
                    app.client_prefix_active = false;
                    if app.latest_client_id == Some(cid) {
                        app.latest_client_id = None;
                    }
                    // Recompute effective size from remaining clients
                    if let Some((w, h)) = compute_effective_client_size(&app) {
                        app.last_window_area = Rect { x: 0, y: 0, width: w, height: h };
                        resize_all_panes(&mut app);
                    }
                    hook_event = Some("client-detached");
                    if app.attached_clients == 0 && app.destroy_unattached {
                        let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
                        let regpath = format!("{}\\.psmux\\{}.port", home, app.port_file_base());
                        let keypath = format!("{}\\.psmux\\{}.key", home, app.port_file_base());
                        let _ = std::fs::remove_file(&regpath);
                        let _ = std::fs::remove_file(&keypath);
                        crate::types::shutdown_persistent_streams();
                        tree::kill_all_children_batch(&mut app.windows);
                        if let Some(mut wp) = app.warm_pane.take() {
                            wp.child.kill().ok();
                        }
                        std::thread::sleep(std::time::Duration::from_millis(10));
                        std::process::exit(0);
                    }
                }
                CtrlReq::DumpLayout(resp) => {
                    let json = dump_layout_json(&mut app)?;
                    let _ = resp.send(json);
                }
                CtrlReq::DumpState(resp, allow_nc) => {
                    // ── Activity / bell / silence detection ──
                    let alert_hooks = helpers::check_window_activity(&mut app);
                    for event in &alert_hooks {
                        if let Some(cmds) = app.hooks.get(*event) { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    }

                    // ── Propagate OSC 0/2 titles to pane.title ──
                    if helpers::propagate_osc_titles(&mut app) {
                        state_dirty = true;
                    }

                    // ── Automatic rename / allow-rename: resolve window names ──
                    {
                        let in_copy = matches!(app.mode, Mode::CopyMode | Mode::CopySearch { .. });
                        let auto_rename = app.automatic_rename;
                        let allow_rename = app.allow_rename;
                        if (auto_rename || allow_rename) && !in_copy {
                            for win in app.windows.iter_mut() {
                                if win.manual_rename { continue; }
                                if let Some(p) = crate::tree::active_pane_mut(&mut win.root, &win.active_path) {
                                    if p.dead { continue; }
                                    if p.last_title_check.elapsed().as_millis() < 1000 { continue; }
                                    p.last_title_check = std::time::Instant::now();
                                    if p.child_pid.is_none() {
                                        p.child_pid = crate::platform::mouse_inject::get_child_pid(&*p.child);
                                    }
                                    let new_name = if auto_rename {
                                        // automatic-rename: use foreground process name
                                        if let Some(pid) = p.child_pid {
                                            match crate::platform::process_info::get_foreground_process_name(pid) {
                                                Some(name) => name,
                                                None => {
                                                    // No foreground child found.  Keep the current
                                                    // window name to avoid flashing to the shell
                                                    // name before a child process spawns (#229).
                                                    // Once a child appears, auto-rename will pick
                                                    // it up on the next tick.
                                                    continue;
                                                }
                                            }
                                        } else if allow_rename && !p.title.is_empty() {
                                            p.title.clone()
                                        } else {
                                            continue;
                                        }
                                    } else if allow_rename {
                                        // allow-rename only: use OSC title from child
                                        if let Ok(parser) = p.term.lock() {
                                            let title = parser.screen().title();
                                            if !title.is_empty() {
                                                title.to_string()
                                            } else {
                                                continue;
                                            }
                                        } else {
                                            continue;
                                        }
                                    } else {
                                        continue;
                                    };
                                    if !new_name.is_empty() && win.name != new_name {
                                        win.name = new_name;
                                        meta_dirty = true;
                                        state_dirty = true;
                                    }
                                }
                            }
                        }
                    }
                    // Fast-path: nothing changed at all → 2-byte "NC" marker
                    // instead of cloning 50-100KB of JSON.
                    // Only allowed for persistent connections that already have
                    // the previous frame; one-shot connections always need full state.
                    let has_squelch = app.windows.get(app.active_idx)
                        .and_then(|w| crate::tree::active_pane(&w.root, &w.active_path))
                        .map_or(false, |p| p.squelch_until.is_some());
                    if allow_nc
                        && !state_dirty
                        && !app.bell_forward
                        && !has_squelch
                        && !cached_dump_state.is_empty()
                        && cached_data_version == combined_data_version(&app)
                    {
                        let _ = resp.send("NC".to_string());
                        continue;
                    }
                    // Rebuild metadata cache if structural changes happened.
                    if meta_dirty {
                        cached_windows_json = list_windows_json_with_tabs(&app)?;
                        cached_tree_json = list_tree_json(&app)?;
                        cached_prefix_str = format_key_binding(&app.prefix_key);
                        cached_prefix2_str = app.prefix2_key.as_ref().map(|k| format_key_binding(k)).unwrap_or_default();
                        cached_base_index = app.window_base_index;
                        cached_pred_dim = app.prediction_dimming;
                        cached_status_style = app.status_style.clone();
                        cached_bindings_json = serialize_bindings_json(&app);
                        meta_dirty = false;
                    }
                    let _t_layout = std::time::Instant::now();
                    let layout_json = dump_layout_json_fast(&mut app)?;
                    let _layout_ms = _t_layout.elapsed().as_micros();
                    combined_buf.clear();
                    let ss_escaped = json_escape_string(&cached_status_style);
                    let sl_expanded = json_escape_string(&expand_format(&app.status_left, &app));
                    let sr_expanded = json_escape_string(&expand_format(&app.status_right, &app));
                    let pbs_escaped = json_escape_string(&app.pane_border_style);
                    let pabs_escaped = json_escape_string(&app.pane_active_border_style);
                    let pbhs_escaped = json_escape_string(&app.pane_border_hover_style);
                    let wsf_escaped = json_escape_string(&app.window_status_format);
                    let wscf_escaped = json_escape_string(&app.window_status_current_format);
                    let wss_escaped = json_escape_string(&app.window_status_separator);
                    let ws_style_escaped = json_escape_string(&app.window_status_style);
                    let wsc_style_escaped = json_escape_string(&app.window_status_current_style);
                    let mode_style_escaped = json_escape_string(&app.mode_style);
                    let status_position_escaped = json_escape_string(&app.status_position);
                    let status_justify_escaped = json_escape_string(&app.status_justify);
                    // Build status_format JSON array for multi-line status bar
                    let status_format_json = {
                        let mut sf = String::from("[");
                        for (i, fmt_str) in app.status_format.iter().enumerate() {
                            if i > 0 { sf.push(','); }
                            sf.push('"');
                            sf.push_str(&json_escape_string(&expand_format(fmt_str, &app)));
                            sf.push('"');
                        }
                        sf.push(']');
                        sf
                    };
                    let cursor_style_code = crate::rendering::configured_cursor_code();
                    let _ = std::fmt::Write::write_fmt(&mut combined_buf, format_args!(
                        "{{\"layout\":{},\"windows\":{},\"prefix\":\"{}\",\"prefix2\":\"{}\",\"tree\":{},\"base_index\":{},\"pane_base_index\":{},\"prediction_dimming\":{},\"status_style\":\"{}\",\"status_left\":\"{}\",\"status_right\":\"{}\",\"pane_border_style\":\"{}\",\"pane_active_border_style\":\"{}\",\"pane_border_hover_style\":\"{}\",\"wsf\":\"{}\",\"wscf\":\"{}\",\"wss\":\"{}\",\"ws_style\":\"{}\",\"wsc_style\":\"{}\",\"clock_mode\":{},\"bindings\":{},\"status_left_length\":{},\"status_right_length\":{},\"status_lines\":{},\"status_format\":{},\"mode_style\":\"{}\",\"status_position\":\"{}\",\"status_justify\":\"{}\",\"cursor_style_code\":{},\"status_visible\":{},\"repeat_time\":{},\"zoomed\":{},\"defaults_suppressed\":{},\"pwsh_mouse_selection\":{},\"mouse_selection\":{},\"paste_detection\":{},\"choose_tree_preview\":{}}}",
                        layout_json, cached_windows_json, cached_prefix_str, cached_prefix2_str, cached_tree_json, cached_base_index, app.pane_base_index, cached_pred_dim, ss_escaped, sl_expanded, sr_expanded, pbs_escaped, pabs_escaped, pbhs_escaped, wsf_escaped, wscf_escaped, wss_escaped, ws_style_escaped, wsc_style_escaped,
                        matches!(app.mode, Mode::ClockMode), cached_bindings_json,
                        app.status_left_length, app.status_right_length, app.status_lines, status_format_json,
                        mode_style_escaped, status_position_escaped, status_justify_escaped,
                        cursor_style_code, app.status_visible, app.repeat_time_ms,
                        app.windows.get(app.active_idx).map_or(false, |w| w.zoom_saved.is_some()),
                        app.defaults_suppressed,
                        app.pwsh_mouse_selection,
                        app.mouse_selection,
                        app.paste_detection,
                        app.choose_tree_preview,
                    ));
                    // Inject overlay state (popup, menu, confirm, display_panes)
                    {
                        // Inject clock_colour if set
                        if let Some(cc) = app.user_options.get("clock-mode-colour") {
                            if combined_buf.ends_with('}') {
                                combined_buf.pop();
                                combined_buf.push_str(",\"clock_colour\":\"");
                                combined_buf.push_str(&json_escape_string(cc));
                                combined_buf.push_str("\"}");
                            }
                        }
                        // Inject pane-border-status and pane-border-format
                        if let Some(pbs) = app.user_options.get("pane-border-status") {
                            if combined_buf.ends_with('}') {
                                combined_buf.pop();
                                combined_buf.push_str(",\"pane_border_status\":\"");
                                combined_buf.push_str(&json_escape_string(pbs));
                                combined_buf.push('"');
                                if let Some(pbf) = app.user_options.get("pane-border-format") {
                                    combined_buf.push_str(",\"pane_border_format\":\"");
                                    combined_buf.push_str(&json_escape_string(pbf));
                                    combined_buf.push('"');
                                }
                                combined_buf.push('}');
                            }
                        }
                        let overlay_json = serialize_overlay_json(&app);
                        if !overlay_json.is_empty() && combined_buf.ends_with('}') {
                            combined_buf.pop();
                            combined_buf.push_str(&overlay_json);
                            combined_buf.push('}');
                        }
                    }
                    cached_dump_state.clear();
                    cached_dump_state.push_str(&combined_buf);
                    // Inject one-shot clipboard data for OSC 52 delivery to
                    // the client.  Only the *response* includes this field;
                    // the cached copy does not, so subsequent NC frames won't
                    // re-trigger clipboard emission on the client.
                    if let Some(clip_text) = app.clipboard_osc52.take() {
                        let clip_b64 = base64_encode(&clip_text);
                        // Replace trailing '}' with the extra field
                        if combined_buf.ends_with('}') {
                            combined_buf.pop();
                            combined_buf.push_str(",\"clipboard_osc52\":\"");
                            combined_buf.push_str(&clip_b64);
                            combined_buf.push_str("\"}");
                        }
                    }
                    // Forward audible bell to client terminal
                    if app.bell_forward {
                        app.bell_forward = false;
                        if combined_buf.ends_with('}') {
                            combined_buf.pop();
                            combined_buf.push_str(",\"bell\":true}");
                        }
                    }
                    cached_data_version = combined_data_version(&app);
                    state_dirty = false;
                    // Timing log: dump-state build time
                    if std::env::var("PSMUX_LATENCY_LOG").unwrap_or_default() == "1" {
                        let total_us = _t_layout.elapsed().as_micros();
                        use std::io::Write as _;
                        static SRV_LOG: std::sync::OnceLock<std::sync::Mutex<std::fs::File>> = std::sync::OnceLock::new();
                        let log = SRV_LOG.get_or_init(|| {
                            let p = std::path::PathBuf::from(std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\gj".into())).join("psmux_server_latency.log");
                            std::sync::Mutex::new(std::fs::File::create(p).expect("create latency log"))
                        });
                        if let Ok(mut f) = log.lock() {
                            let _ = writeln!(f, "[SRV] dump: layout={}us total={}us json_len={}", _layout_ms, total_us, combined_buf.len());
                        }
                    }
                    // Push the newly-built frame to ALL persistent clients so
                    // that other attached sessions see the update immediately,
                    // even if they are idle and not polling dump-state.
                    // Without this, the DumpState handler clears state_dirty,
                    // and the bottom-of-loop push section never fires for frames
                    // already served to the requesting client.
                    // Push combined_buf (not cached_dump_state) so one-shot
                    // fields like bell and clipboard reach all clients.
                    // The cached copy omits them for NC dedup safety.
                    crate::types::push_frame(&combined_buf);
                    let _ = resp.send(combined_buf.clone());
                }
                CtrlReq::SendText(s) => { app.status_message = None; send_text_to_active(&mut app, &s)?; echo_pending_until = Some(Instant::now()); }
                CtrlReq::SendKey(k) => { app.status_message = None; send_key_to_active(&mut app, &k)?; echo_pending_until = Some(Instant::now()); }
                CtrlReq::SendPaste(s) => { send_paste_to_active(&mut app, &s)?; echo_pending_until = Some(Instant::now()); }
                CtrlReq::ZoomPane => { toggle_zoom(&mut app); state_dirty = true; meta_dirty = true; hook_event = Some("after-resize-pane"); }
                CtrlReq::PrefixBegin => { app.client_prefix_active = true; state_dirty = true; }
                CtrlReq::PrefixEnd => { app.client_prefix_active = false; state_dirty = true; }
                CtrlReq::CopyEnter => { enter_copy_mode(&mut app); hook_event = Some("pane-mode-changed"); }
                CtrlReq::CopyEnterPageUp => {
                    enter_copy_mode(&mut app);
                    let half = app.windows.get(app.active_idx)
                        .and_then(|w| active_pane(&w.root, &w.active_path))
                        .map(|p| p.last_rows as usize).unwrap_or(20);
                    scroll_copy_up(&mut app, half);
                    hook_event = Some("pane-mode-changed");
                }
                CtrlReq::ClockMode => { app.mode = Mode::ClockMode; state_dirty = true; hook_event = Some("pane-mode-changed"); }
                CtrlReq::CopyMove(dx, dy) => { move_copy_cursor(&mut app, dx, dy); }
                CtrlReq::CopyAnchor => { if let Some((r,c)) = current_prompt_pos(&mut app) { app.copy_anchor = Some((r,c)); app.copy_anchor_scroll_offset = app.copy_scroll_offset; app.copy_pos = Some((r,c)); } }
                CtrlReq::CopyYank => {
                    let _ = yank_selection(&mut app);
                    if let Some(cmds) = app.hooks.get("pane-set-clipboard") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    exit_copy_mode(&mut app);
                    hook_event = Some("pane-mode-changed");
                }
                CtrlReq::CopyRectToggle => {
                    app.copy_selection_mode = match app.copy_selection_mode {
                        crate::types::SelectionMode::Rect => crate::types::SelectionMode::Char,
                        _ => crate::types::SelectionMode::Rect,
                    };
                }
                CtrlReq::ClientSize(cid, w, h) => { 
                    app.client_sizes.insert(cid, (w, h));
                    app.latest_client_id = Some(cid);
                    // Update registry with new size and activity timestamp
                    if let Some(info) = app.client_registry.get_mut(&cid) {
                        info.width = w;
                        info.height = h;
                        info.last_activity = std::time::Instant::now();
                    }
                    let (ew, eh) = compute_effective_client_size(&app).unwrap_or((w, h));
                    app.last_window_area = Rect { x: 0, y: 0, width: ew, height: eh }; 
                    resize_all_panes(&mut app);
                    // Respawn warm pane at the new terminal dimensions so
                    // the next new-window gets a pane whose parser grid
                    // already matches the display — no resize reflow needed,
                    // prompt appears pixel-perfect on the first frame.
                    let need_respawn = app.warm_pane.as_ref().map_or(true, |wp| wp.rows != eh || wp.cols != ew);
                    if need_respawn {
                        if let Some(mut old) = app.warm_pane.take() { old.child.kill().ok(); }
                        match spawn_warm_pane(&*pty_system, &mut app) {
                            Ok(wp) => { app.warm_pane = Some(wp); }
                            Err(_) => {}
                        }
                    }
                    hook_event = Some("client-resized");
                }
                CtrlReq::FocusPaneCmd(pid) => {
                    let old_path = app.windows[app.active_idx].active_path.clone();
                    switch_with_copy_save(&mut app, |app| { focus_pane_by_id(app, pid); });
                    if app.windows[app.active_idx].active_path != old_path { unzoom_if_zoomed(&mut app); }
                    meta_dirty = true;
                }
                CtrlReq::FocusWindowCmd(wid) => { switch_with_copy_save(&mut app, |app| { if let Some(idx) = find_window_index_by_id(app, wid) { app.active_idx = idx; } }); resize_all_panes(&mut app); meta_dirty = true; }
                CtrlReq::MouseDown(cid,x,y) => { if app.mouse_enabled { app.latest_client_id = Some(cid); remote_mouse_down(&mut app, x, y); state_dirty = true; meta_dirty = true; echo_pending_until = Some(Instant::now()); } }
                CtrlReq::MouseDownRight(cid,x,y) => { if app.mouse_enabled { app.latest_client_id = Some(cid); remote_mouse_button(&mut app, x, y, 2, true); state_dirty = true; echo_pending_until = Some(Instant::now()); } }
                CtrlReq::MouseDownMiddle(cid,x,y) => { if app.mouse_enabled { app.latest_client_id = Some(cid); remote_mouse_button(&mut app, x, y, 1, true); state_dirty = true; echo_pending_until = Some(Instant::now()); } }
                CtrlReq::MouseDrag(cid,x,y) => { if app.mouse_enabled { app.latest_client_id = Some(cid); remote_mouse_drag(&mut app, x, y); state_dirty = true; echo_pending_until = Some(Instant::now()); } }
                CtrlReq::MouseUp(cid,x,y) => { if app.mouse_enabled { app.latest_client_id = Some(cid); remote_mouse_up(&mut app, x, y); state_dirty = true; echo_pending_until = Some(Instant::now()); } }
                CtrlReq::MouseUpRight(cid,x,y) => { if app.mouse_enabled { app.latest_client_id = Some(cid); remote_mouse_button(&mut app, x, y, 2, false); state_dirty = true; echo_pending_until = Some(Instant::now()); } }
                CtrlReq::MouseUpMiddle(cid,x,y) => { if app.mouse_enabled { app.latest_client_id = Some(cid); remote_mouse_button(&mut app, x, y, 1, false); state_dirty = true; echo_pending_until = Some(Instant::now()); } }
                CtrlReq::MouseMove(cid,x,y) => { if app.mouse_enabled { app.latest_client_id = Some(cid); remote_mouse_motion(&mut app, x, y); state_dirty = true; echo_pending_until = Some(Instant::now()); } }
                CtrlReq::ScrollUp(cid, x, y) => { if app.mouse_enabled { app.latest_client_id = Some(cid); remote_scroll_up(&mut app, x, y); state_dirty = true; echo_pending_until = Some(Instant::now()); } }
                CtrlReq::ScrollDown(cid, x, y) => { if app.mouse_enabled { app.latest_client_id = Some(cid); remote_scroll_down(&mut app, x, y); state_dirty = true; echo_pending_until = Some(Instant::now()); } }
                CtrlReq::PaneMouse(cid, pane_id, button, col, row, press) => { if app.mouse_enabled { app.latest_client_id = Some(cid); handle_pane_mouse(&mut app, pane_id, button, col, row, press); state_dirty = true; meta_dirty = true; echo_pending_until = Some(Instant::now()); } }
                CtrlReq::PaneScroll(cid, pane_id, up) => { if app.mouse_enabled { app.latest_client_id = Some(cid); handle_pane_scroll(&mut app, pane_id, up); state_dirty = true; meta_dirty = true; echo_pending_until = Some(Instant::now()); } }
                CtrlReq::SplitSetSizes(cid, path, sizes) => { if app.mouse_enabled { app.latest_client_id = Some(cid); handle_split_set_sizes(&mut app, &path, &sizes); state_dirty = true; meta_dirty = true; echo_pending_until = Some(Instant::now()); } }
                CtrlReq::SplitResizeDone(cid) => { if app.mouse_enabled { app.latest_client_id = Some(cid); handle_split_resize_done(&mut app); state_dirty = true; meta_dirty = true; } }
                CtrlReq::NextWindow => {
                    if let Some(cmds) = app.hooks.get("before-select-window") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    if !app.windows.is_empty() { switch_with_copy_save(&mut app, |app| { app.last_window_idx = app.active_idx; app.active_idx = (app.active_idx + 1) % app.windows.len(); }); resize_all_panes(&mut app); } meta_dirty = true; hook_event = Some("after-select-window");
                }
                CtrlReq::PrevWindow => {
                    if let Some(cmds) = app.hooks.get("before-select-window") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    if !app.windows.is_empty() { switch_with_copy_save(&mut app, |app| { app.last_window_idx = app.active_idx; app.active_idx = (app.active_idx + app.windows.len() - 1) % app.windows.len(); }); resize_all_panes(&mut app); } meta_dirty = true; hook_event = Some("after-select-window");
                }
                CtrlReq::RenameWindow(name) => {
                    if let Some(cmds) = app.hooks.get("before-rename-window") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    let win = &mut app.windows[app.active_idx]; win.name = name; win.manual_rename = true; meta_dirty = true; hook_event = Some("after-rename-window");
                }
                CtrlReq::ListWindows(resp) => { helpers::propagate_osc_titles(&mut app); let json = list_windows_json(&app)?; let _ = resp.send(json); }
                CtrlReq::ListWindowsTmux(resp) => { helpers::propagate_osc_titles(&mut app); let text = list_windows_tmux(&app); let _ = resp.send(text); }
                CtrlReq::ListWindowsFormat(resp, fmt) => { helpers::propagate_osc_titles(&mut app); let text = format_list_windows(&app, &fmt); let _ = resp.send(text); }
                CtrlReq::ListTree(resp) => { let json = list_tree_json(&app)?; let _ = resp.send(json); }
                CtrlReq::WindowLayout(wid, resp) => {
                    let json = crate::util::window_layout_json(&app, wid)
                        .unwrap_or_else(|_| "{}".to_string());
                    let _ = resp.send(json);
                }
                CtrlReq::WindowDump(wid, resp) => {
                    let json = crate::layout::dump_window_layout_json(&mut app, wid)
                        .unwrap_or_else(|_| "{}".to_string());
                    let _ = resp.send(json);
                }
                CtrlReq::ToggleSync => { app.sync_input = !app.sync_input; }
                CtrlReq::SetPaneTitle(title) => {
                    let win = &mut app.windows[app.active_idx];
                    if let Some(p) = active_pane_mut(&mut win.root, &win.active_path) {
                        p.title_locked = !title.is_empty();
                        p.title = title;
                    }
                    meta_dirty = true;
                }
                CtrlReq::SetPaneStyle(style) => {
                    // Per-pane styling (e.g. "bg=default,fg=blue") matching
                    // tmux's `-P` flag which sets window-style + window-active-style.
                    // Store on the pane for API compatibility; ConPTY rendering
                    // doesn't support per-pane fg/bg tinting yet.
                    let win = &mut app.windows[app.active_idx];
                    if let Some(p) = active_pane_mut(&mut win.root, &win.active_path) {
                        p.pane_style = Some(style);
                    }
                }
                CtrlReq::SendKeys(keys, literal) => {
                    let in_copy = matches!(app.mode, Mode::CopyMode | Mode::CopySearch { .. });
                    if in_copy {
                        // In copy/search mode — route through mode-aware handlers
                        if literal {
                            send_text_to_active(&mut app, &keys)?;
                        } else {
                            let parts: Vec<&str> = keys.split_whitespace().collect();
                            for key in parts.iter() {
                                let key_upper = key.to_uppercase();
                                let normalized = match key_upper.as_str() {
                                    "ENTER" => "enter",
                                    "TAB" => "tab",
                                    "BTAB" | "BACKTAB" => "btab",
                                    "ESCAPE" | "ESC" => "esc",
                                    "SPACE" => "space",
                                    "BSPACE" | "BACKSPACE" => "backspace",
                                    "UP" => "up",
                                    "DOWN" => "down",
                                    "RIGHT" => "right",
                                    "LEFT" => "left",
                                    "HOME" => "home",
                                    "END" => "end",
                                    "PAGEUP" | "PPAGE" => "pageup",
                                    "PAGEDOWN" | "NPAGE" => "pagedown",
                                    "DELETE" | "DC" => "delete",
                                    "INSERT" | "IC" => "insert",
                                    _ => "",
                                };
                                if !normalized.is_empty() {
                                    send_key_to_active(&mut app, normalized)?;
                                } else if key_upper.starts_with("C-") || key_upper.starts_with("M-") || (key_upper.starts_with("F") && key_upper.len() >= 2 && key_upper[1..].chars().all(|c| c.is_ascii_digit())) {
                                    send_key_to_active(&mut app, &key.to_lowercase())?;
                                } else {
                                    // Plain text char — route through send_text_to_active (handles copy mode chars)
                                    send_text_to_active(&mut app, key)?;
                                }
                            }
                        }
                    } else if literal {
                        send_text_to_active(&mut app, &keys)?;
                    } else {
                        let parts: Vec<&str> = keys.split_whitespace().collect();
                        for (i, key) in parts.iter().enumerate() {
                            let key_upper = key.to_uppercase();
                            let _is_special = matches!(key_upper.as_str(), 
                                "ENTER" | "TAB" | "BTAB" | "BACKTAB" | "ESCAPE" | "ESC" | "SPACE" | "BSPACE" | "BACKSPACE" |
                                "UP" | "DOWN" | "RIGHT" | "LEFT" | "HOME" | "END" |
                                "PAGEUP" | "PPAGE" | "PAGEDOWN" | "NPAGE" | "DELETE" | "DC" | "INSERT" | "IC" |
                                "F1" | "F2" | "F3" | "F4" | "F5" | "F6" | "F7" | "F8" | "F9" | "F10" | "F11" | "F12"
                            ) || key_upper.starts_with("C-") || key_upper.starts_with("M-") || key_upper.starts_with("S-");
                            
                            match key_upper.as_str() {
                                "ENTER" => send_text_to_active(&mut app, "\r")?,
                                "TAB" => send_text_to_active(&mut app, "\t")?,
                                "BTAB" | "BACKTAB" => send_text_to_active(&mut app, "\x1b[Z")?,
                                "ESCAPE" | "ESC" => send_text_to_active(&mut app, "\x1b")?,
                                "SPACE" => send_text_to_active(&mut app, " ")?,
                                "BSPACE" | "BACKSPACE" => send_text_to_active(&mut app, "\x7f")?,
                                "UP" => send_text_to_active(&mut app, "\x1b[A")?,
                                "DOWN" => send_text_to_active(&mut app, "\x1b[B")?,
                                "RIGHT" => send_text_to_active(&mut app, "\x1b[C")?,
                                "LEFT" => send_text_to_active(&mut app, "\x1b[D")?,
                                "HOME" => send_text_to_active(&mut app, "\x1b[H")?,
                                "END" => send_text_to_active(&mut app, "\x1b[F")?,
                                "PAGEUP" | "PPAGE" => send_text_to_active(&mut app, "\x1b[5~")?,
                                "PAGEDOWN" | "NPAGE" => send_text_to_active(&mut app, "\x1b[6~")?,
                                "DELETE" | "DC" => send_text_to_active(&mut app, "\x1b[3~")?,
                                "INSERT" | "IC" => send_text_to_active(&mut app, "\x1b[2~")?,
                                "F1" => send_text_to_active(&mut app, "\x1bOP")?,
                                "F2" => send_text_to_active(&mut app, "\x1bOQ")?,
                                "F3" => send_text_to_active(&mut app, "\x1bOR")?,
                                "F4" => send_text_to_active(&mut app, "\x1bOS")?,
                                "F5" => send_text_to_active(&mut app, "\x1b[15~")?,
                                "F6" => send_text_to_active(&mut app, "\x1b[17~")?,
                                "F7" => send_text_to_active(&mut app, "\x1b[18~")?,
                                "F8" => send_text_to_active(&mut app, "\x1b[19~")?,
                                "F9" => send_text_to_active(&mut app, "\x1b[20~")?,
                                "F10" => send_text_to_active(&mut app, "\x1b[21~")?,
                                "F11" => send_text_to_active(&mut app, "\x1b[23~")?,
                                "F12" => send_text_to_active(&mut app, "\x1b[24~")?,
                                // Modifier + special key combos (C-Left, S-Right, C-M-Up, etc.)
                                // must be checked BEFORE the generic C-x / M-x single-char handlers.
                                s if crate::input::parse_modified_special_key(s).is_some() => {
                                    let seq = crate::input::parse_modified_special_key(s).unwrap();
                                    send_text_to_active(&mut app, &seq)?;
                                }
                                s if s.starts_with("C-M-") || s.starts_with("C-m-") => {
                                    if let Some(c) = key.chars().nth(4) {
                                        if let Some(ctrl) = crate::input::ctrl_char_send_keys_byte(c) {
                                            send_text_to_active(&mut app, &format!("\x1b{}", ctrl as char))?;
                                        }
                                    }
                                }
                                s if s.starts_with("C-") => {
                                    if let Some(c) = s.chars().nth(2) {
                                        let Some(ctrl) = crate::input::ctrl_char_send_keys_byte(c) else { continue };
                                        send_text_to_active(&mut app, &String::from(ctrl as char))?;
                                        // On Windows, writing 0x03 to the PTY pipe doesn't
                                        // generate CTRL_C_EVENT when ENABLE_PROCESSED_INPUT
                                        // is disabled (e.g. after a TUI app).  Fire the real
                                        // signal via the platform helper so detached/headless
                                        // send-keys C-c reliably interrupts processes.
                                        #[cfg(windows)]
                                        if ctrl == 0x03 {
                                            if let Some(win) = app.windows.get_mut(app.active_idx) {
                                                if let Some(p) = active_pane_mut(&mut win.root, &win.active_path) {
                                                    if p.child_pid.is_none() {
                                                        p.child_pid = crate::platform::mouse_inject::get_child_pid(&*p.child);
                                                    }
                                                    if let Some(pid) = p.child_pid {
                                                        crate::platform::mouse_inject::send_ctrl_c_event(pid, false);
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                                s if s.starts_with("M-") => {
                                    if let Some(c) = key.chars().nth(2) {
                                        send_text_to_active(&mut app, &format!("\x1b{}", c))?;
                                    }
                                }
                                _ => {
                                    send_text_to_active(&mut app, key)?;
                                    if i + 1 < parts.len() {
                                        let next_upper = parts[i + 1].to_uppercase();
                                        let next_is_special = matches!(next_upper.as_str(),
                                            "ENTER" | "TAB" | "BTAB" | "BACKTAB" | "ESCAPE" | "ESC" | "SPACE" | "BSPACE" | "BACKSPACE" |
                                            "UP" | "DOWN" | "RIGHT" | "LEFT" | "HOME" | "END" |
                                            "PAGEUP" | "PPAGE" | "PAGEDOWN" | "NPAGE" | "DELETE" | "DC" | "INSERT" | "IC" |
                                            "F1" | "F2" | "F3" | "F4" | "F5" | "F6" | "F7" | "F8" | "F9" | "F10" | "F11" | "F12"
                                        ) || next_upper.starts_with("C-") || next_upper.starts_with("M-") || next_upper.starts_with("S-");
                                        if !next_is_special {
                                            send_text_to_active(&mut app, " ")?;
                                        }
                                    }
                                }
                            }
                        }
                    }
                    echo_pending_until = Some(Instant::now());
                }
                CtrlReq::SendKeysX(cmd) => {
                    // send-keys -X: dispatch copy-mode commands by name
                    // This is the primary mechanism used by tmux-yank and other plugins
                    let in_copy = matches!(app.mode, Mode::CopyMode | Mode::CopySearch { .. });
                    if !in_copy {
                        // Auto-enter copy mode for commands that require it
                        enter_copy_mode(&mut app);
                    }
                    match cmd.as_str() {
                        "cancel" => {
                            app.mode = Mode::Passthrough;
                            app.copy_anchor = None;
                            app.copy_pos = None;
                            app.copy_scroll_offset = 0;
                            let win = &mut app.windows[app.active_idx];
                            if let Some(p) = active_pane_mut(&mut win.root, &win.active_path) {
                                if let Ok(mut parser) = p.term.lock() {
                                    parser.screen_mut().set_scrollback(0);
                                }
                            }
                            if let Some(cmds) = app.hooks.get("pane-mode-changed") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                        }
                        "begin-selection" => {
                            if let Some((r,c)) = crate::copy_mode::get_copy_pos(&mut app) {
                                app.copy_anchor = Some((r,c));
                                app.copy_anchor_scroll_offset = app.copy_scroll_offset;
                                app.copy_pos = Some((r,c));
                                app.copy_selection_mode = crate::types::SelectionMode::Char;
                            }
                        }
                        "select-line" => {
                            if let Some((r,c)) = crate::copy_mode::get_copy_pos(&mut app) {
                                app.copy_anchor = Some((r,c));
                                app.copy_anchor_scroll_offset = app.copy_scroll_offset;
                                app.copy_pos = Some((r,c));
                                app.copy_selection_mode = crate::types::SelectionMode::Line;
                            }
                        }
                        "rectangle-toggle" => {
                            app.copy_selection_mode = match app.copy_selection_mode {
                                crate::types::SelectionMode::Rect => crate::types::SelectionMode::Char,
                                _ => crate::types::SelectionMode::Rect,
                            };
                        }
                        "copy-selection" => {
                            let _ = yank_selection(&mut app);
                            if let Some(cmds) = app.hooks.get("pane-set-clipboard") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                        }
                        "copy-selection-and-cancel" => {
                            let _ = yank_selection(&mut app);
                            if let Some(cmds) = app.hooks.get("pane-set-clipboard") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                            app.mode = Mode::Passthrough;
                            app.copy_scroll_offset = 0;
                            app.copy_pos = None;
                            if let Some(cmds) = app.hooks.get("pane-mode-changed") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                        }
                        "copy-selection-no-clear" => {
                            let _ = yank_selection(&mut app);
                            if let Some(cmds) = app.hooks.get("pane-set-clipboard") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                        }
                        s if s.starts_with("copy-pipe-and-cancel") || s.starts_with("copy-pipe") => {
                            // copy-pipe[-and-cancel] [command] — yank + pipe to command
                            let _ = yank_selection(&mut app);
                            if let Some(cmds) = app.hooks.get("pane-set-clipboard") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                            // Extract pipe command from argument if present
                            let cancel = s.contains("cancel");
                            let pipe_cmd = cmd.strip_prefix("copy-pipe-and-cancel")
                                .or_else(|| cmd.strip_prefix("copy-pipe"))
                                .unwrap_or("")
                                .trim();
                            if !pipe_cmd.is_empty() {
                                if let Some(text) = app.paste_buffers.first().cloned() {
                                    // Pipe yanked text to the command's stdin
                                    let mut copy_pipe_cmd = std::process::Command::new(if cfg!(windows) { "pwsh" } else { "sh" });
                                    copy_pipe_cmd.args(if cfg!(windows) { vec!["-NoProfile", "-Command", pipe_cmd] } else { vec!["-c", pipe_cmd] })
                                        .stdin(std::process::Stdio::piped())
                                        .stdout(std::process::Stdio::null())
                                        .stderr(std::process::Stdio::null());
                                    { use crate::platform::HideWindowCommandExt; copy_pipe_cmd.hide_window(); }
                                    if let Ok(mut child) = copy_pipe_cmd.spawn() {
                                        if let Some(mut stdin) = child.stdin.take() {
                                            use std::io::Write;
                                            let _ = stdin.write_all(text.as_bytes());
                                        }
                                        let _ = child.wait();
                                    }
                                }
                            }
                            if cancel {
                                app.mode = Mode::Passthrough;
                                app.copy_scroll_offset = 0;
                                app.copy_pos = None;
                                if let Some(cmds) = app.hooks.get("pane-mode-changed") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                            }
                        }
                        "cursor-up" => { move_copy_cursor(&mut app, 0, -1); }
                        "cursor-down" => { move_copy_cursor(&mut app, 0, 1); }
                        "cursor-left" => { move_copy_cursor(&mut app, -1, 0); }
                        "cursor-right" => { move_copy_cursor(&mut app, 1, 0); }
                        "start-of-line" => { crate::copy_mode::move_to_line_start(&mut app); }
                        "end-of-line" => { crate::copy_mode::move_to_line_end(&mut app); }
                        "back-to-indentation" => { crate::copy_mode::move_to_first_nonblank(&mut app); }
                        "next-word" => { crate::copy_mode::move_word_forward(&mut app); }
                        "previous-word" => { crate::copy_mode::move_word_backward(&mut app); }
                        "next-word-end" => { crate::copy_mode::move_word_end(&mut app); }
                        "next-space" => { crate::copy_mode::move_word_forward_big(&mut app); }
                        "previous-space" => { crate::copy_mode::move_word_backward_big(&mut app); }
                        "next-space-end" => { crate::copy_mode::move_word_end_big(&mut app); }
                        "top-line" => { crate::copy_mode::move_to_screen_top(&mut app); }
                        "middle-line" => { crate::copy_mode::move_to_screen_middle(&mut app); }
                        "bottom-line" => { crate::copy_mode::move_to_screen_bottom(&mut app); }
                        "history-top" => { crate::copy_mode::scroll_to_top(&mut app); }
                        "history-bottom" => { crate::copy_mode::scroll_to_bottom(&mut app); }
                        "halfpage-up" => {
                            let half = app.windows.get(app.active_idx)
                                .and_then(|w| active_pane(&w.root, &w.active_path))
                                .map(|p| (p.last_rows / 2) as usize).unwrap_or(10);
                            scroll_copy_up(&mut app, half);
                        }
                        "halfpage-down" => {
                            let half = app.windows.get(app.active_idx)
                                .and_then(|w| active_pane(&w.root, &w.active_path))
                                .map(|p| (p.last_rows / 2) as usize).unwrap_or(10);
                            scroll_copy_down(&mut app, half);
                        }
                        "page-up" => { scroll_copy_up(&mut app, 20); }
                        "page-down" => { scroll_copy_down(&mut app, 20); }
                        "scroll-up" => { scroll_copy_up(&mut app, 1); }
                        "scroll-down" => { scroll_copy_down(&mut app, 1); }
                        "search-forward" | "search-forward-incremental" => {
                            app.mode = Mode::CopySearch { input: String::new(), forward: true };
                        }
                        "search-backward" | "search-backward-incremental" => {
                            app.mode = Mode::CopySearch { input: String::new(), forward: false };
                        }
                        "search-again" => { crate::copy_mode::search_next(&mut app); }
                        "search-reverse" => { crate::copy_mode::search_prev(&mut app); }
                        "copy-end-of-line" => { let _ = crate::copy_mode::copy_end_of_line(&mut app); app.mode = Mode::Passthrough; app.copy_scroll_offset = 0; app.copy_pos = None; }
                        "select-word" => {
                            // Select the word under cursor
                            crate::copy_mode::move_word_backward(&mut app);
                            if let Some((r,c)) = crate::copy_mode::get_copy_pos(&mut app) {
                                app.copy_anchor = Some((r,c));
                                app.copy_anchor_scroll_offset = app.copy_scroll_offset;
                                app.copy_selection_mode = crate::types::SelectionMode::Char;
                            }
                            crate::copy_mode::move_word_end(&mut app);
                        }
                        "other-end" => {
                            if let (Some(a), Some(p)) = (app.copy_anchor, app.copy_pos) {
                                app.copy_anchor = Some(p);
                                app.copy_anchor_scroll_offset = app.copy_scroll_offset;
                                app.copy_pos = Some(a);
                            }
                        }
                        "clear-selection" => {
                            app.copy_anchor = None;
                            app.copy_selection_mode = crate::types::SelectionMode::Char;
                        }
                        "append-selection" => {
                            // Append to existing buffer instead of replacing
                            let _ = yank_selection(&mut app);
                            if let Some(cmds) = app.hooks.get("pane-set-clipboard") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                            if app.paste_buffers.len() >= 2 {
                                let appended = format!("{}{}", app.paste_buffers[1], app.paste_buffers[0]);
                                app.paste_buffers[0] = appended;
                            }
                        }
                        "append-selection-and-cancel" => {
                            let _ = yank_selection(&mut app);
                            if let Some(cmds) = app.hooks.get("pane-set-clipboard") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                            if app.paste_buffers.len() >= 2 {
                                let appended = format!("{}{}", app.paste_buffers[1], app.paste_buffers[0]);
                                app.paste_buffers[0] = appended;
                            }
                            app.mode = Mode::Passthrough;
                            app.copy_scroll_offset = 0;
                            app.copy_pos = None;
                            if let Some(cmds) = app.hooks.get("pane-mode-changed") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                        }
                        "copy-line" => {
                            // Select entire current line and yank
                            if let Some((r, _)) = crate::copy_mode::get_copy_pos(&mut app) {
                                app.copy_anchor = Some((r, 0));
                                app.copy_anchor_scroll_offset = app.copy_scroll_offset;
                                app.copy_selection_mode = crate::types::SelectionMode::Line;
                                let cols = app.windows.get(app.active_idx)
                                    .and_then(|w| active_pane(&w.root, &w.active_path))
                                    .map(|p| p.last_cols).unwrap_or(80);
                                app.copy_pos = Some((r, cols.saturating_sub(1)));
                                let _ = yank_selection(&mut app);
                                if let Some(cmds) = app.hooks.get("pane-set-clipboard") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                            }
                            app.mode = Mode::Passthrough;
                            app.copy_scroll_offset = 0;
                            app.copy_pos = None;
                            if let Some(cmds) = app.hooks.get("pane-mode-changed") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                        }
                        s if s.starts_with("goto-line") => {
                            // goto-line <N> — jump to line N in scrollback
                            let n = s.strip_prefix("goto-line").unwrap_or("").trim()
                                .parse::<u16>().unwrap_or(0);
                            app.copy_pos = Some((n, 0));
                        }
                        "jump-forward" => { app.copy_find_char_pending = Some(0); }
                        "jump-backward" => { app.copy_find_char_pending = Some(1); }
                        "jump-to-forward" => { app.copy_find_char_pending = Some(2); }
                        "jump-to-backward" => { app.copy_find_char_pending = Some(3); }
                        "jump-again" => {
                            // Repeat last find-char in same direction
                            // We'd need to store last char; for now emit the pending
                        }
                        "jump-reverse" => {
                            // Repeat last find-char in reverse direction
                        }
                        "next-paragraph" => {
                            crate::copy_mode::move_next_paragraph(&mut app);
                        }
                        "previous-paragraph" => {
                            crate::copy_mode::move_prev_paragraph(&mut app);
                        }
                        "next-matching-bracket" => {
                            crate::copy_mode::move_matching_bracket(&mut app);
                        }
                        "stop-selection" => {
                            // Keep cursor position but stop extending selection
                            app.copy_anchor = None;
                        }
                        _ => {} // ignore unknown copy-mode commands
                    }
                }
                CtrlReq::SelectPane(dir, keep_zoom) => {
                    if let Some(cmds) = app.hooks.get("before-select-pane") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    // Auto-unzoom when navigating to another pane (tmux behavior).
                    // For directional nav: unzoom first so compute_rects uses
                    // real geometry, then re-zoom only if focus didn't change.
                    // For other cases: only unzoom if focus actually changes.
                    // (fixes #46)
                    match dir.as_str() {
                        "U" | "D" | "L" | "R" => {
                            let focus_dir = match dir.as_str() {
                                "U" => FocusDir::Up, "D" => FocusDir::Down,
                                "L" => FocusDir::Left, _ => FocusDir::Right,
                            };
                            if keep_zoom {
                                let old_path = app.windows[app.active_idx].active_path.clone();
                                switch_with_copy_save(&mut app, |app| {
                                    move_focus_preserving_zoom(app, focus_dir);
                                });
                                if app.windows[app.active_idx].active_path != old_path {
                                    app.last_pane_path = old_path;
                                }
                            } else {
                                let was_zoomed = unzoom_if_zoomed(&mut app);
                                if was_zoomed {
                                // Zoom-aware: check direct neighbor or wrap target (tmux parity: unzoom+wrap).
                                let win = &app.windows[app.active_idx];
                                let mut rects: Vec<(Vec<usize>, ratatui::layout::Rect)> = Vec::new();
                                crate::tree::compute_rects(&win.root, app.last_window_area, &mut rects);
                                let active_idx = rects.iter().position(|(path, _)| *path == win.active_path);
                                let has_target = 
                                    if let Some(ai) = active_idx {
                                        let (_, arect) = &rects[ai];
                                        find_best_pane_in_direction(&rects, ai, arect, focus_dir, &[], &[])
                                            .or_else(|| find_wrap_target(&rects, ai, arect, focus_dir, &[], &[]))
                                            .is_some()
                                    } else { false };
                                    if has_target {
                                        let old_path = app.windows[app.active_idx].active_path.clone();
                                        switch_with_copy_save(&mut app, |app| {
                                            move_focus(app, focus_dir);
                                        });
                                        app.last_pane_path = old_path;
                                    } else {
                                        // No reachable pane (single-pane window) — re-zoom
                                        toggle_zoom(&mut app);
                                    }
                                } else {
                                    let old_path = app.windows[app.active_idx].active_path.clone();
                                    switch_with_copy_save(&mut app, |app| {
                                        move_focus(app, focus_dir);
                                    });
                                    if app.windows[app.active_idx].active_path != old_path {
                                        app.last_pane_path = old_path;
                                    }
                                }
                            }
                        }
                        "last" => {
                            // select-pane -l: switch to last active pane
                            let old_path = app.windows[app.active_idx].active_path.clone();
                            switch_with_copy_save(&mut app, |app| {
                                let win = &mut app.windows[app.active_idx];
                                if !app.last_pane_path.is_empty() {
                                    let tmp = win.active_path.clone();
                                    win.active_path = app.last_pane_path.clone();
                                    app.last_pane_path = tmp;
                                }
                            });
                            if app.windows[app.active_idx].active_path != old_path {
                                // Update MRU for the newly focused pane
                                let win = &mut app.windows[app.active_idx];
                                if let Some(pid) = get_active_pane_id(&win.root, &win.active_path) {
                                    crate::tree::touch_mru(&mut win.pane_mru, pid);
                                }
                                unzoom_if_zoomed(&mut app);
                            }
                        }
                        "mark" => {
                            // select-pane -m: mark the current pane
                            let win = &app.windows[app.active_idx];
                            if let Some(pid) = get_active_pane_id(&win.root, &win.active_path) {
                                app.marked_pane = Some((app.active_idx, pid));
                            }
                        }
                        "next" => {
                            // select-pane next: cycle to next pane (like Prefix+o / tmux -t :.+)
                            let old_path = app.windows[app.active_idx].active_path.clone();
                            switch_with_copy_save(&mut app, |app| {
                                let win = &app.windows[app.active_idx];
                                let mut pane_paths = Vec::new();
                                let mut path = Vec::new();
                                collect_pane_paths_server(&win.root, &mut path, &mut pane_paths);
                                if let Some(cur) = pane_paths.iter().position(|p| *p == win.active_path) {
                                    let next = (cur + 1) % pane_paths.len();
                                    let new_path = pane_paths[next].clone();
                                    let win = &mut app.windows[app.active_idx];
                                    app.last_pane_path = win.active_path.clone();
                                    win.active_path = new_path;
                                }
                            });
                            if app.windows[app.active_idx].active_path != old_path {
                                let win = &mut app.windows[app.active_idx];
                                if let Some(pid) = get_active_pane_id(&win.root, &win.active_path) {
                                    crate::tree::touch_mru(&mut win.pane_mru, pid);
                                }
                                unzoom_if_zoomed(&mut app);
                            }
                        }
                        "prev" => {
                            // select-pane prev: cycle to previous pane (tmux -t :.-)
                            let old_path = app.windows[app.active_idx].active_path.clone();
                            switch_with_copy_save(&mut app, |app| {
                                let win = &app.windows[app.active_idx];
                                let mut pane_paths = Vec::new();
                                let mut path = Vec::new();
                                collect_pane_paths_server(&win.root, &mut path, &mut pane_paths);
                                if let Some(cur) = pane_paths.iter().position(|p| *p == win.active_path) {
                                    let prev = (cur + pane_paths.len() - 1) % pane_paths.len();
                                    let new_path = pane_paths[prev].clone();
                                    let win = &mut app.windows[app.active_idx];
                                    app.last_pane_path = win.active_path.clone();
                                    win.active_path = new_path;
                                }
                            });
                            if app.windows[app.active_idx].active_path != old_path {
                                let win = &mut app.windows[app.active_idx];
                                if let Some(pid) = get_active_pane_id(&win.root, &win.active_path) {
                                    crate::tree::touch_mru(&mut win.pane_mru, pid);
                                }
                                unzoom_if_zoomed(&mut app);
                            }
                        }
                        "unmark" => {
                            // select-pane -M: clear the marked pane
                            app.marked_pane = None;
                        }
                        _ => {}
                    }
                    meta_dirty = true;
                    hook_event = Some("after-select-pane");
                }
                CtrlReq::SelectWindow(idx) => {
                    if let Some(cmds) = app.hooks.get("before-select-window") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    if idx >= app.window_base_index {
                        let internal_idx = idx - app.window_base_index;
                        if internal_idx < app.windows.len() && internal_idx != app.active_idx {
                            switch_with_copy_save(&mut app, |app| {
                                app.last_window_idx = app.active_idx;
                                app.active_idx = internal_idx;
                            });
                            resize_all_panes(&mut app);
                        }
                    }
                    meta_dirty = true;
                    hook_event = Some("after-select-window");
                }
                CtrlReq::ListPanes(resp) => {
                    helpers::propagate_osc_titles(&mut app);
                    let mut output = String::new();
                    let win = &app.windows[app.active_idx];
                    fn collect_panes(node: &Node, panes: &mut Vec<(usize, u16, u16, vt100::MouseProtocolMode, vt100::MouseProtocolEncoding, bool)>) {
                        match node {
                            Node::Leaf(p) => {
                                let (mode, enc, alt) = match p.term.lock() {
                                    Ok(term) => {
                                        let screen = term.screen();
                                        (screen.mouse_protocol_mode(), screen.mouse_protocol_encoding(), screen.alternate_screen())
                                    }
                                    Err(_) => {
                                        // Mutex poisoned — reader thread panicked.  Use safe defaults.
                                        (vt100::MouseProtocolMode::None, vt100::MouseProtocolEncoding::Default, false)
                                    }
                                };
                                panes.push((p.id, p.last_cols, p.last_rows, mode, enc, alt));
                            }
                            Node::Split { children, .. } => {
                                for c in children { collect_panes(c, panes); }
                            }
                        }
                    }
                    let mut panes = Vec::new();
                    collect_panes(&win.root, &mut panes);
                    let active_pane_id = crate::tree::get_active_pane_id(&win.root, &win.active_path);
                    for (pos, (id, cols, rows, _mode, _enc, _alt)) in panes.iter().enumerate() {
                        let idx = pos + app.pane_base_index;
                        let active_marker = if active_pane_id == Some(*id) { " (active)" } else { "" };
                        output.push_str(&format!("{}: [{}x{}] [history {}/{}, 0 bytes] %{}{}\n", idx, cols, rows, app.history_limit, app.history_limit, id, active_marker));
                    }
                    let _ = resp.send(output);
                }
                CtrlReq::ListPanesFormat(resp, fmt) => {
                    helpers::propagate_osc_titles(&mut app);
                    let text = format_list_panes(&app, &fmt, app.active_idx);
                    let _ = resp.send(text);
                }
                CtrlReq::ListAllPanes(resp) => {
                    let mut output = String::new();
                    fn collect_all_panes(node: &Node, panes: &mut Vec<(usize, u16, u16)>) {
                        match node {
                            Node::Leaf(p) => { panes.push((p.id, p.last_cols, p.last_rows)); }
                            Node::Split { children, .. } => { for c in children { collect_all_panes(c, panes); } }
                        }
                    }
                    for (wi, win) in app.windows.iter().enumerate() {
                        let mut panes = Vec::new();
                        collect_all_panes(&win.root, &mut panes);
                        for (id, cols, rows) in panes {
                            output.push_str(&format!("{}:{}: %{} [{}x{}]\n", app.session_name, wi + app.window_base_index, id, cols, rows));
                        }
                    }
                    let _ = resp.send(output);
                }
                CtrlReq::ListAllPanesFormat(resp, fmt) => {
                    let mut lines = Vec::new();
                    for wi in 0..app.windows.len() {
                        lines.push(format_list_panes(&app, &fmt, wi));
                    }
                    let _ = resp.send(lines.join("\n"));
                }
                CtrlReq::KillWindow => {
                    if app.windows.len() > 1 {
                        let mut win = app.windows.remove(app.active_idx);
                        kill_all_children(&mut win.root);
                        if app.active_idx >= app.windows.len() { app.active_idx = app.windows.len() - 1; }
                    } else {
                        // Last window: kill all children; reaper will detect empty session and exit
                        kill_all_children(&mut app.windows[0].root);
                    }
                    hook_event = Some("window-closed");
                }
                CtrlReq::KillSession => {
                    // Fire session-closed hook before cleanup
                    if let Some(cmds) = app.hooks.get("session-closed") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    // Remove port/key files FIRST so clients see the session
                    // as gone immediately, then kill processes.
                    let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
                    let regpath = format!("{}\\.psmux\\{}.port", home, app.port_file_base());
                    let keypath = format!("{}\\.psmux\\{}.key", home, app.port_file_base());
                    let _ = std::fs::remove_file(&regpath);
                    let _ = std::fs::remove_file(&keypath);
                    crate::types::shutdown_persistent_streams();
                    // Kill all child processes using a single process snapshot
                    tree::kill_all_children_batch(&mut app.windows);
                    // Kill warm pane's child (process::exit skips Drop)
                    if let Some(mut wp) = app.warm_pane.take() { wp.child.kill().ok(); }
                    // TerminateProcess is synchronous on Windows — processes
                    // are already dead.  Minimal delay for OS handle cleanup.
                    std::thread::sleep(std::time::Duration::from_millis(10));
                    std::process::exit(0);
                }
                CtrlReq::HasSession(resp) => {
                    let _ = resp.send(true);
                }
                CtrlReq::RenameSession(name) => {
                    if let Some(cmds) = app.hooks.get("before-rename-session") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
                    let old_path = format!("{}\\.psmux\\{}.port", home, app.port_file_base());
                    let old_keypath = format!("{}\\.psmux\\{}.key", home, app.port_file_base());
                    // Compute new port file base with socket_name prefix
                    let new_base = if let Some(ref sn) = app.socket_name {
                        format!("{}__{}" , sn, name)
                    } else {
                        name.clone()
                    };
                    let new_path = format!("{}\\.psmux\\{}.port", home, new_base);
                    let new_keypath = format!("{}\\.psmux\\{}.key", home, new_base);
                    if let Some(port) = app.control_port {
                        let _ = std::fs::remove_file(&old_path);
                        let _ = std::fs::write(&new_path, port.to_string());
                        if let Ok(key) = std::fs::read_to_string(&old_keypath) {
                            let _ = std::fs::remove_file(&old_keypath);
                            let _ = std::fs::write(&new_keypath, key);
                        }
                    }
                    app.session_name = name;
                    // Update env so run-shell/hooks from this server target the new name
                    env::set_var("PSMUX_TARGET_SESSION", app.port_file_base());
                    hook_event = Some("after-rename-session");
                }
                CtrlReq::ClaimSession(name, client_cwd, resp) => {
                    // Same as RenameSession but with a synchronous response
                    // so the CLI knows the rename completed before attaching.
                    let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
                    let old_path = format!("{}\\.psmux\\{}.port", home, app.port_file_base());
                    let old_keypath = format!("{}\\.psmux\\{}.key", home, app.port_file_base());
                    let new_base = if let Some(ref sn) = app.socket_name {
                        format!("{}__{}" , sn, name)
                    } else {
                        name.clone()
                    };
                    let new_path = format!("{}\\.psmux\\{}.port", home, new_base);
                    let new_keypath = format!("{}\\.psmux\\{}.key", home, new_base);
                    if let Some(port) = app.control_port {
                        let _ = std::fs::remove_file(&old_path);
                        let _ = std::fs::write(&new_path, port.to_string());
                        if let Ok(key) = std::fs::read_to_string(&old_keypath) {
                            let _ = std::fs::remove_file(&old_keypath);
                            let _ = std::fs::write(&new_keypath, key);
                        }
                    }
                    app.session_name = name;
                    // Update env so run-shell/hooks from this server target the new name
                    env::set_var("PSMUX_TARGET_SESSION", app.port_file_base());
                    // Honour the client's working directory: the warm server
                    // was spawned from a previous session whose CWD may differ
                    // from where the user ran `psmux` now.  Update the
                    // server's CWD (for future pane spawns) and silently
                    // inject `cd` into the active pane so the shell starts
                    // in the right directory.  A clear screen command is
                    // chained after cd so the user never sees the injected
                    // command or its echo.
                    if let Some(ref cwd) = client_cwd {
                        let cwd_path = std::path::Path::new(cwd);
                        if cwd_path.is_dir() {
                            let server_cwd_differs = env::current_dir()
                                .map(|cur| cur != cwd_path)
                                .unwrap_or(true);
                            if server_cwd_differs {
                                env::set_current_dir(cwd_path).ok();
                                // Inject cd + clear into the active pane so
                                // the directory change is invisible to the
                                // user.  Leading space keeps it out of shell
                                // history; the clear wipes visible traces.
                                //
                                // The vt100 parser watches for the CSI 2J
                                // that cls/clear generates, which tells the
                                // layout serialiser the clear finished
                                // (event-driven, no guessing).  A safety
                                // timeout is a fallback for unusual shells.
                                if let Some(win) = app.windows.last_mut() {
                                    if let Some(p) = active_pane_mut(&mut win.root, &win.active_path) {
                                        use std::io::Write as _;
                                        let escaped = cwd.replace('\'', "''");
                                        let clear = if cfg!(windows) { "cls" } else { "clear" };
                                        let cd_cmd = format!(" cd '{}'; {}\r", escaped, clear);
                                        // Tell the vt100 parser to watch for the
                                        // next screen-clear event (CSI 2J/3J).
                                        if let Ok(mut parser) = p.term.lock() {
                                            parser.screen_mut().set_squelch_clear_pending(true);
                                        }
                                        p.squelch_until = Some(Instant::now() + Duration::from_millis(500));
                                        let _ = p.writer.write_all(cd_cmd.as_bytes());
                                        let _ = p.writer.flush();
                                    }
                                }
                            }
                        }
                    }
                    // Update env so run-shell/hooks from this server target the new name
                    env::set_var("PSMUX_TARGET_SESSION", app.port_file_base());
                    // Re-load user config so the claimed session reflects the
                    // current config file.  The warm server loaded config at
                    // its own startup, but the user may have changed their
                    // config since then (or the warm server was spawned by a
                    // different session with a different PSMUX_CONFIG_FILE).
                    app.key_tables.clear();
                    app.defaults_suppressed = false;
                    crate::config::populate_default_bindings(&mut app);
                    load_config(&mut app);
                    // Update shared aliases after config reload
                    if let Ok(mut w) = shared_aliases_main.write() {
                        *w = app.command_aliases.clone();
                    }
                    // Fire client-session-changed hook (warm server claimed by new session)
                    if let Some(cmds) = app.hooks.get("client-session-changed") { let cmds = cmds.clone(); for cmd in &cmds { let _ = execute_command_string(&mut app, cmd); } }
                    meta_dirty = true;
                    state_dirty = true;
                    let _ = resp.send("OK\n".to_string());
                    // Spawn a replacement warm server for the NEXT new-session
                    spawn_warm_server(&app);
                    hook_event = Some("after-rename-session");
                }
                CtrlReq::SwapPane(dir) => {
                    // tmux: swap-pane without -Z permanently unzooms (#82)
                    unzoom_if_zoomed(&mut app);
                    match dir.as_str() {
                        "U" => { swap_pane(&mut app, FocusDir::Up); }
                        "D" => { swap_pane(&mut app, FocusDir::Down); }
                        _ => { swap_pane(&mut app, FocusDir::Down); }
                    }
                    hook_event = Some("after-swap-pane");
                }
                CtrlReq::ResizePane(dir, amount) => {
                    unzoom_if_zoomed(&mut app);
                    match dir.as_str() {
                        "U" | "D" => { resize_pane_vertical(&mut app, if dir == "U" { -(amount as i16) } else { amount as i16 }); }
                        "L" | "R" => { resize_pane_horizontal(&mut app, if dir == "L" { -(amount as i16) } else { amount as i16 }); }
                        _ => {}
                    }
                    resize_all_panes(&mut app); meta_dirty = true;
                    hook_event = Some("after-resize-pane");
                }
                CtrlReq::SetBuffer(content) => {
                    app.paste_buffers.insert(0, content);
                    if app.paste_buffers.len() > 10 { app.paste_buffers.pop(); }
                }
                CtrlReq::SetNamedBuffer(name, content) => {
                    app.named_buffers.insert(name, content);
                }
                CtrlReq::ListBuffers(resp) => {
                    let mut output = String::new();
                    // List auto-named buffers (positional stack)
                    for (i, buf) in app.paste_buffers.iter().enumerate() {
                        let preview: String = buf.chars().take(50).collect();
                        output.push_str(&format!("buffer{}: {} bytes: \"{}\"\n", i, buf.len(), preview));
                    }
                    // List named buffers
                    let mut names: Vec<&String> = app.named_buffers.keys().collect();
                    names.sort();
                    for name in names {
                        let buf = &app.named_buffers[name];
                        let preview: String = buf.chars().take(50).collect();
                        output.push_str(&format!("{}: {} bytes: \"{}\"\n", name, buf.len(), preview));
                    }
                    let _ = resp.send(output);
                }
                CtrlReq::ListBuffersFormat(resp, fmt) => {
                    let mut output = Vec::new();
                    for (i, _buf) in app.paste_buffers.iter().enumerate() {
                        set_buffer_idx_override(Some(i));
                        output.push(expand_format(&fmt, &app));
                        set_buffer_idx_override(None);
                    }
                    // Named buffers with format: use name override
                    let mut names: Vec<String> = app.named_buffers.keys().cloned().collect();
                    names.sort();
                    for name in &names {
                        set_named_buffer_override(Some(name.clone()));
                        output.push(expand_format(&fmt, &app));
                        set_named_buffer_override(None);
                    }
                    let _ = resp.send(output.join("\n"));
                }
                CtrlReq::ShowBuffer(resp) => {
                    let content = app.paste_buffers.first().cloned().unwrap_or_default();
                    let _ = resp.send(content);
                }
                CtrlReq::ShowBufferAt(resp, idx) => {
                    let content = app.paste_buffers.get(idx).cloned().unwrap_or_default();
                    let _ = resp.send(content);
                }
                CtrlReq::ShowNamedBuffer(resp, name) => {
                    let content = app.named_buffers.get(&name).cloned().unwrap_or_default();
                    let _ = resp.send(content);
                }
                CtrlReq::DeleteBuffer => {
                    if !app.paste_buffers.is_empty() { app.paste_buffers.remove(0); }
                }
                CtrlReq::DeleteBufferAt(idx) => {
                    if idx < app.paste_buffers.len() { app.paste_buffers.remove(idx); }
                }
                CtrlReq::DeleteNamedBuffer(name) => {
                    app.named_buffers.remove(&name);
                }
                CtrlReq::PasteBufferAt(idx) => {
                    if idx < app.paste_buffers.len() {
                        let text = app.paste_buffers[idx].clone();
                        let win = &mut app.windows[app.active_idx];
                        if let Some(p) = crate::tree::active_pane_mut(&mut win.root, &win.active_path) {
                            let _ = write!(p.writer, "{}", text);
                        }
                    }
                }
                CtrlReq::DisplayMessage(resp, fmt, target_pane_idx, set_status_bar, duration_ms) => {
                    // Propagate OSC titles so #{pane_title} reflects latest state
                    helpers::propagate_osc_titles(&mut app);
                    let result = if let Some(pane_idx) = target_pane_idx {
                        // -t targeting: evaluate format for the specific pane
                        // using PANE_POS_OVERRIDE so #{pane_active} reflects
                        // the REAL active pane, not the target (#113)
                        crate::format::expand_format_for_pane(&fmt, &app, app.active_idx, pane_idx)
                    } else {
                        expand_format(&fmt, &app)
                    };
                    if set_status_bar {
                        app.status_message = Some((result.clone(), Instant::now(), duration_ms));
                        state_dirty = true;
                    }
                    let _ = resp.send(result);
                }
                CtrlReq::LastWindow => {
                    if app.windows.len() > 1 && app.last_window_idx < app.windows.len() {
                        switch_with_copy_save(&mut app, |app| {
                            let tmp = app.active_idx;
                            app.active_idx = app.last_window_idx;
                            app.last_window_idx = tmp;
                        });
                    }
                    meta_dirty = true;
                    hook_event = Some("after-select-window");
                }
                CtrlReq::LastPane => {
                    switch_with_copy_save(&mut app, |app| {
                        let win = &mut app.windows[app.active_idx];
                        if !app.last_pane_path.is_empty() && path_exists(&win.root, &app.last_pane_path) {
                            let tmp = win.active_path.clone();
                            win.active_path = app.last_pane_path.clone();
                            app.last_pane_path = tmp;
                        } else if !win.active_path.is_empty() {
                            let last = win.active_path.last_mut();
                            if let Some(idx) = last {
                                *idx = (*idx + 1) % 2;
                            }
                        }
                    });
                    meta_dirty = true;
                }
                CtrlReq::RotateWindow(reverse) => {
                    rotate_panes(&mut app, reverse);
                    hook_event = Some("after-rotate-window");
                }
                CtrlReq::DisplayPanes => {
                    app.mode = Mode::PaneChooser { opened_at: std::time::Instant::now() };
                    state_dirty = true;
                }
                CtrlReq::DisplayPaneSelect(digit) => {
                    // User pressed a digit during display-panes overlay: select the matching pane
                    let win = &app.windows[app.active_idx];
                    let mut rects: Vec<(Vec<usize>, ratatui::layout::Rect)> = Vec::new();
                    crate::tree::compute_rects(&win.root, app.last_window_area, &mut rects);
                    for (i, (path, _)) in rects.iter().enumerate() {
                        if i >= 10 { break; }
                        let mapped = (i + app.pane_base_index) % 10;
                        if mapped == digit {
                            let new_path = path.clone();
                            let old_path = app.windows[app.active_idx].active_path.clone();
                            app.windows[app.active_idx].active_path = new_path;
                            if app.windows[app.active_idx].active_path != old_path {
                                app.last_pane_path = old_path;
                            }
                            break;
                        }
                    }
                    app.mode = Mode::Passthrough;
                    state_dirty = true;
                    meta_dirty = true;
                }
                CtrlReq::BreakPane => {
                    unzoom_if_zoomed(&mut app);
                    break_pane_to_window(&mut app);
                    hook_event = Some("after-break-pane");
                    meta_dirty = true;
                }
                CtrlReq::JoinPane { src_win, src_pane, target_win, target_pane, horizontal }
                | CtrlReq::MovePane { src_win, src_pane, target_win, target_pane, horizontal } => {
                    unzoom_if_zoomed(&mut app);
                    // Resolve source window index (default: active window)
                    let src_idx = src_win.unwrap_or(app.active_idx);
                    // Resolve target window index (default: active window, but must differ from source)
                    let raw_target_win = target_win.unwrap_or(app.active_idx);
                    if src_idx < app.windows.len() && raw_target_win < app.windows.len() && src_idx != raw_target_win {
                        // Resolve source pane path within source window
                        let src_path = if let Some(pidx) = src_pane {
                            // Get Nth pane path in DFS order
                            let mut leaves = Vec::new();
                            tree::collect_leaf_paths_pub(&app.windows[src_idx].root, &mut Vec::new(), &mut leaves);
                            if let Some((_, p)) = leaves.get(pidx) {
                                p.clone()
                            } else {
                                app.windows[src_idx].active_path.clone()
                            }
                        } else {
                            app.windows[src_idx].active_path.clone()
                        };
                        // Unzoom source window if needed
                        if let Some(saved) = app.windows[src_idx].zoom_saved.take() {
                            let win = &mut app.windows[src_idx];
                            for (p, sz) in saved.into_iter() {
                                if let Some(Node::Split { sizes, .. }) = crate::tree::get_split_mut(&mut win.root, &p) { *sizes = sz; }
                            }
                        }
                        let src_root = std::mem::replace(&mut app.windows[src_idx].root,
                            Node::Split { kind: LayoutKind::Horizontal, sizes: vec![], children: vec![] });
                        let (remaining, extracted) = tree::extract_node(src_root, &src_path);
                        if let Some(pane_node) = extracted {
                            let src_empty = remaining.is_none();
                            if let Some(rem) = remaining {
                                app.windows[src_idx].root = rem;
                                app.windows[src_idx].active_path = tree::first_leaf_path(&app.windows[src_idx].root);
                            }
                            // Adjust target index if source window will be removed and target is after it
                            let tgt = if src_empty && raw_target_win > src_idx { raw_target_win - 1 } else { raw_target_win };
                            if src_empty {
                                app.windows.remove(src_idx);
                                if app.active_idx >= app.windows.len() {
                                    app.active_idx = app.windows.len().saturating_sub(1);
                                }
                            }
                            // Graft pane into target window
                            if tgt < app.windows.len() {
                                // Resolve target pane path
                                let tgt_path = if let Some(tpidx) = target_pane {
                                    let mut leaves = Vec::new();
                                    tree::collect_leaf_paths_pub(&app.windows[tgt].root, &mut Vec::new(), &mut leaves);
                                    if let Some((_, p)) = leaves.get(tpidx) {
                                        p.clone()
                                    } else {
                                        app.windows[tgt].active_path.clone()
                                    }
                                } else {
                                    app.windows[tgt].active_path.clone()
                                };
                                let split_kind = if horizontal { LayoutKind::Horizontal } else { LayoutKind::Vertical };
                                tree::replace_leaf_with_split(&mut app.windows[tgt].root, &tgt_path, split_kind, pane_node);
                                app.active_idx = tgt;
                            }
                            resize_all_panes(&mut app);
                            meta_dirty = true;
                            hook_event = Some("after-join-pane");
                        } else {
                            // Extraction failed — restore
                            if let Some(rem) = remaining {
                                app.windows[src_idx].root = rem;
                            }
                        }
                    }
                }
                // ── Cross-session pane forwarding ───────────────────────
                CtrlReq::PaneForwardExtract(win_idx, pane_idx, resp) => {
                    crate::cross_session_server::handle_pane_forward_extract(&mut app, win_idx, pane_idx, resp);
                    resize_all_panes(&mut app);
                    meta_dirty = true;
                }
                CtrlReq::PaneForwardInject {
                    source_session, source_addr, source_key,
                    forward_id, fwd_port, pid, title, rows, cols,
                    screen_b64, target_win, target_pane, horizontal,
                } => {
                    crate::cross_session_server::handle_pane_forward_inject(
                        &mut app, source_session, source_addr, source_key,
                        forward_id, fwd_port, pid, title, rows, cols,
                        screen_b64, target_win, target_pane, horizontal,
                    );
                    resize_all_panes(&mut app);
                    meta_dirty = true;
                    hook_event = Some("after-join-pane");
                }
                CtrlReq::PaneForwardResize(fwd_id, fwd_rows, fwd_cols) => {
                    if let Some(fp) = app.forwarded_panes.get(&fwd_id) {
                        let _ = fp.master.resize(portable_pty::PtySize {
                            rows: fwd_rows, cols: fwd_cols, pixel_width: 0, pixel_height: 0,
                        });
                    }
                }
                CtrlReq::PaneForwardStatus(fwd_id, resp) => {
                    let status = if let Some(fp) = app.forwarded_panes.get_mut(&fwd_id) {
                        match fp.child.try_wait() {
                            Ok(Some(_)) => "exited".to_string(),
                            Ok(None) => "running".to_string(),
                            Err(_) => "exited".to_string(),
                        }
                    } else {
                        "exited".to_string()
                    };
                    let _ = resp.send(status);
                }
                CtrlReq::PaneForwardKill(fwd_id) => {
                    if let Some(mut fp) = app.forwarded_panes.remove(&fwd_id) {
                        fp.shutdown.store(true, std::sync::atomic::Ordering::Relaxed);
                        let _ = fp.child.kill();
                    }
                }
                CtrlReq::RespawnPane(workdir, kill) => {
                    respawn_active_pane(&mut app, Some(&*pty_system), workdir.as_deref(), kill)?;
                    hook_event = Some("after-respawn-pane");
                }
                CtrlReq::BindKey(table_name, key, command, repeat) => {
                    if let Some(kc) = parse_key_string(&key) {
                        let kc = normalize_key_for_binding(kc);
                        // Support `\;` chaining in server-side bind-key
                        let sub_cmds = crate::config::split_chained_commands_pub(&command);
                        let action = if sub_cmds.len() > 1 {
                            Some(Action::CommandChain(sub_cmds))
                        } else {
                            parse_command_to_action(&command)
                        };
                        if let Some(act) = action {
                            let table = app.key_tables.entry(table_name).or_default();
                            table.retain(|b| b.key != kc);
                            table.push(Bind { key: kc, action: act, repeat });
                        }
                    }
                    meta_dirty = true;
                    state_dirty = true;
                }
                CtrlReq::UnbindKey(key, table) => {
                    if let Some(kc) = parse_key_string(&key) {
                        let kc = normalize_key_for_binding(kc);
                        let target = table.unwrap_or_else(|| "prefix".to_string());
                        if let Some(binds) = app.key_tables.get_mut(&target) {
                            binds.retain(|b| b.key != kc);
                        }
                    }
                    meta_dirty = true;
                    state_dirty = true;
                }
                CtrlReq::UnbindAll => {
                    app.key_tables.clear();
                    app.defaults_suppressed = true;
                    meta_dirty = true;
                    state_dirty = true;
                }
                CtrlReq::UnbindAllInTable(table) => {
                    if let Some(binds) = app.key_tables.get_mut(&table) {
                        binds.clear();
                    }
                    meta_dirty = true;
                    state_dirty = true;
                }
                CtrlReq::ListKeys(resp) => {
                    // Build list-keys output from the canonical help module
                    let user_iter = app.key_tables.iter().flat_map(|(table_name, binds)| {
                        binds.iter().map(move |bind| {
                            let key_str = format_key_binding(&bind.key);
                            let action_str = format_action(&bind.action);
                            (table_name.as_str(), key_str, action_str, bind.repeat)
                        })
                    });
                    let output = help::build_list_keys_output(user_iter, app.defaults_suppressed);
                    let _ = resp.send(output);
                }
                CtrlReq::SetOption(option, value) => {
                    apply_set_option(&mut app, &option, &value, false);
                    app.user_set_options.insert(option.clone());
                    // Update shared aliases if command-alias changed
                    if option == "command-alias" {
                        if let Ok(mut map) = shared_aliases_main.write() {
                            *map = app.command_aliases.clone();
                        }
                    }
                    meta_dirty = true;
                    state_dirty = true;
                }
                CtrlReq::SetOptionQuiet(option, value, quiet) => {
                    let old_shell = app.default_shell.clone();
                    apply_set_option(&mut app, &option, &value, quiet);
                    app.user_set_options.insert(option.clone());
                    // If default-shell changed, kill the warm pane so the next
                    // new-window spawns the correct shell (fixes #99).
                    if app.default_shell != old_shell {
                        if let Some(mut wp) = app.warm_pane.take() {
                            wp.child.kill().ok();
                        }
                    }
                    // Update shared aliases if command-alias changed
                    if option == "command-alias" {
                        if let Ok(mut map) = shared_aliases_main.write() {
                            *map = app.command_aliases.clone();
                        }
                    }
                    meta_dirty = true;
                    state_dirty = true;
                }
                CtrlReq::SetOptionUnset(option) => {
                    // Reset option to default or remove @user-option
                    if option.starts_with('@') {
                        app.user_options.remove(&option);
                    } else {
                        match option.as_str() {
                            "status-left" => { app.status_left = "psmux:#I".to_string(); }
                            "status-right" => { app.status_right = "#{?window_bigger,[#{window_offset_x}#,#{window_offset_y}] ,}\"#{=21:pane_title}\" %H:%M %d-%b-%y".to_string(); }
                            "mouse" => { app.mouse_enabled = true; }
                            "scroll-enter-copy-mode" => { app.scroll_enter_copy_mode = true; }
                            "pwsh-mouse-selection" => { app.pwsh_mouse_selection = false; }
                            "mouse-selection" => { app.mouse_selection = true; }
                            "paste-detection" => { app.paste_detection = true; }
                            "choose-tree-preview" => { app.choose_tree_preview = false; }
                            "escape-time" => { app.escape_time_ms = 500; }
                            "history-limit" => { app.history_limit = 2000; }
                            "display-time" => { app.display_time_ms = 750; }
                            "mode-keys" => { app.mode_keys = "emacs".to_string(); }
                            "status" => { app.status_visible = true; }
                            "status-position" => { app.status_position = "bottom".to_string(); }
                            "status-style" => { app.status_style = String::new(); }
                            "renumber-windows" => { app.renumber_windows = false; }
                            "remain-on-exit" => { app.remain_on_exit = false; }
                            "destroy-unattached" => { app.destroy_unattached = false; }
                            "exit-empty" => { app.exit_empty = true; }
                            "automatic-rename" => { app.automatic_rename = true; }
                            "pane-border-style" => { app.pane_border_style = String::new(); }
                            "pane-active-border-style" => { app.pane_active_border_style = "fg=green".to_string(); }
                            "pane-border-hover-style" => { app.pane_border_hover_style = "fg=yellow".to_string(); }
                            "window-status-format" => { app.window_status_format = "#I:#W#{?window_flags,#{window_flags}, }".to_string(); }
                            "window-status-current-format" => { app.window_status_current_format = "#I:#W#{?window_flags,#{window_flags}, }".to_string(); }
                            "window-status-separator" => { app.window_status_separator = " ".to_string(); }
                            "cursor-style" => { std::env::set_var("PSMUX_CURSOR_STYLE", "bar"); }
                            "cursor-blink" => { std::env::set_var("PSMUX_CURSOR_BLINK", "1"); }
                            _ => {}
                        }
                    }
                }
                CtrlReq::SetOptionAppend(option, value) => {
                    // Append to existing option value
                    if option.starts_with('@') {
                        let existing = app.user_options.get(&option).cloned().unwrap_or_default();
                        app.user_options.insert(option, format!("{}{}", existing, value));
                    } else {
                        match option.as_str() {
                            "status-left" => { app.status_left.push_str(&value); }
                            "status-right" => { app.status_right.push_str(&value); }
                            "status-style" => { app.status_style.push_str(&value); }
                            "pane-border-style" => { app.pane_border_style.push_str(&value); }
                            "pane-active-border-style" => { app.pane_active_border_style.push_str(&value); }
                            "pane-border-hover-style" => { app.pane_border_hover_style.push_str(&value); }
                            "window-status-format" => { app.window_status_format.push_str(&value); }
                            "window-status-current-format" => { app.window_status_current_format.push_str(&value); }
                            _ => {}
                        }
                    }
                }
                CtrlReq::SetOptionOnlyIfUnset(option, value) => {
                    let already_set = if option.starts_with('@') {
                        app.user_options.contains_key(&option)
                    } else {
                        app.user_set_options.contains(&option)
                    };
                    if !already_set {
                        apply_set_option(&mut app, &option, &value, false);
                        app.user_set_options.insert(option.clone());
                        if option == "command-alias" {
                            if let Ok(mut map) = shared_aliases_main.write() {
                                *map = app.command_aliases.clone();
                            }
                        }
                        meta_dirty = true;
                        state_dirty = true;
                    }
                }
                CtrlReq::ShowOptions(resp) => {
                    let mut output = String::new();
                    output.push_str(&format!("prefix {}\n", format_key_binding(&app.prefix_key)));
                    if let Some(ref p2) = app.prefix2_key {
                        output.push_str(&format!("prefix2 {}\n", format_key_binding(p2)));
                    }
                    output.push_str(&format!("base-index {}\n", app.window_base_index));
                    output.push_str(&format!("pane-base-index {}\n", app.pane_base_index));
                    output.push_str(&format!("escape-time {}\n", app.escape_time_ms));
                    output.push_str(&format!("mouse {}\n", if app.mouse_enabled { "on" } else { "off" }));
                    output.push_str(&format!("scroll-enter-copy-mode {}\n", if app.scroll_enter_copy_mode { "on" } else { "off" }));
                    output.push_str(&format!("pwsh-mouse-selection {}\n", if app.pwsh_mouse_selection { "on" } else { "off" }));
                    output.push_str(&format!("mouse-selection {}\n", if app.mouse_selection { "on" } else { "off" }));
                    output.push_str(&format!("paste-detection {}\n", if app.paste_detection { "on" } else { "off" }));
                    output.push_str(&format!("choose-tree-preview {}\n", if app.choose_tree_preview { "on" } else { "off" }));
                    output.push_str(&format!("status {}\n", if app.status_visible { "on" } else { "off" }));
                    output.push_str(&format!("status-position {}\n", app.status_position));
                    output.push_str(&format!("status-left \"{}\"\n", app.status_left));
                    output.push_str(&format!("status-right \"{}\"\n", app.status_right));
                    output.push_str(&format!("history-limit {}\n", app.history_limit));
                    output.push_str(&format!("display-time {}\n", app.display_time_ms));
                    output.push_str(&format!("display-panes-time {}\n", app.display_panes_time_ms));
                    output.push_str(&format!("mode-keys {}\n", app.mode_keys));
                    output.push_str(&format!("focus-events {}\n", if app.focus_events { "on" } else { "off" }));
                    output.push_str(&format!("renumber-windows {}\n", if app.renumber_windows { "on" } else { "off" }));
                    output.push_str(&format!("automatic-rename {}\n", if app.automatic_rename { "on" } else { "off" }));
                    output.push_str(&format!("monitor-activity {}\n", if app.monitor_activity { "on" } else { "off" }));
                    output.push_str(&format!("synchronize-panes {}\n", if app.sync_input { "on" } else { "off" }));
                    output.push_str(&format!("remain-on-exit {}\n", if app.remain_on_exit { "on" } else { "off" }));
                    output.push_str(&format!("destroy-unattached {}\n", if app.destroy_unattached { "on" } else { "off" }));
                    output.push_str(&format!("exit-empty {}\n", if app.exit_empty { "on" } else { "off" }));
                    output.push_str(&format!("set-titles {}\n", if app.set_titles { "on" } else { "off" }));
                    if !app.set_titles_string.is_empty() {
                        output.push_str(&format!("set-titles-string \"{}\"\n", app.set_titles_string));
                    }
                    output.push_str(&format!(
                        "prediction-dimming {}\n",
                        if app.prediction_dimming { "on" } else { "off" }
                    ));
                    output.push_str(&format!("allow-predictions {}\n", if app.allow_predictions { "on" } else { "off" }));
                    output.push_str(&format!("cursor-style {}\n", std::env::var("PSMUX_CURSOR_STYLE").unwrap_or_else(|_| "bar".to_string())));
                    output.push_str(&format!("cursor-blink {}\n", if std::env::var("PSMUX_CURSOR_BLINK").unwrap_or_else(|_| "1".to_string()) != "0" { "on" } else { "off" }));
                    {
                        let shell_val = if app.default_shell.is_empty() {
                            crate::pane::cached_shell().unwrap_or("pwsh.exe").to_string()
                        } else {
                            app.default_shell.clone()
                        };
                        output.push_str(&format!("default-shell {}\n", shell_val));
                    }
                    output.push_str(&format!("word-separators \"{}\"\n", app.word_separators));
                    if !app.pane_border_style.is_empty() {
                        output.push_str(&format!("pane-border-style \"{}\"\n", app.pane_border_style));
                    }
                    if !app.pane_active_border_style.is_empty() {
                        output.push_str(&format!("pane-active-border-style \"{}\"\n", app.pane_active_border_style));
                    }
                    if !app.pane_border_hover_style.is_empty() {
                        output.push_str(&format!("pane-border-hover-style \"{}\"\n", app.pane_border_hover_style));
                    }
                    if !app.status_style.is_empty() {
                        output.push_str(&format!("status-style \"{}\"\n", app.status_style));
                    }
                    if !app.status_left_style.is_empty() {
                        output.push_str(&format!("status-left-style \"{}\"\n", app.status_left_style));
                    }
                    if !app.status_right_style.is_empty() {
                        output.push_str(&format!("status-right-style \"{}\"\n", app.status_right_style));
                    }
                    output.push_str(&format!("status-interval {}\n", app.status_interval));
                    output.push_str(&format!("status-justify {}\n", app.status_justify));
                    output.push_str(&format!("window-status-format \"{}\"\n", app.window_status_format));
                    output.push_str(&format!("window-status-current-format \"{}\"\n", app.window_status_current_format));
                    if !app.window_status_style.is_empty() {
                        output.push_str(&format!("window-status-style \"{}\"\n", app.window_status_style));
                    }
                    if !app.window_status_current_style.is_empty() {
                        output.push_str(&format!("window-status-current-style \"{}\"\n", app.window_status_current_style));
                    }
                    if !app.window_status_activity_style.is_empty() {
                        output.push_str(&format!("window-status-activity-style \"{}\"\n", app.window_status_activity_style));
                    }
                    if !app.message_style.is_empty() {
                        output.push_str(&format!("message-style \"{}\"\n", app.message_style));
                    }
                    if !app.message_command_style.is_empty() {
                        output.push_str(&format!("message-command-style \"{}\"\n", app.message_command_style));
                    }
                    if !app.mode_style.is_empty() {
                        output.push_str(&format!("mode-style \"{}\"\n", app.mode_style));
                    }
                    // Include @user-options (used by plugins)
                    for (key, val) in &app.user_options {
                        output.push_str(&format!("{} \"{}\"\n", key, val));
                    }
                    // New options
                    output.push_str(&format!("main-pane-width {}\n", app.main_pane_width));
                    output.push_str(&format!("main-pane-height {}\n", app.main_pane_height));
                    output.push_str(&format!("status-left-length {}\n", app.status_left_length));
                    output.push_str(&format!("status-right-length {}\n", app.status_right_length));
                    output.push_str(&format!("window-size {}\n", app.window_size));
                    output.push_str(&format!("allow-passthrough {}\n", app.allow_passthrough));
                    output.push_str(&format!("set-clipboard {}\n", app.set_clipboard));
                    if !app.copy_command.is_empty() {
                        output.push_str(&format!("copy-command \"{}\"\n", app.copy_command));
                    }
                    output.push_str(&format!("allow-rename {}\n", if app.allow_rename { "on" } else { "off" }));
                    output.push_str(&format!("allow-set-title {}\n", if app.allow_set_title { "on" } else { "off" }));
                    output.push_str(&format!("bell-action {}\n", app.bell_action));
                    output.push_str(&format!("activity-action {}\n", app.activity_action));
                    output.push_str(&format!("silence-action {}\n", app.silence_action));
                    output.push_str(&format!("update-environment \"{}\"\n", app.update_environment.join(" ")));
                    if let Some(ref group) = app.session_group {
                        output.push_str(&format!("session-group \"{}\"\n", group));
                    }
                    for (alias, expansion) in &app.command_aliases {
                        output.push_str(&format!("command-alias \"{}={}\"\n", alias, expansion));
                    }
                    let _ = resp.send(output);
                }
                CtrlReq::SourceFile(path) => {
                    // Reset binding state so config reload gets a clean slate.
                    // If the config has unbind-key -a, it will re-set the flag.
                    app.defaults_suppressed = false;
                    app.key_tables.clear();
                    crate::config::populate_default_bindings(&mut app);
                    // Use config helper for standard source-file behavior (-F support,
                    // nested parse context). Keep direct glob handling for wildcard sources.
                    let is_format_expand = path.starts_with("-F ") || path.starts_with("-F\t");
                    let path_for_glob = if is_format_expand { path[3..].trim() } else { &path };
                    if !is_format_expand && (path_for_glob.contains('*') || path_for_glob.contains('?')) {
                        let expanded = if path_for_glob.starts_with('~') {
                            let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
                            path_for_glob.replacen('~', &home, 1)
                        } else {
                            path_for_glob.to_string()
                        };
                        if let Ok(entries) = glob::glob(&expanded) {
                            for entry in entries.flatten() {
                                if let Ok(contents) = std::fs::read_to_string(&entry) {
                                    parse_config_content(&mut app, &contents);
                                }
                            }
                        }
                    } else {
                        crate::config::source_file(&mut app, &path);
                    }
                    // Mark dirty so the client receives updated config
                    // (status bar, bindings, styles, etc.) on the next
                    // dump-state instead of getting an NC fast-path reply.
                    state_dirty = true;
                    meta_dirty = true;
                }
                CtrlReq::MoveWindow(target) => {
                    if let Some(t) = target {
                        if t < app.windows.len() && app.active_idx != t {
                            let win = app.windows.remove(app.active_idx);
                            let insert_idx = if t > app.active_idx { t - 1 } else { t };
                            app.windows.insert(insert_idx.min(app.windows.len()), win);
                            app.active_idx = insert_idx.min(app.windows.len() - 1);
                        }
                    }
                }
                CtrlReq::SwapWindow(target) => {
                    if target < app.windows.len() && app.active_idx != target {
                        app.windows.swap(app.active_idx, target);
                    }
                }
                CtrlReq::LinkWindow(src_idx_opt, dst_idx_opt) => {
                    // link-window: within a single session, create a linked window
                    // referencing the source window. Since PTY handles can't be shared
                    // across windows, this spawns a new shell and marks it as linked.
                    let src = src_idx_opt.unwrap_or(app.active_idx);
                    if src < app.windows.len() {
                        let src_id = app.windows[src].id;
                        let src_name = app.windows[src].name.clone();
                        let dst = dst_idx_opt.unwrap_or(app.windows.len());
                        let pty_system = portable_pty::native_pty_system();
                        match crate::pane::create_window(&*pty_system, &mut app, None, None) {
                            Ok(()) => {
                                let new_idx = app.windows.len() - 1;
                                app.windows[new_idx].linked_from = Some(src_id);
                                app.windows[new_idx].name = src_name;
                                if dst < new_idx {
                                    let win = app.windows.remove(new_idx);
                                    app.windows.insert(dst, win);
                                    if app.active_idx > dst && app.active_idx <= new_idx {
                                        app.active_idx = app.active_idx.saturating_sub(1);
                                    }
                                }
                                resize_all_panes(&mut app);
                                meta_dirty = true;
                                hook_event = Some("window-linked");
                            }
                            Err(_e) => {
                                app.status_message = Some(("link-window: failed to create linked window".to_string(), std::time::Instant::now(), None));
                            }
                        }
                    } else {
                        app.status_message = Some(("link-window: source window not found".to_string(), std::time::Instant::now(), None));
                    }
                    state_dirty = true;
                }
                CtrlReq::UnlinkWindow => {
                    if app.windows.len() > 1 {
                        let mut win = app.windows.remove(app.active_idx);
                        kill_all_children(&mut win.root);
                        if app.active_idx >= app.windows.len() {
                            app.active_idx = app.windows.len() - 1;
                        }
                        resize_all_panes(&mut app);
                        meta_dirty = true;
                        hook_event = Some("window-unlinked");
                    }
                }
                CtrlReq::SetSessionGroup(group_name) => {
                    app.session_group = Some(group_name);
                    state_dirty = true;
                }
                CtrlReq::FindWindow(resp, pattern) => {
                    let mut output = String::new();
                    for (i, win) in app.windows.iter().enumerate() {
                        if win.name.contains(&pattern) {
                            output.push_str(&format!("{}: {} []\n", i + app.window_base_index, win.name));
                        }
                    }
                    let _ = resp.send(output);
                }
                CtrlReq::PipePane(cmd, stdin, stdout, toggle) => {
                    let win = &app.windows[app.active_idx];
                    let pane_id = get_active_pane_id(&win.root, &win.active_path).unwrap_or(0);
                    let has_existing = app.pipe_panes.iter().any(|p| p.pane_id == pane_id);
                    
                    if cmd.is_empty() {
                        // No command: close any existing pipe on this pane
                        if let Some(idx) = app.pipe_panes.iter().position(|p| p.pane_id == pane_id) {
                            if let Some(ref mut proc) = app.pipe_panes[idx].process {
                                let _ = proc.kill();
                            }
                            app.pipe_panes.remove(idx);
                        }
                    } else if toggle && has_existing {
                        // -o flag with existing pipe: close it (toggle off), don't start new
                        if let Some(idx) = app.pipe_panes.iter().position(|p| p.pane_id == pane_id) {
                            if let Some(ref mut proc) = app.pipe_panes[idx].process {
                                let _ = proc.kill();
                            }
                            app.pipe_panes.remove(idx);
                        }
                    } else {
                        // Close any existing pipe first (replace)
                        if let Some(idx) = app.pipe_panes.iter().position(|p| p.pane_id == pane_id) {
                            if let Some(ref mut proc) = app.pipe_panes[idx].process {
                                let _ = proc.kill();
                            }
                            app.pipe_panes.remove(idx);
                        }
                        // Start new pipe
                        let (shell_prog, shell_args) = crate::commands::resolve_run_shell();
                        let process = {
                            let mut c = std::process::Command::new(&shell_prog);
                            for a in &shell_args { c.arg(a); }
                            c.arg(&cmd);
                            c.stdin(if stdout { std::process::Stdio::piped() } else { std::process::Stdio::null() });
                            c.stdout(if stdin { std::process::Stdio::piped() } else { std::process::Stdio::null() });
                            c.stderr(std::process::Stdio::null());
                            { use crate::platform::HideWindowCommandExt; c.hide_window(); }
                            c.spawn().ok()
                        };
                        
                        app.pipe_panes.push(PipePaneState {
                            pane_id,
                            process,
                            stdin,
                            stdout,
                        });
                    }
                }
                CtrlReq::SelectLayout(layout) => {
                    unzoom_if_zoomed(&mut app);
                    apply_layout(&mut app, &layout);
                    resize_all_panes(&mut app);
                    meta_dirty = true;
                    state_dirty = true;
                }
                CtrlReq::NextLayout => {
                    unzoom_if_zoomed(&mut app);
                    cycle_layout(&mut app);
                    resize_all_panes(&mut app);
                    meta_dirty = true;
                    state_dirty = true;
                }
                CtrlReq::ListClients(resp) => {
                    let mut output = String::new();
                    if app.client_registry.is_empty() {
                        // Fallback for backward compat when no clients registered yet
                        output.push_str(&format!("/dev/pts/0: {}: {} [{}x{}] (utf8)\n", 
                            app.session_name, 
                            app.windows[app.active_idx].name,
                            app.last_window_area.width,
                            app.last_window_area.height
                        ));
                    } else {
                        let mut clients: Vec<&crate::types::ClientInfo> = app.client_registry.values().collect();
                        clients.sort_by_key(|c| c.id);
                        for ci in &clients {
                            let activity_secs = ci.last_activity.elapsed().as_secs();
                            let kind = if ci.is_control { " (control mode)" } else { "" };
                            output.push_str(&format!("{}: {}: {} [{}x{}] (utf8){} [activity={}s ago]\n",
                                ci.tty_name,
                                app.session_name,
                                app.windows[app.active_idx].name,
                                ci.width, ci.height,
                                kind,
                                activity_secs,
                            ));
                        }
                    }
                    let _ = resp.send(output);
                }
                CtrlReq::ListClientsFormat(resp, fmt) => {
                    let mut output = String::new();
                    let mut clients: Vec<&crate::types::ClientInfo> = app.client_registry.values().collect();
                    clients.sort_by_key(|c| c.id);
                    for ci in &clients {
                        let activity_secs = ci.last_activity.elapsed().as_secs();
                        let line = fmt
                            .replace("#{client_name}", &ci.tty_name)
                            .replace("#{client_tty}", &ci.tty_name)
                            .replace("#{client_width}", &ci.width.to_string())
                            .replace("#{client_height}", &ci.height.to_string())
                            .replace("#{client_activity}", &activity_secs.to_string())
                            .replace("#{client_session}", &app.session_name)
                            .replace("#{session_name}", &app.session_name)
                            .replace("#{client_control_mode}", if ci.is_control { "1" } else { "0" });
                        output.push_str(&line);
                        output.push('\n');
                    }
                    let _ = resp.send(output);
                }
                CtrlReq::ForceDetachClient(target_cid) => {
                    // Force-detach a specific client by shutting down its TCP stream
                    app.client_sizes.remove(&target_cid);
                    let was_present = app.client_registry.remove(&target_cid).is_some();
                    if was_present {
                        app.attached_clients = app.attached_clients.saturating_sub(1);
                    }
                    if app.latest_client_id == Some(target_cid) {
                        app.latest_client_id = app.client_registry.keys().max().copied();
                    }
                    // Shut down the TCP stream to force disconnect
                    crate::types::shutdown_client_stream(target_cid);
                    // Recompute effective size from remaining clients
                    if let Some((w, h)) = compute_effective_client_size(&app) {
                        app.last_window_area = Rect { x: 0, y: 0, width: w, height: h };
                        resize_all_panes(&mut app);
                    }
                    // Fire detach notification
                    control::emit_notification(&app, crate::types::ControlNotification::ClientDetached {
                        client: format!("/dev/pts/{}", target_cid),
                    });
                    hook_event = Some("client-detached");
                    if app.attached_clients == 0 && app.destroy_unattached {
                        let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
                        let regpath = format!("{}\\.psmux\\{}.port", home, app.port_file_base());
                        let keypath = format!("{}\\.psmux\\{}.key", home, app.port_file_base());
                        let _ = std::fs::remove_file(&regpath);
                        let _ = std::fs::remove_file(&keypath);
                        crate::types::shutdown_persistent_streams();
                        tree::kill_all_children_batch(&mut app.windows);
                        if let Some(mut wp) = app.warm_pane.take() {
                            wp.child.kill().ok();
                        }
                        std::thread::sleep(std::time::Duration::from_millis(10));
                        std::process::exit(0);
                    }
                }
                CtrlReq::SwitchClient(target, flag) => {
                    // Resolve the target session name based on the flag
                    let current = app.port_file_base();
                    let all_sessions = crate::session::list_session_names();
                    let resolved = match flag {
                        't' => {
                            // Direct target: validate it exists
                            if target.is_empty() {
                                None
                            } else if all_sessions.contains(&target) {
                                Some(target.clone())
                            } else {
                                // Try partial match (prefix)
                                all_sessions.iter().find(|s| s.starts_with(&target)).cloned()
                            }
                        }
                        'n' => {
                            // Next session (alphabetically after current)
                            let pos = all_sessions.iter().position(|s| s == &current);
                            match pos {
                                Some(i) if i + 1 < all_sessions.len() => Some(all_sessions[i + 1].clone()),
                                Some(_) => all_sessions.first().cloned(), // wrap around
                                None => all_sessions.first().cloned(),
                            }
                        }
                        'p' => {
                            // Previous session (alphabetically before current)
                            let pos = all_sessions.iter().position(|s| s == &current);
                            match pos {
                                Some(0) => all_sessions.last().cloned(), // wrap around
                                Some(i) => Some(all_sessions[i - 1].clone()),
                                None => all_sessions.last().cloned(),
                            }
                        }
                        'l' => {
                            // Last session (read from last_session file)
                            let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
                            let last_path = format!("{}\\.psmux\\last_session", home);
                            std::fs::read_to_string(&last_path).ok()
                                .map(|s| s.trim().to_string())
                                .filter(|s| !s.is_empty() && s != &current && all_sessions.contains(s))
                        }
                        _ => None,
                    };
                    match resolved {
                        Some(ref sess) if sess != &current => {
                            // Signal the attached client to switch by sending a directive
                            if let Some(cid) = app.latest_client_id {
                                crate::types::send_directive_to_client(cid, &format!("SWITCH {}", sess));
                            } else {
                                // No specific client ID, send to all attached clients
                                crate::types::send_directive_to_all_clients(&format!("SWITCH {}", sess));
                            }
                        }
                        Some(_) => {
                            // Target is the same as current session
                            app.status_message = Some(("switch-client: already on that session".to_string(), std::time::Instant::now(), None));
                            state_dirty = true;
                        }
                        None => {
                            if flag == 't' && !target.is_empty() {
                                app.status_message = Some((format!("switch-client: session not found: {}", target), std::time::Instant::now(), None));
                            } else if flag == 'l' {
                                app.status_message = Some(("switch-client: no last session".to_string(), std::time::Instant::now(), None));
                            } else if all_sessions.len() <= 1 {
                                app.status_message = Some(("switch-client: only one session available".to_string(), std::time::Instant::now(), None));
                            } else {
                                app.status_message = Some(("switch-client: no target session".to_string(), std::time::Instant::now(), None));
                            }
                            state_dirty = true;
                        }
                    }
                }
                CtrlReq::SwitchClientTable(table) => {
                    app.current_key_table = Some(table);
                    state_dirty = true;
                }
                CtrlReq::ListCommands(resp) => {
                    let cmds = TMUX_COMMANDS.join("\n");
                    let _ = resp.send(cmds);
                }
                CtrlReq::LockClient => {
                    app.status_message = Some(("lock: not available on Windows".to_string(), std::time::Instant::now(), None));
                    state_dirty = true;
                }
                CtrlReq::RefreshClient => { state_dirty = true; meta_dirty = true; }
                CtrlReq::SuspendClient => {
                    app.status_message = Some(("suspend: not available on Windows".to_string(), std::time::Instant::now(), None));
                    state_dirty = true;
                }
                CtrlReq::CopyModePageUp => {
                    enter_copy_mode(&mut app);
                    move_copy_cursor(&mut app, 0, -20);
                }
                CtrlReq::ClearHistory => {
                    let win = &mut app.windows[app.active_idx];
                    if let Some(p) = active_pane_mut(&mut win.root, &win.active_path) {
                        if let Ok(mut parser) = p.term.lock() {
                            *parser = vt100::Parser::new(p.last_rows, p.last_cols, app.history_limit);
                        }
                    }
                }
                CtrlReq::SaveBuffer(path) => {
                    if let Some(content) = app.paste_buffers.first() {
                        let _ = std::fs::write(&path, content);
                    }
                }
                CtrlReq::LoadBuffer(path) => {
                    if let Ok(content) = std::fs::read_to_string(&path) {
                        app.paste_buffers.insert(0, content);
                        if app.paste_buffers.len() > 10 {
                            app.paste_buffers.pop();
                        }
                    }
                }
                CtrlReq::SetEnvironment(key, value) => {
                    app.environment.insert(key.clone(), value.clone());
                    env::set_var(&key, &value);
                    // Kill the warm pane and respawn so it picks up the new
                    // env var at process level — avoids PTY echo (#137).
                    if app.warm_pane.is_some() {
                        if let Some(mut old_wp) = app.warm_pane.take() {
                            old_wp.child.kill().ok();
                        }
                        match spawn_warm_pane(&*pty_system, &mut app) {
                            Ok(new_wp) => { app.warm_pane = Some(new_wp); }
                            Err(e) => { eprintln!("psmux: warm pane respawn (SetEnv) failed: {e}"); }
                        }
                    }
                }
                CtrlReq::UnsetEnvironment(key) => {
                    app.environment.remove(&key);
                    env::remove_var(&key);
                    // Kill the warm pane and respawn so it no longer has
                    // the removed env var — avoids PTY echo (#137).
                    if app.warm_pane.is_some() {
                        if let Some(mut old_wp) = app.warm_pane.take() {
                            old_wp.child.kill().ok();
                        }
                        match spawn_warm_pane(&*pty_system, &mut app) {
                            Ok(new_wp) => { app.warm_pane = Some(new_wp); }
                            Err(e) => { eprintln!("psmux: warm pane respawn (UnsetEnv) failed: {e}"); }
                        }
                    }
                }
                CtrlReq::ShowEnvironment(resp) => {
                    let mut output = String::new();
                    // Show psmux/tmux-specific environment vars
                    for (key, value) in &app.environment {
                        output.push_str(&format!("{}={}\n", key, value));
                    }
                    // Also show inherited PSMUX_/TMUX_ vars from process env
                    for (key, value) in env::vars() {
                        if (key.starts_with("PSMUX") || key.starts_with("TMUX")) && !app.environment.contains_key(&key) {
                            output.push_str(&format!("{}={}\n", key, value));
                        }
                    }
                    let _ = resp.send(output);
                }
                CtrlReq::SetHook(hook, cmd) => {
                    // Replace (not append) to match tmux semantics – prevents
                    // duplicate hooks on config reload (issue #133).
                    app.hooks.insert(hook, vec![cmd]);
                }
                CtrlReq::AppendHook(hook, cmd) => {
                    // -a/-ga: append to existing hook list so multiple
                    // plugins can register separate handlers (tmux semantics).
                    app.hooks.entry(hook).or_insert_with(Vec::new).push(cmd);
                }
                CtrlReq::ShowHooks(resp) => {
                    let mut output = String::new();
                    for (name, commands) in &app.hooks {
                        if commands.len() == 1 {
                            output.push_str(&format!("{} -> {}\n", name, commands[0]));
                        } else {
                            for (i, cmd) in commands.iter().enumerate() {
                                output.push_str(&format!("{}[{}] -> {}\n", name, i, cmd));
                            }
                        }
                    }
                    if output.is_empty() {
                        output.push_str("(no hooks)\n");
                    }
                    let _ = resp.send(output);
                }
                CtrlReq::RemoveHook(hook) => {
                    app.hooks.remove(&hook);
                }
                CtrlReq::KillServer => {
                    // Notify control clients that the server is going away,
                    // matching tmux's "%exit" wire notification before close.
                    // Flushes through the writer thread so iTerm2 sees a
                    // proper EOF-with-reason instead of a raw TCP RST.
                    if !app.control_clients.is_empty() {
                        control::emit_notification(
                            &app,
                            crate::types::ControlNotification::Exit {
                                reason: Some("server exited".to_string()),
                            },
                        );
                        // Brief drain window so writer threads can flush
                        // %exit + ST before the process exits.
                        std::thread::sleep(std::time::Duration::from_millis(80));
                    }
                    // Remove port/key files FIRST so clients see the session
                    // as gone immediately, then kill processes.
                    let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
                    let regpath = format!("{}\\.psmux\\{}.port", home, app.port_file_base());
                    let keypath = format!("{}\\.psmux\\{}.key", home, app.port_file_base());
                    let _ = std::fs::remove_file(&regpath);
                    let _ = std::fs::remove_file(&keypath);
                    crate::types::shutdown_persistent_streams();
                    // Kill all child processes using a single process snapshot
                    tree::kill_all_children_batch(&mut app.windows);
                    // Kill warm pane's child (process::exit skips Drop)
                    if let Some(mut wp) = app.warm_pane.take() { wp.child.kill().ok(); }
                    // TerminateProcess is synchronous on Windows — processes
                    // are already dead.  Minimal delay for OS handle cleanup.
                    std::thread::sleep(std::time::Duration::from_millis(10));
                    std::process::exit(0);
                }
                CtrlReq::WaitFor(channel, op) => {
                    match op {
                        WaitForOp::Lock => {
                            let entry = app.wait_channels.entry(channel).or_insert_with(|| WaitChannel {
                                locked: false,
                                waiters: Vec::new(),
                            });
                            entry.locked = true;
                        }
                        WaitForOp::Unlock => {
                            if let Some(ch) = app.wait_channels.get_mut(&channel) {
                                ch.locked = false;
                                for waiter in ch.waiters.drain(..) {
                                    let _ = waiter.send(());
                                }
                            }
                        }
                        WaitForOp::Signal => {
                            if let Some(ch) = app.wait_channels.get_mut(&channel) {
                                for waiter in ch.waiters.drain(..) {
                                    let _ = waiter.send(());
                                }
                            }
                        }
                        WaitForOp::Wait => {
                            app.wait_channels.entry(channel).or_insert_with(|| WaitChannel {
                                locked: false,
                                waiters: Vec::new(),
                            });
                        }
                    }
                }
                CtrlReq::DisplayMenu(menu_def, x, y) => {
                    let menu = parse_menu_definition(&menu_def, x, y);
                    if !menu.items.is_empty() {
                        app.mode = Mode::MenuMode { menu };
                        state_dirty = true;
                    }
                }
                CtrlReq::DisplayMenuDirect(menu) => {
                    if !menu.items.is_empty() {
                        app.mode = Mode::MenuMode { menu };
                        state_dirty = true;
                    }
                }
                CtrlReq::DisplayPopup(command, width_spec, height_spec, close_on_exit, start_dir) => {
                    // Resolve percentage dimensions against terminal area (#154)
                    let term_w = app.last_window_area.width;
                    let term_h = app.last_window_area.height;
                    let width = parse_popup_dim(&width_spec, term_w, 80);
                    let height = parse_popup_dim(&height_spec, term_h, 24);
                    // Expand format variables in start_dir (e.g. #{pane_current_path})
                    let start_dir = start_dir.map(|d| expand_format(&d, &app)).filter(|d| !d.is_empty());
                    let saved_dir = if start_dir.is_some() { env::current_dir().ok() } else { None };
                    if let Some(dir) = &start_dir { let _ = env::set_current_dir(dir); }
                    if !command.is_empty() {
                        // Spawn popup as a real Pane via the popup module
                        let inner_h = height.saturating_sub(2);
                        let inner_w = width.saturating_sub(2);
                        let pane_result = crate::popup::create_popup_pane(
                            &command,
                            start_dir.as_deref(),
                            inner_h,
                            inner_w,
                            app.next_pane_id,
                            &app.session_name,
                            &app.environment,
                        );
                        if let Some(prev) = saved_dir { let _ = env::set_current_dir(prev); }
                        
                        app.mode = Mode::PopupMode {
                            command: command.clone(),
                            output: String::new(),
                            process: None,
                            width,
                            height,
                            close_on_exit,
                            popup_pane: pane_result,
                            scroll_offset: 0,
                        };
                        state_dirty = true;
                    } else {
                        if let Some(prev) = saved_dir { let _ = env::set_current_dir(prev); }
                        app.mode = Mode::PopupMode {
                            command: String::new(),
                            output: "Press 'q' or Escape to close\n".to_string(),
                            process: None,
                            width,
                            height,
                            close_on_exit: true,
                            popup_pane: None,
                            scroll_offset: 0,
                        };
                        state_dirty = true;
                    }
                }
                CtrlReq::ConfirmBefore(prompt, cmd) => {
                    let prompt_text = if prompt.is_empty() {
                        format!("Confirm: {}? (y/n)", cmd)
                    } else {
                        // Don't append (y/n) if prompt already contains it
                        if prompt.contains("(y/n)") {
                            prompt.clone()
                        } else {
                            let base = prompt.trim_end_matches('?');
                            format!("{}? (y/n)", base)
                        }
                    };
                    app.mode = Mode::ConfirmMode {
                        prompt: prompt_text,
                        command: cmd,
                        input: String::new(),
                    };
                    state_dirty = true;
                }
                CtrlReq::ResizePaneAbsolute(axis, size) => {
                    unzoom_if_zoomed(&mut app);
                    resize_pane_absolute(&mut app, &axis, size);
                    resize_all_panes(&mut app);
                    hook_event = Some("after-resize-pane");
                }
                CtrlReq::ResizePanePercent(axis, pct) => {
                    unzoom_if_zoomed(&mut app);
                    // Convert percentage to absolute size based on current window dimensions
                    let area = app.last_window_area;
                    let total = if axis == "x" { area.width } else { area.height };
                    let abs_size = ((total as u32) * (pct as u32) / 100).max(1) as u16;
                    resize_pane_absolute(&mut app, &axis, abs_size);
                    resize_all_panes(&mut app);
                    hook_event = Some("after-resize-pane");
                }
                CtrlReq::ShowOptionValue(resp, name) => {
                    let val = get_option_value(&app, &name);
                    let _ = resp.send(val);
                }
                CtrlReq::ShowWindowOptionValue(resp, name) => {
                    let val = get_window_option_value(&app, &name);
                    let _ = resp.send(val);
                }
                CtrlReq::ShowWindowOptions(resp) => {
                    let _ = resp.send(render_window_options(&app));
                }
                CtrlReq::ChooseBuffer(resp) => {
                    let mut output = String::new();
                    for (i, buf) in app.paste_buffers.iter().enumerate() {
                        let preview: String = buf.chars().take(50).collect();
                        let preview = preview.replace('\n', "\\n").replace('\r', "");
                        output.push_str(&format!("buffer{}: {} bytes: \"{}\"\n", i, buf.len(), preview));
                    }
                    let mut names: Vec<&String> = app.named_buffers.keys().collect();
                    names.sort();
                    for name in names {
                        let buf = &app.named_buffers[name];
                        let preview: String = buf.chars().take(50).collect();
                        let preview = preview.replace('\n', "\\n").replace('\r', "");
                        output.push_str(&format!("{}: {} bytes: \"{}\"\n", name, buf.len(), preview));
                    }
                    let _ = resp.send(output);
                }
                CtrlReq::ServerInfo(resp) => {
                    let info = format!(
                        "psmux {} (Windows)\npid: {}\nsession: {}\nwindows: {}\nuptime: {}s\nsocket: {}",
                        VERSION,
                        std::process::id(),
                        app.session_name,
                        app.windows.len(),
                        (chrono::Local::now() - app.created_at).num_seconds(),
                        {
                            let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
                            format!("{}\\.psmux\\{}.port", home, app.port_file_base())
                        }
                    );
                    let _ = resp.send(info);
                }
                CtrlReq::SendPrefix => {
                    // Send the prefix key to the active pane as if typed
                    let prefix = app.prefix_key;
                    let encoded: Vec<u8> = match prefix.0 {
                        crossterm::event::KeyCode::Char(c) if prefix.1.contains(crossterm::event::KeyModifiers::CONTROL) => {
                            vec![(c.to_ascii_lowercase() as u8) & 0x1F]
                        }
                        crossterm::event::KeyCode::Char(c) => format!("{}", c).into_bytes(),
                        _ => vec![],
                    };
                    if !encoded.is_empty() {
                        let win = &mut app.windows[app.active_idx];
                        if let Some(p) = active_pane_mut(&mut win.root, &win.active_path) {
                            let _ = p.writer.write_all(&encoded);
                            let _ = p.writer.flush();
                        }
                    }
                }
                CtrlReq::PrevLayout => {
                    unzoom_if_zoomed(&mut app);
                    cycle_layout_reverse(&mut app);
                    resize_all_panes(&mut app);
                    meta_dirty = true;
                    state_dirty = true;
                }
                CtrlReq::FocusIn => {
                    if app.focus_events {
                        // Forward focus-in escape sequence to all panes in active window
                        let win = &mut app.windows[app.active_idx];
                        fn send_focus_seq(node: &mut Node, seq: &[u8]) {
                            match node {
                                Node::Leaf(p) => { let _ = p.writer.write_all(seq); let _ = p.writer.flush(); }
                                Node::Split { children, .. } => { for c in children { send_focus_seq(c, seq); } }
                            }
                        }
                        send_focus_seq(&mut win.root, b"\x1b[I");
                    }
                    hook_event = Some("pane-focus-in");
                }
                CtrlReq::FocusOut => {
                    if app.focus_events {
                        let win = &mut app.windows[app.active_idx];
                        fn send_focus_seq(node: &mut Node, seq: &[u8]) {
                            match node {
                                Node::Leaf(p) => { let _ = p.writer.write_all(seq); let _ = p.writer.flush(); }
                                Node::Split { children, .. } => { for c in children { send_focus_seq(c, seq); } }
                            }
                        }
                        send_focus_seq(&mut win.root, b"\x1b[O");
                    }
                    hook_event = Some("pane-focus-out");
                }
                CtrlReq::CommandPrompt(initial) => {
                    app.mode = Mode::CommandPrompt { input: initial.clone(), cursor: initial.len() };
                    state_dirty = true;
                }
                CtrlReq::ShowMessages(resp) => {
                    // Return message log (tmux stores recent log messages)
                    let _ = resp.send(String::new());
                }
                CtrlReq::ResizeWindow(_dim, _size) => {
                    // On Windows, window size is controlled by the terminal emulator;
                    // resize-window is a no-op since we adapt to the terminal size.
                }
                CtrlReq::RespawnWindow => {
                    // Kill all panes in the active window and respawn	
                    respawn_active_pane(&mut app, Some(&*pty_system), None, true)?;
                    state_dirty = true;
                }
                CtrlReq::PopupInput(data) => {
                    if let Mode::PopupMode { ref mut popup_pane, .. } = app.mode {
                        if let Some(ref mut pty) = popup_pane {
                            // If child has exited, 'q' closes the popup
                            let child_exited = matches!(pty.child.try_wait(), Ok(Some(_)));
                            if child_exited && data == b"q" {
                                app.mode = Mode::Passthrough;
                            } else if !child_exited {
                                let _ = pty.writer.write_all(&data);
                                let _ = pty.writer.flush();
                            }
                        } else {
                            // No PTY means static popup — 'q' closes it
                            if data == b"q" {
                                app.mode = Mode::Passthrough;
                            }
                        }
                    }
                    state_dirty = true;
                }
                CtrlReq::OverlayClose => {
                    match app.mode {
                        Mode::PopupMode { .. } | Mode::MenuMode { .. } | Mode::ConfirmMode { .. } | Mode::PaneChooser { .. } | Mode::ClockMode | Mode::CustomizeMode { .. } => {
                            app.mode = Mode::Passthrough;
                            state_dirty = true;
                        }
                        _ => {}
                    }
                }
                CtrlReq::ConfirmRespond(yes) => {
                    if let Mode::ConfirmMode { ref command, .. } = app.mode {
                        let cmd = command.clone();
                        app.mode = Mode::Passthrough;
                        if yes {
                            let _ = execute_command_string(&mut app, &cmd);
                        }
                        state_dirty = true;
                    }
                }
                CtrlReq::MenuSelect(idx) => {
                    if let Mode::MenuMode { ref menu } = app.mode {
                        if let Some(item) = menu.items.get(idx) {
                            if !item.is_separator && !item.command.is_empty() {
                                let cmd = item.command.clone();
                                app.mode = Mode::Passthrough;
                                let _ = execute_command_string(&mut app, &cmd);
                                state_dirty = true;
                            }
                        }
                    }
                }
                CtrlReq::MenuNavigate(delta) => {
                    if let Mode::MenuMode { ref mut menu } = app.mode {
                        let len = menu.items.len();
                        if len > 0 {
                            if delta > 0 {
                                // Move down, skipping separators
                                let mut next = (menu.selected + 1) % len;
                                let start = next;
                                while menu.items[next].is_separator {
                                    next = (next + 1) % len;
                                    if next == start { break; }
                                }
                                menu.selected = next;
                            } else {
                                // Move up, skipping separators
                                let mut next = if menu.selected == 0 { len - 1 } else { menu.selected - 1 };
                                let start = next;
                                while menu.items[next].is_separator {
                                    next = if next == 0 { len - 1 } else { next - 1 };
                                    if next == start { break; }
                                }
                                menu.selected = next;
                            }
                            state_dirty = true;
                        }
                    }
                }
                CtrlReq::ShowTextPopup(title, content) => {
                    let lines: Vec<&str> = content.lines().collect();
                    let width = lines.iter().map(|l| l.len()).max().unwrap_or(40).max(20) as u16 + 4;
                    let height = (lines.len() as u16 + 2).max(5);
                    app.mode = Mode::PopupMode {
                        command: title,
                        output: content,
                        process: None,
                        width: width.min(120),
                        height,
                        close_on_exit: false,
                        popup_pane: None,
                        scroll_offset: 0,
                    };
                    state_dirty = true;
                }
                CtrlReq::StatusMessage(msg) => {
                    app.status_message = Some((msg, std::time::Instant::now(), None));
                    state_dirty = true;
                }
                CtrlReq::ClearPromptHistory => {
                    app.command_history.clear();
                    app.command_history_idx = 0;
                }
                CtrlReq::ShowPromptHistory(persistent) => {
                    if persistent {
                        let content = if app.command_history.is_empty() {
                            "(no prompt history)\n".to_string()
                        } else {
                            app.command_history.iter().enumerate()
                                .map(|(i, cmd)| format!("{}: {}", i, cmd))
                                .collect::<Vec<_>>().join("\n")
                        };
                        let lines: Vec<&str> = content.lines().collect();
                        let width = lines.iter().map(|l| l.len()).max().unwrap_or(40).max(20) as u16 + 4;
                        let height = (lines.len() as u16 + 2).max(5);
                        app.mode = Mode::PopupMode {
                            command: "show-prompt-history".to_string(),
                            output: content,
                            process: None,
                            width: width.min(120),
                            height: height.min(40),
                            close_on_exit: false,
                            popup_pane: None,
                            scroll_offset: 0,
                        };
                        state_dirty = true;
                    }
                }
                CtrlReq::ControlRegister { client_id, echo, notif_tx } => {
                    app.control_clients.insert(client_id, crate::types::ControlClient {
                        client_id,
                        cmd_counter: 0,
                        echo_enabled: echo,
                        notification_tx: notif_tx,
                        paused_panes: std::collections::HashSet::new(),
                        subscriptions: std::collections::HashMap::new(),
                        subscription_values: std::collections::HashMap::new(),
                        subscription_last_check: std::collections::HashMap::new(),
                        pause_after_secs: None,
                        output_paused_panes: std::collections::HashSet::new(),
                        pane_last_output: std::collections::HashMap::new(),
                    });
                    // Register control client in the client registry
                    let tty = format!("/dev/pts/{}", client_id);
                    app.client_registry.insert(client_id, crate::types::ClientInfo {
                        id: client_id,
                        width: app.last_window_area.width,
                        height: app.last_window_area.height,
                        connected_at: std::time::Instant::now(),
                        last_activity: std::time::Instant::now(),
                        tty_name: tty,
                        is_control: true,
                    });
                    app.attached_clients = app.attached_clients.saturating_add(1);
                    // NOTE: Real tmux does NOT emit a notification burst on
                    // -CC attach (see tmux/control.c control_start()). It
                    // sends only the DCS opener "\033P1000p" and lets the
                    // client (e.g. iTerm2) drive the dialog with explicit
                    // list-sessions / list-windows -F commands. The DCS is
                    // emitted by server/connection.rs right after the
                    // CONTROL_NOECHO line is parsed. Issue #261 fix:
                    // missing DCS was the actual hang cause for iTerm2.
                    //
                    // The emit_initial_state() helper remains available for
                    // future explicit refresh hooks but is intentionally
                    // NOT called here — calling it would emit notifications
                    // before iTerm2's own list-* commands, which is
                    // non-standard.
                }
                CtrlReq::ControlSubscribe { client_id, name, target, format } => {
                    if let Some(cc) = app.control_clients.get_mut(&client_id) {
                        cc.subscriptions.insert(name.clone(), (target, format));
                        // Clear cached value so the first check always emits
                        cc.subscription_values.remove(&name);
                        cc.subscription_last_check.remove(&name);
                    }
                }
                CtrlReq::ControlUnsubscribe { client_id, name } => {
                    if let Some(cc) = app.control_clients.get_mut(&client_id) {
                        cc.subscriptions.remove(&name);
                        cc.subscription_values.remove(&name);
                        cc.subscription_last_check.remove(&name);
                    }
                }
                CtrlReq::ControlSetPauseAfter { client_id, pause_after_secs } => {
                    if let Some(cc) = app.control_clients.get_mut(&client_id) {
                        cc.pause_after_secs = pause_after_secs;
                        if pause_after_secs.is_none() {
                            // Clear all pause state when disabling
                            cc.output_paused_panes.clear();
                            cc.pane_last_output.clear();
                        }
                    }
                }
                CtrlReq::ControlContinuePane { client_id, pane_id } => {
                    if let Some(cc) = app.control_clients.get_mut(&client_id) {
                        if cc.output_paused_panes.remove(&pane_id) {
                            let _ = cc.notification_tx.try_send(
                                crate::types::ControlNotification::Continue { pane_id }
                            );
                        }
                    }
                }
                CtrlReq::ControlDeregister { client_id } => {
                    app.control_clients.remove(&client_id);
                    app.client_registry.remove(&client_id);
                    app.attached_clients = app.attached_clients.saturating_sub(1);
                }
                CtrlReq::CustomizeMode => {
                    let options = crate::server::option_catalog::build_option_list(&app);
                    app.mode = Mode::CustomizeMode {
                        options,
                        selected: 0,
                        scroll_offset: 0,
                        editing: false,
                        edit_buffer: String::new(),
                        edit_cursor: 0,
                        filter: String::new(),
                    };
                    state_dirty = true;
                }
                CtrlReq::CustomizeNavigate(delta) => {
                    if let Mode::CustomizeMode { ref options, ref mut selected, ref filter, ref mut scroll_offset, editing, .. } = app.mode {
                        if !editing {
                            let visible: Vec<usize> = options.iter().enumerate()
                                .filter(|(_, (name, _, _))| filter.is_empty() || name.contains(filter.as_str()))
                                .map(|(i, _)| i)
                                .collect();
                            if !visible.is_empty() {
                                let cur_pos = visible.iter().position(|&i| i == *selected).unwrap_or(0);
                                let new_pos = if delta > 0 {
                                    (cur_pos + delta as usize).min(visible.len() - 1)
                                } else {
                                    cur_pos.saturating_sub((-delta) as usize)
                                };
                                *selected = visible[new_pos];
                                // Update scroll offset to keep selection visible
                                if new_pos < *scroll_offset {
                                    *scroll_offset = new_pos;
                                } else if new_pos >= *scroll_offset + 20 {
                                    *scroll_offset = new_pos.saturating_sub(19);
                                }
                            }
                            state_dirty = true;
                        }
                    }
                }
                CtrlReq::CustomizeEdit => {
                    if let Mode::CustomizeMode { ref options, selected, ref mut editing, ref mut edit_buffer, ref mut edit_cursor, .. } = app.mode {
                        if !*editing {
                            if let Some((_, value, _)) = options.get(selected) {
                                *edit_buffer = value.clone();
                                *edit_cursor = edit_buffer.len();
                                *editing = true;
                                state_dirty = true;
                            }
                        }
                    }
                }
                CtrlReq::CustomizeEditUpdate(text) => {
                    if let Mode::CustomizeMode { editing, ref mut edit_buffer, ref mut edit_cursor, .. } = app.mode {
                        if editing {
                            *edit_buffer = text.clone();
                            *edit_cursor = edit_buffer.len();
                            state_dirty = true;
                        }
                    }
                }
                CtrlReq::CustomizeEditConfirm => {
                    if let Mode::CustomizeMode { ref mut options, selected, ref mut editing, ref edit_buffer, .. } = app.mode {
                        if *editing {
                            let name = options[selected].0.clone();
                            let value = edit_buffer.clone();
                            options[selected].1 = value.clone();
                            *editing = false;
                            options::apply_set_option(&mut app, &name, &value, true);
                            state_dirty = true;
                        }
                    }
                }
                CtrlReq::CustomizeEditCancel => {
                    if let Mode::CustomizeMode { ref mut editing, ref mut edit_buffer, .. } = app.mode {
                        if *editing {
                            *editing = false;
                            *edit_buffer = String::new();
                            state_dirty = true;
                        }
                    }
                }
                CtrlReq::CustomizeResetDefault => {
                    if let Mode::CustomizeMode { ref mut options, selected, editing, .. } = app.mode {
                        if !editing {
                            if let Some(def) = option_catalog::default_for(&options[selected].0) {
                                let name = options[selected].0.clone();
                                let value = def.to_string();
                                options[selected].1 = value.clone();
                                options::apply_set_option(&mut app, &name, &value, true);
                                state_dirty = true;
                            }
                        }
                    }
                }
                CtrlReq::CustomizeFilter(text) => {
                    if let Mode::CustomizeMode { ref mut filter, ref mut selected, ref mut scroll_offset, ref options, .. } = app.mode {
                        *filter = text;
                        // Reset selection to first matching option
                        let first_match = options.iter().enumerate()
                            .find(|(_, (name, _, _))| filter.is_empty() || name.contains(filter.as_str()))
                            .map(|(i, _)| i);
                        if let Some(idx) = first_match {
                            *selected = idx;
                        }
                        *scroll_offset = 0;
                        state_dirty = true;
                    }
                }
                CtrlReq::RunCommand(cmd, resp) => {
                    let result = execute_command_string(&mut app, &cmd);
                    match result {
                        Ok(()) => { let _ = resp.send("OK".to_string()); }
                        Err(e) => { let _ = resp.send(format!("error: {}", e)); }
                    }
                }
            }
            // Log any active_idx change for debugging window-switch issues
            if app.active_idx != _prev_active_idx && crate::debug_log::server_log_enabled() {
                crate::debug_log::server_log("switch", &format!(
                    "active_idx changed {} -> {} by req={} hook={:?}",
                    _prev_active_idx, app.active_idx, _req_tag, hook_event));
            }
            // Fire any hooks registered for the event that just occurred
            if let Some(event) = hook_event {
                let _pre_hook_idx = app.active_idx;
                let cmds: Vec<String> = app.hooks.get(event).cloned().unwrap_or_default();
                for cmd in cmds {
                    let _ = execute_command_string(&mut app, &cmd);
                }
                // Emit control mode notifications for hook events
                if !app.control_clients.is_empty() {
                    let active_win = &app.windows[app.active_idx];
                    let win_id = active_win.id;
                    let active_pane_id = get_active_pane_id(&active_win.root, &active_win.active_path).unwrap_or(0);
                    match event {
                        "after-new-window" => {
                            control::emit_notification(&app, crate::types::ControlNotification::WindowAdd { window_id: win_id });
                        }
                        "after-kill-pane" | "window-closed" => {
                            control::emit_notification(&app, crate::types::ControlNotification::WindowClose { window_id: win_id });
                        }
                        "after-rename-window" => {
                            let name = active_win.name.clone();
                            control::emit_notification(&app, crate::types::ControlNotification::WindowRenamed { window_id: win_id, name });
                        }
                        "after-select-window" => {
                            control::emit_notification(&app, crate::types::ControlNotification::SessionWindowChanged {
                                session_id: app.session_id, window_id: win_id,
                            });
                        }
                        "after-select-pane" => {
                            control::emit_notification(&app, crate::types::ControlNotification::WindowPaneChanged {
                                window_id: win_id, pane_id: active_pane_id,
                            });
                        }
                        "after-rename-session" => {
                            let name = app.session_name.clone();
                            control::emit_notification(&app, crate::types::ControlNotification::SessionRenamed { name });
                        }
                        "client-attached" => {
                            let name = app.session_name.clone();
                            control::emit_notification(&app, crate::types::ControlNotification::SessionChanged {
                                session_id: app.session_id, name,
                            });
                        }
                        "client-detached" => {
                            control::emit_notification(&app, crate::types::ControlNotification::ClientDetached {
                                client: "client".to_string(),
                            });
                        }
                        "after-split-window" | "after-resize-pane" | "after-break-pane"
                        | "after-join-pane" | "after-rotate-window" | "after-swap-pane" => {
                            control::emit_notification(&app, crate::types::ControlNotification::LayoutChange {
                                window_id: win_id,
                                layout: format!("{}x{}", app.last_window_area.width, app.last_window_area.height),
                            });
                        }
                        "window-linked" => {
                            control::emit_notification(&app, crate::types::ControlNotification::WindowAdd { window_id: win_id });
                        }
                        "window-unlinked" => {
                            control::emit_notification(&app, crate::types::ControlNotification::WindowClose { window_id: win_id });
                        }
                        _ => {}
                    }
                }
                // Check if the hook itself changed active_idx
                if app.active_idx != _pre_hook_idx && crate::debug_log::server_log_enabled() {
                    crate::debug_log::server_log("switch", &format!(
                        "active_idx changed {} -> {} by HOOK event={}",
                        _pre_hook_idx, app.active_idx, event));
                }
            }
            // Restore temporary -t focus after non-temp command completes.
            // Use pane ID (not path) because kill-pane restructures the
            // tree and invalidates saved paths (#71).
            if !is_temp_focus {
                if let Some((restore_idx, restore_pane_id)) = temp_focus_restore.take() {
                    if restore_idx < app.windows.len() {
                        app.active_idx = restore_idx;
                        let win = &mut app.windows[restore_idx];
                        if let Some(path) = crate::tree::find_path_by_id(&win.root, restore_pane_id) {
                            win.active_path = path;
                        }
                        // If the pane was killed, keep whatever active_path
                        // kill_pane_at_path already set (MRU target).
                    }
                }
            }
            if mutates_state {
                state_dirty = true;
            }
        }
                // No trailing cleanup: temp_focus_restore persists across
                // batch boundaries so the actual command that follows in a
                // later batch can still benefit from the temp focus (and
                // will restore when it processes as a non-temp-focus req).
            }
        }
        // Drain async run-shell results (non-blocking).
        if let Some(rx) = app.run_shell_rx.as_ref() {
            while let Ok((title, text)) = rx.try_recv() {
                if !text.is_empty() {
                    let lines: Vec<&str> = text.lines().collect();
                    let width = lines.iter().map(|l| l.len()).max().unwrap_or(40).max(20) as u16 + 4;
                    let height = (lines.len() as u16 + 2).max(5);
                    app.mode = Mode::PopupMode {
                        command: title,
                        output: text,
                        process: None,
                        width: width.min(120),
                        height,
                        close_on_exit: false,
                        popup_pane: None,
                        scroll_offset: 0,
                    };
                    state_dirty = true;
                }
            }
        }
        // ── Server-push: proactively send frames to attached clients ──
        // Instead of waiting for clients to poll dump-state, serialize
        // and push whenever state changed (PTY output, new window, key
        // echo, etc.).  This gives event-driven rendering like wezterm:
        // frames arrive within 1-5ms of ConPTY output instead of waiting
        // for the next client poll cycle (up to 50ms).
        if (state_dirty || meta_dirty) && crate::types::has_frame_receivers() {
            // Check bell/activity state for the pushed frame
            let push_alert_hooks = helpers::check_window_activity(&mut app);
            for event in &push_alert_hooks {
                crate::commands::fire_hooks(&mut app, event);
            }
            // Rebuild metadata cache if structural changes happened.
            if meta_dirty {
                cached_windows_json = list_windows_json_with_tabs(&app)?;
                cached_tree_json = list_tree_json(&app)?;
                cached_prefix_str = format_key_binding(&app.prefix_key);
                cached_prefix2_str = app.prefix2_key.as_ref().map(|k| format_key_binding(k)).unwrap_or_default();
                cached_base_index = app.window_base_index;
                cached_pred_dim = app.prediction_dimming;
                cached_status_style = app.status_style.clone();
                cached_bindings_json = serialize_bindings_json(&app);
                meta_dirty = false;
            }
            let layout_json = dump_layout_json_fast(&mut app)?;
            combined_buf.clear();
            let ss_escaped = json_escape_string(&cached_status_style);
            let sl_expanded = json_escape_string(&expand_format(&app.status_left, &app));
            let sr_expanded = json_escape_string(&expand_format(&app.status_right, &app));
            let pbs_escaped = json_escape_string(&app.pane_border_style);
            let pabs_escaped = json_escape_string(&app.pane_active_border_style);
            let pbhs_escaped = json_escape_string(&app.pane_border_hover_style);
            let wsf_escaped = json_escape_string(&app.window_status_format);
            let wscf_escaped = json_escape_string(&app.window_status_current_format);
            let wss_escaped = json_escape_string(&app.window_status_separator);
            let ws_style_escaped = json_escape_string(&app.window_status_style);
            let wsc_style_escaped = json_escape_string(&app.window_status_current_style);
            let mode_style_escaped = json_escape_string(&app.mode_style);
            let status_position_escaped = json_escape_string(&app.status_position);
            let status_justify_escaped = json_escape_string(&app.status_justify);
            let status_format_json = {
                let mut sf = String::from("[");
                for (i, fmt_str) in app.status_format.iter().enumerate() {
                    if i > 0 { sf.push(','); }
                    sf.push('"');
                    sf.push_str(&json_escape_string(&expand_format(fmt_str, &app)));
                    sf.push('"');
                }
                sf.push(']');
                sf
            };
            let cursor_style_code = crate::rendering::configured_cursor_code();
            let _ = std::fmt::Write::write_fmt(&mut combined_buf, format_args!(
                "{{\"layout\":{},\"windows\":{},\"prefix\":\"{}\",\"prefix2\":\"{}\",\"tree\":{},\"base_index\":{},\"pane_base_index\":{},\"prediction_dimming\":{},\"status_style\":\"{}\",\"status_left\":\"{}\",\"status_right\":\"{}\",\"pane_border_style\":\"{}\",\"pane_active_border_style\":\"{}\",\"pane_border_hover_style\":\"{}\",\"wsf\":\"{}\",\"wscf\":\"{}\",\"wss\":\"{}\",\"ws_style\":\"{}\",\"wsc_style\":\"{}\",\"clock_mode\":{},\"bindings\":{},\"status_left_length\":{},\"status_right_length\":{},\"status_lines\":{},\"status_format\":{},\"mode_style\":\"{}\",\"status_position\":\"{}\",\"status_justify\":\"{}\",\"cursor_style_code\":{},\"status_visible\":{},\"repeat_time\":{},\"zoomed\":{},\"pwsh_mouse_selection\":{},\"mouse_selection\":{},\"choose_tree_preview\":{}}}",
                layout_json, cached_windows_json, cached_prefix_str, cached_prefix2_str, cached_tree_json, cached_base_index, app.pane_base_index, cached_pred_dim, ss_escaped, sl_expanded, sr_expanded, pbs_escaped, pabs_escaped, pbhs_escaped, wsf_escaped, wscf_escaped, wss_escaped, ws_style_escaped, wsc_style_escaped,
                matches!(app.mode, Mode::ClockMode), cached_bindings_json,
                app.status_left_length, app.status_right_length, app.status_lines, status_format_json,
                mode_style_escaped, status_position_escaped, status_justify_escaped,
                cursor_style_code, app.status_visible, app.repeat_time_ms,
                app.windows.get(app.active_idx).map_or(false, |w| w.zoom_saved.is_some()),
                app.pwsh_mouse_selection,
                app.mouse_selection,
                app.choose_tree_preview,
            ));
            // Inject overlay state (popup, menu, confirm, display_panes)
            {
                // Inject clock_colour if set
                if let Some(cc) = app.user_options.get("clock-mode-colour") {
                    if combined_buf.ends_with('}') {
                        combined_buf.pop();
                        combined_buf.push_str(",\"clock_colour\":\"");
                        combined_buf.push_str(&json_escape_string(cc));
                        combined_buf.push_str("\"}");
                    }
                }
                // Inject pane-border-status and pane-border-format
                if let Some(pbs) = app.user_options.get("pane-border-status") {
                    if combined_buf.ends_with('}') {
                        combined_buf.pop();
                        combined_buf.push_str(",\"pane_border_status\":\"");
                        combined_buf.push_str(&json_escape_string(pbs));
                        combined_buf.push('"');
                        if let Some(pbf) = app.user_options.get("pane-border-format") {
                            combined_buf.push_str(",\"pane_border_format\":\"");
                            combined_buf.push_str(&json_escape_string(pbf));
                            combined_buf.push('"');
                        }
                        combined_buf.push('}');
                    }
                }
                let overlay_json = serialize_overlay_json(&app);
                if !overlay_json.is_empty() && combined_buf.ends_with('}') {
                    combined_buf.pop();
                    combined_buf.push_str(&overlay_json);
                    combined_buf.push('}');
                }
            }
            // Inject clipboard data if pending
            if let Some(clip_text) = app.clipboard_osc52.take() {
                let clip_b64 = base64_encode(&clip_text);
                if combined_buf.ends_with('}') {
                    combined_buf.pop();
                    combined_buf.push_str(",\"clipboard_osc52\":\"");
                    combined_buf.push_str(&clip_b64);
                    combined_buf.push_str("\"}");
                }
            }
            cached_dump_state.clear();
            cached_dump_state.push_str(&combined_buf);
            // Inject bell AFTER caching (one-shot: should not persist in cache)
            if app.bell_forward {
                app.bell_forward = false;
                if combined_buf.ends_with('}') {
                    combined_buf.pop();
                    combined_buf.push_str(",\"bell\":true}");
                }
            }
            cached_data_version = combined_data_version(&app);
            state_dirty = false;
            crate::types::push_frame(&combined_buf);
        }
        // ── Status-interval timer: fire hooks periodically ──
        if app.status_interval > 0 {
            let elapsed = app.last_status_interval_fire.elapsed().as_secs();
            if elapsed >= app.status_interval {
                app.last_status_interval_fire = std::time::Instant::now();
                let _pre_status_idx = app.active_idx;
                let cmds: Vec<String> = app.hooks.get("status-interval").cloned().unwrap_or_default();
                for cmd in cmds {
                    let bg_cmd = crate::commands::ensure_background(&cmd);
                    let _ = execute_command_string(&mut app, &bg_cmd);
                }
                if app.active_idx != _pre_status_idx && crate::debug_log::server_log_enabled() {
                    crate::debug_log::server_log("switch", &format!(
                        "active_idx changed {} -> {} by status-interval hook",
                        _pre_status_idx, app.active_idx));
                }
                // Mark state dirty so the next loop iteration pushes a fresh
                // frame with re-expanded strftime codes (%H:%M:%S, %r, etc.)
                // in status-left / status-right.  Without this, the status
                // bar clock never updates for persistent (TUI) clients.
                state_dirty = true;
            }
        }
        // ── Subscription check: expand format strings and emit %subscription-changed ──
        // Zero cost when no clients have subscriptions.
        if !app.control_clients.is_empty() {
            let now_sub = std::time::Instant::now();
            // Phase 1: collect (client_id, sub_name, format) pairs that need checking
            let mut to_check: Vec<(u64, String, String)> = Vec::new();
            for client in app.control_clients.values_mut() {
                if client.subscriptions.is_empty() {
                    continue;
                }
                let sub_names: Vec<String> = client.subscriptions.keys().cloned().collect();
                for name in sub_names {
                    // Rate limit: at most once per second per subscription
                    if let Some(last) = client.subscription_last_check.get(&name) {
                        if now_sub.duration_since(*last).as_secs() < 1 {
                            continue;
                        }
                    }
                    client.subscription_last_check.insert(name.clone(), now_sub);
                    let format = client.subscriptions[&name].1.clone();
                    to_check.push((client.client_id, name, format));
                }
            }
            // Phase 2: expand formats with immutable borrow of app
            let mut sub_results: Vec<(u64, String, String)> = Vec::new();
            for (cid, name, format) in &to_check {
                let expanded = crate::format::expand_format(format, &app);
                sub_results.push((*cid, name.clone(), expanded));
            }
            // Phase 3: compare and emit notifications
            let active_win = &app.windows[app.active_idx];
            let win_id = active_win.id;
            let pane_id = get_active_pane_id(&active_win.root, &active_win.active_path).unwrap_or(0);
            let session_id = app.session_id;
            let win_idx = app.active_idx;
            let mut sub_notifs: Vec<(u64, crate::types::ControlNotification)> = Vec::new();
            for (cid, name, expanded) in sub_results {
                if let Some(cc) = app.control_clients.get(&cid) {
                    let changed = match cc.subscription_values.get(&name) {
                        Some(prev) => prev != &expanded,
                        None => true,
                    };
                    if changed {
                        sub_notifs.push((cid, crate::types::ControlNotification::SubscriptionChanged {
                            name: name.clone(),
                            session_id,
                            window_id: win_id,
                            window_index: win_idx,
                            pane_id,
                            value: expanded.clone(),
                        }));
                    }
                }
            }
            // Phase 4: update cached values and send notifications
            for (cid, ref notif) in &sub_notifs {
                if let Some(cc) = app.control_clients.get_mut(cid) {
                    if let crate::types::ControlNotification::SubscriptionChanged { name, value, .. } = notif {
                        cc.subscription_values.insert(name.clone(), value.clone());
                    }
                }
            }
            for (cid, notif) in sub_notifs {
                if let Some(cc) = app.control_clients.get(&cid) {
                    let _ = cc.notification_tx.try_send(notif);
                }
            }
        }
        // ── PaneChooser timeout ──
        // Auto-close display-panes overlay after display-panes-time (default 1000ms).
        if let Mode::PaneChooser { opened_at } = &app.mode {
            if opened_at.elapsed() > Duration::from_millis(app.display_panes_time_ms) {
                app.mode = Mode::Passthrough;
                state_dirty = true;
            }
        }
        // ── Popup child exit detection ──
        // Check if popup PTY's child process has exited; if so, auto-close.
        if let Mode::PopupMode { ref mut popup_pane, close_on_exit, .. } = app.mode {
            let should_close = if let Some(ref mut pane) = popup_pane {
                matches!(pane.child.try_wait(), Ok(Some(_)))
            } else { false };
            if should_close && close_on_exit {
                app.mode = Mode::Passthrough;
                state_dirty = true;
            }
        }
        // Check if all windows/panes have exited (throttled to every 250ms)
        if last_reap.elapsed() >= Duration::from_millis(100) {
            last_reap = Instant::now();
            let (all_empty, any_pruned, any_newly_dead) = tree::reap_children(&mut app)?;
            if any_pruned {
                // A pane was removed from the tree - resize remaining panes to fill the space
                resize_all_panes(&mut app);
            }
            if any_pruned || any_newly_dead {
                // A pane exited — fire hooks whether it was removed (remain-on-exit off)
                // or just marked dead (remain-on-exit on).  Fixes #227.
                state_dirty = true;
                meta_dirty = true;
                crate::commands::fire_hooks(&mut app, "pane-died");
                crate::commands::fire_hooks(&mut app, "pane-exited");
            }
            if app.exit_empty && all_empty {
                let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
                let regpath = format!("{}\\.psmux\\{}.port", home, app.port_file_base());
                let keypath = format!("{}\\.psmux\\{}.key", home, app.port_file_base());
                let _ = std::fs::remove_file(&regpath);
                let _ = std::fs::remove_file(&keypath);
                crate::types::shutdown_persistent_streams();
                // Kill warm pane's child (process::exit skips Drop)
                if let Some(mut wp) = app.warm_pane.take() { wp.child.kill().ok(); }
                std::thread::sleep(std::time::Duration::from_millis(10));
                std::process::exit(0);
            }
        }
        // recv_timeout already handles the wait; no additional sleep needed.
    }
    #[allow(unreachable_code)]
    Ok(())
}

#[cfg(test)]
#[path = "../../tests-rs/test_server.rs"]
mod tests;

#[cfg(test)]
#[path = "../../tests-rs/test_issue169_manual_rename.rs"]
mod test_issue169;

#[cfg(test)]
#[path = "../../tests-rs/test_pane_title.rs"]
mod test_pane_title;

#[cfg(test)]
#[path = "../../tests-rs/test_issue202_switch_client.rs"]
mod test_issue202;

#[cfg(test)]
#[path = "../../tests-rs/test_new_session_env.rs"]
mod test_new_session_env;
