Skip to main content

par_term/app/
window_state.rs

1//! Per-window state for multi-window terminal emulator
2//!
3//! This module contains `WindowState`, which holds all state specific to a single window,
4//! including its renderer, tab manager, input handler, and UI components.
5
6use crate::ai_inspector::chat::ChatMessage;
7use crate::ai_inspector::panel::{AIInspectorPanel, InspectorAction};
8use crate::app::anti_idle::should_send_keep_alive;
9use crate::app::debug_state::DebugState;
10use crate::badge::{BadgeState, render_badge};
11use crate::cell_renderer::PaneViewport;
12use crate::clipboard_history_ui::{ClipboardHistoryAction, ClipboardHistoryUI};
13use crate::close_confirmation_ui::{CloseConfirmAction, CloseConfirmationUI};
14use crate::command_history::CommandHistory;
15use crate::command_history_ui::{CommandHistoryAction, CommandHistoryUI};
16use crate::config::{
17    Config, CursorShaderMetadataCache, CursorStyle, ShaderInstallPrompt, ShaderMetadataCache,
18};
19use crate::help_ui::HelpUI;
20use crate::input::InputHandler;
21use crate::integrations_ui::{IntegrationsResponse, IntegrationsUI};
22use crate::keybindings::KeybindingRegistry;
23use crate::paste_special_ui::{PasteSpecialAction, PasteSpecialUI};
24use crate::profile::{ProfileManager, storage as profile_storage};
25use crate::profile_drawer_ui::{ProfileDrawerAction, ProfileDrawerUI};
26use crate::progress_bar::{ProgressBarSnapshot, render_progress_bars};
27use crate::quit_confirmation_ui::{QuitConfirmAction, QuitConfirmationUI};
28use crate::remote_shell_install_ui::{RemoteShellInstallAction, RemoteShellInstallUI};
29use crate::renderer::{
30    DividerRenderInfo, PaneDividerSettings, PaneRenderInfo, PaneTitleInfo, Renderer,
31};
32use crate::scrollback_metadata::ScrollbackMark;
33use crate::search::SearchUI;
34use crate::selection::SelectionMode;
35use crate::shader_install_ui::{ShaderInstallResponse, ShaderInstallUI};
36use crate::shader_watcher::{ShaderReloadEvent, ShaderType, ShaderWatcher};
37use crate::smart_selection::SmartSelectionCache;
38use crate::ssh_connect_ui::{SshConnectAction, SshConnectUI};
39use crate::status_bar::StatusBarUI;
40use crate::tab::{TabId, TabManager};
41use crate::tab_bar_ui::{TabBarAction, TabBarUI};
42use crate::tmux::{TmuxSession, TmuxSync};
43use crate::tmux_session_picker_ui::{SessionPickerAction, TmuxSessionPickerUI};
44use crate::tmux_status_bar_ui::TmuxStatusBarUI;
45use anyhow::Result;
46use par_term_acp::{
47    Agent, AgentConfig, AgentMessage, AgentStatus, ClientCapabilities, FsCapabilities, SafePaths,
48    discover_agents,
49};
50use par_term_emu_core_rust::cursor::CursorStyle as TermCursorStyle;
51use std::sync::Arc;
52use tokio::runtime::Runtime;
53use tokio::sync::mpsc;
54use wgpu::SurfaceError;
55use winit::dpi::PhysicalSize;
56use winit::window::Window;
57
58/// Renderer sizing info needed for split pane calculations
59struct RendererSizing {
60    size: PhysicalSize<u32>,
61    content_offset_y: f32,
62    content_offset_x: f32,
63    content_inset_bottom: f32,
64    content_inset_right: f32,
65    cell_width: f32,
66    cell_height: f32,
67    padding: f32,
68    status_bar_height: f32,
69    scale_factor: f32,
70}
71
72/// Pane render data tuple for split pane rendering
73type PaneRenderData = (
74    PaneViewport,
75    Vec<crate::cell_renderer::Cell>,
76    (usize, usize),
77    Option<(usize, usize)>,
78    f32,
79    Vec<ScrollbackMark>,
80    usize,                               // scrollback_len
81    usize,                               // scroll_offset
82    Option<crate::pane::PaneBackground>, // per-pane background
83);
84
85/// Per-window state that manages a single terminal window with multiple tabs
86pub struct WindowState {
87    pub(crate) config: Config,
88    pub(crate) window: Option<Arc<Window>>,
89    pub(crate) renderer: Option<Renderer>,
90    pub(crate) input_handler: InputHandler,
91    pub(crate) runtime: Arc<Runtime>,
92
93    /// Tab manager for handling multiple terminal tabs
94    pub(crate) tab_manager: TabManager,
95    /// Tab bar UI
96    pub(crate) tab_bar_ui: TabBarUI,
97    /// tmux status bar UI
98    pub(crate) tmux_status_bar_ui: TmuxStatusBarUI,
99    /// Custom status bar UI
100    pub(crate) status_bar_ui: StatusBarUI,
101
102    pub(crate) debug: DebugState,
103
104    /// Cursor opacity for smooth fade animation (0.0 = invisible, 1.0 = fully visible)
105    pub(crate) cursor_opacity: f32,
106    /// Time of last cursor blink toggle
107    pub(crate) last_cursor_blink: Option<std::time::Instant>,
108    /// Time of last key press (to reset cursor blink)
109    pub(crate) last_key_press: Option<std::time::Instant>,
110    /// Whether window is currently in fullscreen mode
111    pub(crate) is_fullscreen: bool,
112    /// egui context for GUI rendering
113    pub(crate) egui_ctx: Option<egui::Context>,
114    /// egui-winit state for event handling
115    pub(crate) egui_state: Option<egui_winit::State>,
116    /// Pending egui events to inject into next frame's raw_input.
117    /// Used when macOS menu accelerators intercept Cmd+V/C/A before egui sees them
118    /// while an egui overlay (profile modal, search, etc.) is active.
119    pub(crate) pending_egui_events: Vec<egui::Event>,
120    /// Whether egui has completed its first ctx.run() call
121    /// Before first run, egui's is_using_pointer() returns unreliable results
122    pub(crate) egui_initialized: bool,
123    /// Cache for parsed shader metadata (used for config resolution)
124    pub(crate) shader_metadata_cache: ShaderMetadataCache,
125    /// Cache for parsed cursor shader metadata (used for config resolution)
126    pub(crate) cursor_shader_metadata_cache: CursorShaderMetadataCache,
127    /// Help UI manager
128    pub(crate) help_ui: HelpUI,
129    /// Clipboard history UI manager
130    pub(crate) clipboard_history_ui: ClipboardHistoryUI,
131    /// Command history UI manager (fuzzy search)
132    pub(crate) command_history_ui: CommandHistoryUI,
133    /// Persistent command history
134    pub(crate) command_history: CommandHistory,
135    /// Commands already synced from marks to persistent history (avoids repeated adds)
136    synced_commands: std::collections::HashSet<String>,
137    /// Paste special UI manager (text transformations)
138    pub(crate) paste_special_ui: PasteSpecialUI,
139    /// tmux session picker UI
140    pub(crate) tmux_session_picker_ui: TmuxSessionPickerUI,
141    /// Search UI manager
142    pub(crate) search_ui: SearchUI,
143    /// AI Inspector side panel
144    pub(crate) ai_inspector: AIInspectorPanel,
145    /// Last known AI Inspector panel consumed width (logical pixels).
146    /// Used to detect width changes from drag-resizing and trigger terminal reflow.
147    pub(crate) last_inspector_width: f32,
148    /// ACP agent message receiver
149    pub(crate) agent_rx: Option<mpsc::UnboundedReceiver<AgentMessage>>,
150    /// ACP agent message sender (kept to signal prompt completion)
151    pub(crate) agent_tx: Option<mpsc::UnboundedSender<AgentMessage>>,
152    /// ACP agent (managed via tokio)
153    pub(crate) agent: Option<Arc<tokio::sync::Mutex<Agent>>>,
154    /// ACP JSON-RPC client for sending responses without locking the agent.
155    /// Stored separately to avoid deadlocks: `send_prompt` holds the agent lock
156    /// while waiting for the prompt response, but the agent's tool calls
157    /// (e.g. `fs/readTextFile`) need us to respond via this same client.
158    pub(crate) agent_client: Option<Arc<par_term_acp::JsonRpcClient>>,
159    /// Available agent configs
160    pub(crate) available_agents: Vec<AgentConfig>,
161    /// Shader install prompt UI
162    pub(crate) shader_install_ui: ShaderInstallUI,
163    /// Receiver for shader installation results (from background thread)
164    pub(crate) shader_install_receiver: Option<std::sync::mpsc::Receiver<Result<usize, String>>>,
165    /// Combined integrations welcome dialog UI
166    pub(crate) integrations_ui: IntegrationsUI,
167    /// Close confirmation dialog UI (for tabs with running jobs)
168    pub(crate) close_confirmation_ui: CloseConfirmationUI,
169    /// Quit confirmation dialog UI (prompt before closing window)
170    pub(crate) quit_confirmation_ui: QuitConfirmationUI,
171    /// Remote shell integration install dialog UI
172    pub(crate) remote_shell_install_ui: RemoteShellInstallUI,
173    /// SSH Quick Connect dialog UI
174    pub(crate) ssh_connect_ui: SshConnectUI,
175    /// Whether terminal session recording is active
176    pub(crate) is_recording: bool,
177    /// When recording started
178    #[allow(dead_code)]
179    pub(crate) recording_start_time: Option<std::time::Instant>,
180    /// Flag to indicate shutdown is in progress
181    pub(crate) is_shutting_down: bool,
182    /// Window index (1-based) for display in title bar
183    pub(crate) window_index: usize,
184
185    // Smart redraw tracking (event-driven rendering)
186    /// Whether we need to render next frame
187    pub(crate) needs_redraw: bool,
188    /// Set when an agent/MCP config update was applied — signals WindowManager to
189    /// sync its own config copy so subsequent saves don't overwrite agent changes.
190    pub(crate) config_changed_by_agent: bool,
191    /// When to blink cursor next
192    pub(crate) cursor_blink_timer: Option<std::time::Instant>,
193    /// Whether we need to rebuild renderer after font-related changes
194    pub(crate) pending_font_rebuild: bool,
195
196    // Focus state for power saving
197    /// Whether the window currently has focus
198    pub(crate) is_focused: bool,
199    /// Last time a frame was rendered (for FPS throttling when unfocused)
200    pub(crate) last_render_time: Option<std::time::Instant>,
201
202    // Flicker reduction state
203    /// When cursor was last hidden (for reduce_flicker feature)
204    pub(crate) cursor_hidden_since: Option<std::time::Instant>,
205    /// Whether we have pending terminal updates deferred due to cursor being hidden
206    pub(crate) flicker_pending_render: bool,
207
208    // Throughput mode state
209    /// When throughput mode batching started (for render interval timing)
210    pub(crate) throughput_batch_start: Option<std::time::Instant>,
211
212    // Shader hot reload
213    /// Shader file watcher for hot reload support
214    pub(crate) shader_watcher: Option<ShaderWatcher>,
215    /// Config file watcher for automatic reload (e.g., when ACP agent modifies config.yaml)
216    pub(crate) config_watcher: Option<crate::config::watcher::ConfigWatcher>,
217    /// Watcher for `.config-update.json` written by the MCP server
218    pub(crate) config_update_watcher: Option<crate::config::watcher::ConfigWatcher>,
219    /// Last shader reload error message (for display in UI)
220    pub(crate) shader_reload_error: Option<String>,
221    /// Background shader reload result: None = no change, Some(None) = success, Some(Some(err)) = error
222    /// Used to propagate hot reload results to standalone settings window
223    pub(crate) background_shader_reload_result: Option<Option<String>>,
224    /// Cursor shader reload result: None = no change, Some(None) = success, Some(Some(err)) = error
225    /// Used to propagate hot reload results to standalone settings window
226    pub(crate) cursor_shader_reload_result: Option<Option<String>>,
227
228    /// Flag to signal that the settings window should be opened
229    /// This is set by keyboard handlers and consumed by the window manager
230    pub(crate) open_settings_window_requested: bool,
231
232    /// Pending arrangement restore request (name of arrangement to restore)
233    pub(crate) pending_arrangement_restore: Option<String>,
234
235    /// Flag to request reload of dynamic profiles
236    pub(crate) reload_dynamic_profiles_requested: bool,
237
238    // Profile management
239    /// Profile manager for storing and managing terminal profiles
240    pub(crate) profile_manager: ProfileManager,
241    /// Profile drawer UI (collapsible side panel)
242    pub(crate) profile_drawer_ui: ProfileDrawerUI,
243    /// Flag to signal that the settings window should open to the Profiles tab
244    pub(crate) open_settings_profiles_tab: bool,
245    /// Flag to indicate profiles menu needs to be updated in the main menu
246    pub(crate) profiles_menu_needs_update: bool,
247    /// Track if we blocked a mouse press for UI - also block the corresponding release
248    pub(crate) ui_consumed_mouse_press: bool,
249    /// Eat the first mouse click after window focus to prevent forwarding to PTY.
250    /// Without this, clicking to focus the window sends a mouse event to tmux (or
251    /// other mouse-aware apps), which can trigger a zero-char selection that clears
252    /// the system clipboard — destroying any clipboard image.
253    pub(crate) focus_click_pending: bool,
254
255    // Resize overlay state
256    /// Whether the resize overlay is currently visible
257    pub(crate) resize_overlay_visible: bool,
258    /// When to hide the resize overlay (after resize stops)
259    pub(crate) resize_overlay_hide_time: Option<std::time::Instant>,
260    /// Current resize dimensions: (width_px, height_px, cols, rows)
261    pub(crate) resize_dimensions: Option<(u32, u32, usize, usize)>,
262
263    // Toast notification state
264    /// Current toast message to display
265    pub(crate) toast_message: Option<String>,
266    /// When to hide the toast notification
267    pub(crate) toast_hide_time: Option<std::time::Instant>,
268
269    // Pane identification overlay
270    /// When to hide the pane index overlay
271    pub(crate) pane_identify_hide_time: Option<std::time::Instant>,
272
273    /// Recently closed tab metadata for session undo (reopen closed tab)
274    pub(crate) closed_tabs: std::collections::VecDeque<super::tab_ops::ClosedTabInfo>,
275
276    /// Keybinding registry for user-defined keyboard shortcuts
277    pub(crate) keybinding_registry: KeybindingRegistry,
278
279    /// Cache for compiled smart selection regex patterns
280    pub(crate) smart_selection_cache: SmartSelectionCache,
281
282    // tmux integration state
283    /// tmux control mode session (if connected)
284    pub(crate) tmux_session: Option<TmuxSession>,
285    /// tmux state synchronization manager
286    pub(crate) tmux_sync: TmuxSync,
287    /// Current tmux session name (for window title display)
288    pub(crate) tmux_session_name: Option<String>,
289    /// Tab ID where the tmux gateway connection lives (where we write commands)
290    pub(crate) tmux_gateway_tab_id: Option<TabId>,
291    /// Parsed prefix key from config (cached for performance)
292    pub(crate) tmux_prefix_key: Option<crate::tmux::PrefixKey>,
293    /// Prefix key state (whether we're waiting for command key)
294    pub(crate) tmux_prefix_state: crate::tmux::PrefixState,
295    /// Mapping from tmux pane IDs to native pane IDs for output routing
296    pub(crate) tmux_pane_to_native_pane:
297        std::collections::HashMap<crate::tmux::TmuxPaneId, crate::pane::PaneId>,
298    /// Reverse mapping from native pane IDs to tmux pane IDs for input routing
299    pub(crate) native_pane_to_tmux_pane:
300        std::collections::HashMap<crate::pane::PaneId, crate::tmux::TmuxPaneId>,
301
302    // Broadcast input mode
303    /// Whether keyboard input is broadcast to all panes in current tab
304    pub(crate) broadcast_input: bool,
305
306    // Badge overlay
307    /// Badge state for session information display
308    pub(crate) badge_state: BadgeState,
309
310    // Copy mode (vi-style keyboard text selection)
311    /// Copy mode state machine
312    pub(crate) copy_mode: crate::copy_mode::CopyModeState,
313
314    // File transfer state
315    /// File transfer UI state (active transfers, pending saves/uploads, dialog state)
316    pub(crate) file_transfer_state: crate::app::file_transfers::FileTransferState,
317}
318
319/// Extract an `f32` from a JSON value that may be an integer or float.
320fn json_as_f32(value: &serde_json::Value) -> Result<f32, String> {
321    if let Some(f) = value.as_f64() {
322        Ok(f as f32)
323    } else if let Some(i) = value.as_i64() {
324        Ok(i as f32)
325    } else {
326        Err("expected number".to_string())
327    }
328}
329
330impl WindowState {
331    /// Create a new window state with the given configuration
332    pub fn new(config: Config, runtime: Arc<Runtime>) -> Self {
333        let keybinding_registry = KeybindingRegistry::from_config(&config.keybindings);
334        let shaders_dir = Config::shaders_dir();
335        let tmux_prefix_key = crate::tmux::PrefixKey::parse(&config.tmux_prefix_key);
336
337        let mut input_handler = InputHandler::new();
338        // Initialize Option/Alt key modes from config
339        input_handler
340            .update_option_key_modes(config.left_option_key_mode, config.right_option_key_mode);
341
342        // Load profiles from disk
343        let profile_manager = match profile_storage::load_profiles() {
344            Ok(manager) => manager,
345            Err(e) => {
346                log::warn!("Failed to load profiles: {}", e);
347                ProfileManager::new()
348            }
349        };
350
351        // Create badge state and AI inspector before moving config
352        let badge_state = BadgeState::new(&config);
353        let ai_inspector = AIInspectorPanel::new(&config);
354
355        // Discover available ACP agents
356        let config_dir = dirs::config_dir().unwrap_or_default().join("par-term");
357        let available_agents = discover_agents(&config_dir);
358        let command_history_max = config.command_history_max_entries;
359
360        Self {
361            config,
362            window: None,
363            renderer: None,
364            input_handler,
365            runtime,
366
367            tab_manager: TabManager::new(),
368            tab_bar_ui: TabBarUI::new(),
369            tmux_status_bar_ui: TmuxStatusBarUI::new(),
370            status_bar_ui: StatusBarUI::new(),
371
372            debug: DebugState::new(),
373
374            cursor_opacity: 1.0,
375            last_cursor_blink: None,
376            last_key_press: None,
377            is_fullscreen: false,
378            egui_ctx: None,
379            egui_state: None,
380            pending_egui_events: Vec::new(),
381            egui_initialized: false,
382            shader_metadata_cache: ShaderMetadataCache::with_shaders_dir(shaders_dir.clone()),
383            cursor_shader_metadata_cache: CursorShaderMetadataCache::with_shaders_dir(shaders_dir),
384            help_ui: HelpUI::new(),
385            clipboard_history_ui: ClipboardHistoryUI::new(),
386            command_history_ui: CommandHistoryUI::new(),
387            command_history: {
388                let mut ch = CommandHistory::new(command_history_max);
389                ch.load();
390                ch
391            },
392            synced_commands: std::collections::HashSet::new(),
393            paste_special_ui: PasteSpecialUI::new(),
394            tmux_session_picker_ui: TmuxSessionPickerUI::new(),
395            search_ui: SearchUI::new(),
396            ai_inspector,
397            last_inspector_width: 0.0,
398            agent_rx: None,
399            agent_tx: None,
400            agent: None,
401            agent_client: None,
402            available_agents,
403            shader_install_ui: ShaderInstallUI::new(),
404            shader_install_receiver: None,
405            integrations_ui: IntegrationsUI::new(),
406            close_confirmation_ui: CloseConfirmationUI::new(),
407            quit_confirmation_ui: QuitConfirmationUI::new(),
408            remote_shell_install_ui: RemoteShellInstallUI::new(),
409            ssh_connect_ui: SshConnectUI::new(),
410            is_recording: false,
411            recording_start_time: None,
412            is_shutting_down: false,
413            window_index: 1, // Will be set by WindowManager when window is created
414
415            needs_redraw: true,
416            config_changed_by_agent: false,
417            cursor_blink_timer: None,
418            pending_font_rebuild: false,
419
420            is_focused: true, // Assume focused on creation
421            last_render_time: None,
422
423            cursor_hidden_since: None,
424            flicker_pending_render: false,
425
426            throughput_batch_start: None,
427
428            shader_watcher: None,
429            config_watcher: None,
430            config_update_watcher: None,
431            shader_reload_error: None,
432            background_shader_reload_result: None,
433            cursor_shader_reload_result: None,
434
435            open_settings_window_requested: false,
436            pending_arrangement_restore: None,
437            reload_dynamic_profiles_requested: false,
438
439            profile_manager,
440            profile_drawer_ui: ProfileDrawerUI::new(),
441            open_settings_profiles_tab: false,
442            profiles_menu_needs_update: true, // Update menu on startup
443            ui_consumed_mouse_press: false,
444            focus_click_pending: false,
445
446            resize_overlay_visible: false,
447            resize_overlay_hide_time: None,
448            resize_dimensions: None,
449
450            toast_message: None,
451            toast_hide_time: None,
452            pane_identify_hide_time: None,
453            closed_tabs: std::collections::VecDeque::new(),
454
455            keybinding_registry,
456
457            smart_selection_cache: SmartSelectionCache::new(),
458
459            tmux_session: None,
460            tmux_sync: TmuxSync::new(),
461            tmux_session_name: None,
462            tmux_gateway_tab_id: None,
463            tmux_prefix_key,
464            tmux_prefix_state: crate::tmux::PrefixState::new(),
465            tmux_pane_to_native_pane: std::collections::HashMap::new(),
466            native_pane_to_tmux_pane: std::collections::HashMap::new(),
467
468            broadcast_input: false,
469
470            badge_state,
471
472            copy_mode: crate::copy_mode::CopyModeState::new(),
473
474            file_transfer_state: crate::app::file_transfers::FileTransferState::default(),
475        }
476    }
477
478    /// Format window title with optional window number
479    /// This should be used everywhere a title is set to ensure consistency
480    pub(crate) fn format_title(&self, base_title: &str) -> String {
481        if self.config.show_window_number {
482            format!("{} [{}]", base_title, self.window_index)
483        } else {
484            base_title.to_string()
485        }
486    }
487
488    // ========================================================================
489    // Active Tab State Accessors (compatibility - may be useful later)
490    // ========================================================================
491    #[allow(dead_code)]
492    pub(crate) fn terminal(
493        &self,
494    ) -> Option<&Arc<tokio::sync::Mutex<crate::terminal::TerminalManager>>> {
495        self.active_terminal()
496    }
497
498    #[allow(dead_code)]
499    pub(crate) fn scroll_state(&self) -> Option<&crate::scroll_state::ScrollState> {
500        self.tab_manager.active_tab().map(|t| &t.scroll_state)
501    }
502
503    #[allow(dead_code)]
504    pub(crate) fn scroll_state_mut(&mut self) -> Option<&mut crate::scroll_state::ScrollState> {
505        self.tab_manager
506            .active_tab_mut()
507            .map(|t| &mut t.scroll_state)
508    }
509
510    #[allow(dead_code)]
511    pub(crate) fn mouse(&self) -> Option<&crate::app::mouse::MouseState> {
512        self.tab_manager.active_tab().map(|t| &t.mouse)
513    }
514
515    #[allow(dead_code)]
516    pub(crate) fn mouse_mut(&mut self) -> Option<&mut crate::app::mouse::MouseState> {
517        self.tab_manager.active_tab_mut().map(|t| &mut t.mouse)
518    }
519
520    #[allow(dead_code)]
521    pub(crate) fn bell(&self) -> Option<&crate::app::bell::BellState> {
522        self.tab_manager.active_tab().map(|t| &t.bell)
523    }
524
525    #[allow(dead_code)]
526    pub(crate) fn bell_mut(&mut self) -> Option<&mut crate::app::bell::BellState> {
527        self.tab_manager.active_tab_mut().map(|t| &mut t.bell)
528    }
529
530    #[allow(dead_code)]
531    pub(crate) fn cache(&self) -> Option<&crate::app::render_cache::RenderCache> {
532        self.tab_manager.active_tab().map(|t| &t.cache)
533    }
534
535    #[allow(dead_code)]
536    pub(crate) fn cache_mut(&mut self) -> Option<&mut crate::app::render_cache::RenderCache> {
537        self.tab_manager.active_tab_mut().map(|t| &mut t.cache)
538    }
539
540    #[allow(dead_code)]
541    pub(crate) fn refresh_task(&self) -> Option<&Option<tokio::task::JoinHandle<()>>> {
542        self.tab_manager.active_tab().map(|t| &t.refresh_task)
543    }
544
545    #[allow(dead_code)]
546    pub(crate) fn abort_refresh_task(&mut self) {
547        if let Some(tab) = self.tab_manager.active_tab_mut()
548            && let Some(task) = tab.refresh_task.take()
549        {
550            task.abort();
551        }
552    }
553
554    /// Extract a substring based on character columns to avoid UTF-8 slicing panics
555    pub(crate) fn extract_columns(line: &str, start_col: usize, end_col: Option<usize>) -> String {
556        let mut extracted = String::new();
557        let end_bound = end_col.unwrap_or(usize::MAX);
558
559        if start_col > end_bound {
560            return extracted;
561        }
562
563        for (idx, ch) in line.chars().enumerate() {
564            if idx > end_bound {
565                break;
566            }
567
568            if idx >= start_col {
569                extracted.push(ch);
570            }
571        }
572
573        extracted
574    }
575
576    // ========================================================================
577    // DRY Helper Methods
578    // ========================================================================
579
580    /// Invalidate the active tab's cell cache, forcing regeneration on next render
581    #[inline]
582    pub(crate) fn invalidate_tab_cache(&mut self) {
583        if let Some(tab) = self.tab_manager.active_tab_mut() {
584            tab.cache.cells = None;
585        }
586    }
587
588    /// Request window redraw if window exists
589    #[inline]
590    pub(crate) fn request_redraw(&self) {
591        if let Some(window) = &self.window {
592            crate::debug_trace!("REDRAW", "request_redraw called");
593            window.request_redraw();
594        } else {
595            crate::debug_trace!("REDRAW", "request_redraw called but no window");
596        }
597    }
598
599    /// Invalidate cache and request redraw (common pattern after state changes)
600    #[inline]
601    #[allow(dead_code)] // Available for future use, cannot be used inside renderer borrow blocks
602    pub(crate) fn invalidate_and_redraw(&mut self) {
603        self.invalidate_tab_cache();
604        self.needs_redraw = true;
605        self.request_redraw();
606    }
607
608    /// Clear renderer cells and invalidate cache (used when switching tabs)
609    pub(crate) fn clear_and_invalidate(&mut self) {
610        if let Some(renderer) = &mut self.renderer {
611            renderer.clear_all_cells();
612        }
613        self.invalidate_tab_cache();
614        self.needs_redraw = true;
615        self.request_redraw();
616    }
617
618    /// Rebuild the renderer after font-related changes and resize the terminal accordingly
619    pub(crate) fn rebuild_renderer(&mut self) -> Result<()> {
620        use crate::app::renderer_init::RendererInitParams;
621
622        let window = if let Some(w) = &self.window {
623            Arc::clone(w)
624        } else {
625            return Ok(()); // Nothing to rebuild yet
626        };
627
628        // Create renderer using DRY init params
629        let theme = self.config.load_theme();
630        // Get shader metadata from cache for full 3-tier resolution
631        let metadata = self
632            .config
633            .custom_shader
634            .as_ref()
635            .and_then(|name| self.shader_metadata_cache.get(name).cloned());
636        // Get cursor shader metadata from cache for full 3-tier resolution
637        let cursor_metadata = self
638            .config
639            .cursor_shader
640            .as_ref()
641            .and_then(|name| self.cursor_shader_metadata_cache.get(name).cloned());
642        let params = RendererInitParams::from_config(
643            &self.config,
644            &theme,
645            metadata.as_ref(),
646            cursor_metadata.as_ref(),
647        );
648
649        // Drop the old renderer BEFORE creating a new one.
650        // wgpu only allows one surface per window, so the old surface must be
651        // released before we can create a new one.
652        self.renderer = None;
653
654        let mut renderer = self
655            .runtime
656            .block_on(params.create_renderer(Arc::clone(&window)))?;
657
658        let (cols, rows) = renderer.grid_size();
659        let cell_width = renderer.cell_width();
660        let cell_height = renderer.cell_height();
661        let width_px = (cols as f32 * cell_width) as usize;
662        let height_px = (rows as f32 * cell_height) as usize;
663
664        // Resize all tabs' terminals
665        for tab in self.tab_manager.tabs_mut() {
666            if let Ok(mut term) = tab.terminal.try_lock() {
667                let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
668                term.set_cell_dimensions(cell_width as u32, cell_height as u32);
669                term.set_theme(self.config.load_theme());
670            }
671            tab.cache.cells = None;
672        }
673
674        // Apply cursor shader configuration
675        self.apply_cursor_shader_config(&mut renderer, &params);
676
677        self.renderer = Some(renderer);
678        self.needs_redraw = true;
679
680        // Re-apply AI Inspector panel inset to the new renderer.
681        // The old renderer had the correct content_inset_right but the new one
682        // starts with 0.0. Force last_inspector_width to 0 so sync detects the change.
683        self.last_inspector_width = 0.0;
684        self.sync_ai_inspector_width();
685
686        // Reset egui with preserved memory (window positions, collapse state)
687        self.init_egui(&window, true);
688        self.request_redraw();
689
690        Ok(())
691    }
692
693    /// Initialize the window asynchronously
694    pub(crate) async fn initialize_async(&mut self, window: Window) -> Result<()> {
695        use crate::app::renderer_init::RendererInitParams;
696
697        // Enable IME (Input Method Editor) to receive all character events including Space
698        window.set_ime_allowed(true);
699        log::debug!("IME enabled for character input");
700
701        // Detect system theme at startup and apply if auto_dark_mode is enabled
702        if self.config.auto_dark_mode {
703            let is_dark = window
704                .theme()
705                .is_none_or(|t| t == winit::window::Theme::Dark);
706            if self.config.apply_system_theme(is_dark) {
707                log::info!(
708                    "Auto dark mode: detected {} system theme, using theme: {}",
709                    if is_dark { "dark" } else { "light" },
710                    self.config.theme
711                );
712            }
713        }
714
715        // Detect system theme at startup and apply tab style if tab_style is Automatic
716        {
717            let is_dark = window
718                .theme()
719                .is_none_or(|t| t == winit::window::Theme::Dark);
720            if self.config.apply_system_tab_style(is_dark) {
721                log::info!(
722                    "Auto tab style: detected {} system theme, applying {} tab style",
723                    if is_dark { "dark" } else { "light" },
724                    if is_dark {
725                        self.config.dark_tab_style.display_name()
726                    } else {
727                        self.config.light_tab_style.display_name()
728                    }
729                );
730            }
731        }
732
733        let window = Arc::new(window);
734
735        // Initialize egui context and state (no memory to preserve on first init)
736        self.init_egui(&window, false);
737
738        // Create renderer using DRY init params
739        let theme = self.config.load_theme();
740        // Get shader metadata from cache for full 3-tier resolution
741        let metadata = self
742            .config
743            .custom_shader
744            .as_ref()
745            .and_then(|name| self.shader_metadata_cache.get(name).cloned());
746        // Get cursor shader metadata from cache for full 3-tier resolution
747        let cursor_metadata = self
748            .config
749            .cursor_shader
750            .as_ref()
751            .and_then(|name| self.cursor_shader_metadata_cache.get(name).cloned());
752        let params = RendererInitParams::from_config(
753            &self.config,
754            &theme,
755            metadata.as_ref(),
756            cursor_metadata.as_ref(),
757        );
758        let mut renderer = params.create_renderer(Arc::clone(&window)).await?;
759
760        // macOS: Configure CAMetalLayer (transparency + performance)
761        // This MUST be done AFTER creating the wgpu surface/renderer
762        // so that the CAMetalLayer has been created by wgpu
763        #[cfg(target_os = "macos")]
764        {
765            if let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(&window) {
766                log::warn!("Failed to configure Metal layer: {}", e);
767                log::warn!(
768                    "Continuing anyway - may experience reduced FPS or missing transparency on macOS"
769                );
770            }
771            // Set initial layer opacity to match config (content only, frame unaffected)
772            if let Err(e) = crate::macos_metal::set_layer_opacity(&window, 1.0) {
773                log::warn!("Failed to set initial Metal layer opacity: {}", e);
774            }
775            // Apply initial blur settings if enabled
776            if self.config.blur_enabled
777                && self.config.window_opacity < 1.0
778                && let Err(e) = crate::macos_blur::set_window_blur(&window, self.config.blur_radius)
779            {
780                log::warn!("Failed to set initial window blur: {}", e);
781            }
782        }
783
784        // Apply cursor shader configuration
785        self.apply_cursor_shader_config(&mut renderer, &params);
786
787        // Set tab bar offsets BEFORE creating the first tab
788        // This ensures the terminal is sized correctly from the start
789        // Use 1 as tab count since we're about to create the first tab
790        let initial_tab_bar_height = self.tab_bar_ui.get_height(1, &self.config);
791        let initial_tab_bar_width = self.tab_bar_ui.get_width(1, &self.config);
792        let (initial_cols, initial_rows) = renderer.grid_size();
793        log::info!(
794            "Tab bar init: mode={:?}, position={:?}, height={:.1}, width={:.1}, initial_grid={}x{}, content_offset_y_before={:.1}",
795            self.config.tab_bar_mode,
796            self.config.tab_bar_position,
797            initial_tab_bar_height,
798            initial_tab_bar_width,
799            initial_cols,
800            initial_rows,
801            renderer.content_offset_y()
802        );
803        self.apply_tab_bar_offsets(&mut renderer, initial_tab_bar_height, initial_tab_bar_width);
804
805        // Get the renderer's grid size BEFORE storing it (and before creating tabs)
806        // This ensures the shell starts with correct dimensions that account for tab bar
807        let (renderer_cols, renderer_rows) = renderer.grid_size();
808        let cell_width = renderer.cell_width();
809        let cell_height = renderer.cell_height();
810
811        self.window = Some(Arc::clone(&window));
812        self.renderer = Some(renderer);
813
814        // Initialize shader watcher if hot reload is enabled
815        self.init_shader_watcher();
816
817        // Initialize config file watcher for automatic reload
818        self.init_config_watcher();
819
820        // Initialize config-update file watcher (MCP server writes here)
821        self.init_config_update_watcher();
822
823        // Sync status bar monitor state based on config
824        self.status_bar_ui.sync_monitor_state(&self.config);
825
826        // Create the first tab with the correct grid size from the renderer
827        // This ensures the shell is spawned with dimensions that account for tab bar
828        log::info!(
829            "Creating first tab with grid size {}x{} (accounting for tab bar)",
830            renderer_cols,
831            renderer_rows
832        );
833        let tab_id = self.tab_manager.new_tab(
834            &self.config,
835            Arc::clone(&self.runtime),
836            false,                                // First tab doesn't inherit cwd
837            Some((renderer_cols, renderer_rows)), // Pass correct grid size
838        )?;
839
840        // Set cell dimensions on the terminal (for TIOCGWINSZ pixel size reporting)
841        if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
842            let width_px = (renderer_cols as f32 * cell_width) as usize;
843            let height_px = (renderer_rows as f32 * cell_height) as usize;
844
845            if let Ok(mut term) = tab.terminal.try_lock() {
846                term.set_cell_dimensions(cell_width as u32, cell_height as u32);
847                // Send resize to ensure PTY has correct pixel dimensions
848                let _ = term.resize_with_pixels(renderer_cols, renderer_rows, width_px, height_px);
849                log::info!(
850                    "Initial terminal dimensions: {}x{} ({}x{} px)",
851                    renderer_cols,
852                    renderer_rows,
853                    width_px,
854                    height_px
855                );
856            }
857
858            // Start refresh task for the first tab
859            tab.start_refresh_task(
860                Arc::clone(&self.runtime),
861                Arc::clone(&window),
862                self.config.max_fps,
863            );
864        }
865
866        // Auto-connect agent if panel is open on startup and auto-launch is enabled
867        if self.ai_inspector.open {
868            self.try_auto_connect_agent();
869        }
870
871        // Check if we should prompt user to install integrations (shaders and/or shell integration)
872        if self.config.should_prompt_integrations() {
873            log::info!("Integrations not installed - showing welcome dialog");
874            self.integrations_ui.show_dialog();
875            self.needs_redraw = true;
876            window.request_redraw();
877        }
878
879        Ok(())
880    }
881
882    /// Force surface reconfiguration - useful when rendering becomes corrupted
883    /// after moving between monitors or when automatic detection fails.
884    /// Also clears glyph cache to ensure fonts render correctly.
885    pub(crate) fn force_surface_reconfigure(&mut self) {
886        log::info!("Force surface reconfigure triggered");
887
888        if let Some(renderer) = &mut self.renderer {
889            // Reconfigure the surface
890            renderer.reconfigure_surface();
891
892            // Clear glyph cache to force re-rasterization at correct DPI
893            renderer.clear_glyph_cache();
894
895            // Invalidate cached cells to force full re-render
896            if let Some(tab) = self.tab_manager.active_tab_mut() {
897                tab.cache.cells = None;
898            }
899        }
900
901        // On macOS, reconfigure the Metal layer
902        #[cfg(target_os = "macos")]
903        {
904            if let Some(window) = &self.window
905                && let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(window)
906            {
907                log::warn!("Failed to reconfigure Metal layer: {}", e);
908            }
909        }
910
911        // Request redraw
912        self.needs_redraw = true;
913        self.request_redraw();
914    }
915
916    // ========================================================================
917    // Tab Bar Offsets
918    // ========================================================================
919
920    /// Apply tab bar offsets based on the current position configuration.
921    /// Sets content_offset_y (top), content_offset_x (left), and content_inset_bottom (bottom).
922    /// Returns Some((cols, rows)) if any offset changed and caused a grid resize.
923    pub(crate) fn apply_tab_bar_offsets(
924        &self,
925        renderer: &mut crate::renderer::Renderer,
926        tab_bar_height: f32,
927        tab_bar_width: f32,
928    ) -> Option<(usize, usize)> {
929        Self::apply_tab_bar_offsets_for_position(
930            self.config.tab_bar_position,
931            renderer,
932            tab_bar_height,
933            tab_bar_width,
934        )
935    }
936
937    /// Static helper to apply tab bar offsets (avoids borrowing self).
938    pub(crate) fn apply_tab_bar_offsets_for_position(
939        position: crate::config::TabBarPosition,
940        renderer: &mut crate::renderer::Renderer,
941        tab_bar_height: f32,
942        tab_bar_width: f32,
943    ) -> Option<(usize, usize)> {
944        use crate::config::TabBarPosition;
945        let (offset_y, offset_x, inset_bottom) = match position {
946            TabBarPosition::Top => (tab_bar_height, 0.0, 0.0),
947            TabBarPosition::Bottom => (0.0, 0.0, tab_bar_height),
948            TabBarPosition::Left => (0.0, tab_bar_width, 0.0),
949        };
950
951        let mut result = None;
952        if let Some(grid) = renderer.set_content_offset_y(offset_y) {
953            result = Some(grid);
954        }
955        if let Some(grid) = renderer.set_content_offset_x(offset_x) {
956            result = Some(grid);
957        }
958        if let Some(grid) = renderer.set_content_inset_bottom(inset_bottom) {
959            result = Some(grid);
960        }
961        result
962    }
963
964    // AI Inspector Panel Width Sync
965    // ========================================================================
966
967    /// Sync the AI Inspector panel consumed width with the renderer.
968    ///
969    /// When the panel opens, closes, or is resized by dragging, the terminal
970    /// column count must be updated so text reflows to fit the available space.
971    /// This method checks whether the consumed width has changed and, if so,
972    /// updates the renderer's right content inset and resizes all terminals.
973    pub(crate) fn sync_ai_inspector_width(&mut self) {
974        let current_width = self.ai_inspector.consumed_width();
975
976        if let Some(renderer) = &mut self.renderer {
977            // Always verify the renderer's content_inset_right matches the expected
978            // physical value. This catches cases where content_inset_right was reset
979            // (e.g., renderer rebuild, scale factor change) even when the logical
980            // panel width hasn't changed.
981            // The renderer's set_content_inset_right() has its own guard that only
982            // triggers a resize when the physical value actually differs.
983            if let Some((new_cols, new_rows)) = renderer.set_content_inset_right(current_width) {
984                let cell_width = renderer.cell_width();
985                let cell_height = renderer.cell_height();
986                let width_px = (new_cols as f32 * cell_width) as usize;
987                let height_px = (new_rows as f32 * cell_height) as usize;
988
989                for tab in self.tab_manager.tabs_mut() {
990                    if let Ok(mut term) = tab.terminal.try_lock() {
991                        term.set_cell_dimensions(cell_width as u32, cell_height as u32);
992                        let _ = term.resize_with_pixels(new_cols, new_rows, width_px, height_px);
993                    }
994                    tab.cache.cells = None;
995                }
996
997                crate::debug_info!(
998                    "AI_INSPECTOR",
999                    "Panel width synced to {:.0}px, resized terminals to {}x{}",
1000                    current_width,
1001                    new_cols,
1002                    new_rows
1003                );
1004                self.needs_redraw = true;
1005            } else if (current_width - self.last_inspector_width).abs() >= 1.0 {
1006                // Logical width changed but physical grid didn't resize
1007                // (could happen with very small changes below cell width threshold)
1008                self.needs_redraw = true;
1009            }
1010        }
1011
1012        // Persist panel width to config when the user finishes resizing.
1013        if !self.ai_inspector.is_resizing()
1014            && (current_width - self.last_inspector_width).abs() >= 1.0
1015            && current_width > 0.0
1016            && self.ai_inspector.open
1017        {
1018            self.config.ai_inspector_width = self.ai_inspector.width;
1019            // Save to disk so the width is remembered across sessions.
1020            if let Err(e) = self.config.save() {
1021                log::error!("Failed to save AI inspector width: {}", e);
1022            }
1023        }
1024
1025        self.last_inspector_width = current_width;
1026    }
1027
1028    /// Connect to an ACP agent by identity string.
1029    ///
1030    /// This extracts the agent connection logic so it can be called both from
1031    /// `InspectorAction::ConnectAgent` and from the auto-connect-on-open path.
1032    pub(crate) fn connect_agent(&mut self, identity: &str) {
1033        if let Some(agent_config) = self
1034            .available_agents
1035            .iter()
1036            .find(|a| a.identity == identity)
1037        {
1038            // Clean up any previous agent before starting a new connection.
1039            if let Some(old_agent) = self.agent.take() {
1040                let runtime = self.runtime.clone();
1041                runtime.spawn(async move {
1042                    let mut agent = old_agent.lock().await;
1043                    agent.disconnect().await;
1044                });
1045            }
1046            self.agent_rx = None;
1047            self.agent_tx = None;
1048            self.agent_client = None;
1049
1050            let (tx, rx) = mpsc::unbounded_channel();
1051            self.agent_rx = Some(rx);
1052            self.agent_tx = Some(tx.clone());
1053            let ui_tx = tx.clone();
1054            let safe_paths = SafePaths {
1055                config_dir: Config::config_dir(),
1056                shaders_dir: Config::shaders_dir(),
1057            };
1058            let mcp_server_bin =
1059                std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("par-term"));
1060            let agent = Agent::new(agent_config.clone(), tx, safe_paths, mcp_server_bin);
1061            agent.auto_approve.store(
1062                self.config.ai_inspector_auto_approve,
1063                std::sync::atomic::Ordering::Relaxed,
1064            );
1065            let agent = Arc::new(tokio::sync::Mutex::new(agent));
1066            self.agent = Some(agent.clone());
1067
1068            // Determine CWD for the agent session
1069            let fallback_cwd = std::env::current_dir()
1070                .unwrap_or_default()
1071                .to_string_lossy()
1072                .to_string();
1073            let cwd = if let Some(tab) = self.tab_manager.active_tab() {
1074                if let Ok(term) = tab.terminal.try_lock() {
1075                    term.shell_integration_cwd()
1076                        .unwrap_or_else(|| fallback_cwd.clone())
1077                } else {
1078                    fallback_cwd.clone()
1079                }
1080            } else {
1081                fallback_cwd
1082            };
1083
1084            let capabilities = ClientCapabilities {
1085                fs: FsCapabilities {
1086                    read_text_file: true,
1087                    write_text_file: true,
1088                    list_directory: true,
1089                    find: true,
1090                },
1091                terminal: self.config.ai_inspector_agent_terminal_access,
1092                config: true,
1093            };
1094
1095            let auto_approve = self.config.ai_inspector_auto_approve;
1096            let runtime = self.runtime.clone();
1097            runtime.spawn(async move {
1098                let mut agent = agent.lock().await;
1099                if let Err(e) = agent.connect(&cwd, capabilities).await {
1100                    log::error!("ACP: failed to connect to agent: {e}");
1101                    return;
1102                }
1103                if let Some(client) = &agent.client {
1104                    let _ = ui_tx.send(AgentMessage::ClientReady(Arc::clone(client)));
1105                }
1106                if auto_approve && let Err(e) = agent.set_mode("bypassPermissions").await {
1107                    log::error!("ACP: failed to set bypassPermissions mode: {e}");
1108                }
1109            });
1110        }
1111    }
1112
1113    /// Auto-connect to the configured agent if auto-launch is enabled and no agent is connected.
1114    pub(crate) fn try_auto_connect_agent(&mut self) {
1115        if self.config.ai_inspector_auto_launch
1116            && self.ai_inspector.agent_status == AgentStatus::Disconnected
1117            && self.agent.is_none()
1118        {
1119            let identity = self.config.ai_inspector_agent.clone();
1120            if !identity.is_empty() {
1121                log::info!("ACP: auto-connecting to agent '{}'", identity);
1122                self.connect_agent(&identity);
1123            }
1124        }
1125    }
1126
1127    // Status Bar Inset Sync
1128    // ========================================================================
1129
1130    /// Sync the status bar bottom inset with the renderer so that the terminal
1131    /// grid does not extend behind the status bar.
1132    ///
1133    /// Must be called before cells are gathered each frame so the grid size
1134    /// is correct. Only triggers a terminal resize when the height changes
1135    /// (e.g., status bar toggled on/off or height changed in settings).
1136    pub(crate) fn sync_status_bar_inset(&mut self) {
1137        let is_tmux = self.is_tmux_connected();
1138        let tmux_bar = crate::tmux_status_bar_ui::TmuxStatusBarUI::height(&self.config, is_tmux);
1139        let custom_bar = self.status_bar_ui.height(&self.config, self.is_fullscreen);
1140        let total = tmux_bar + custom_bar;
1141
1142        if let Some(renderer) = &mut self.renderer
1143            && let Some((new_cols, new_rows)) = renderer.set_egui_bottom_inset(total)
1144        {
1145            let cell_width = renderer.cell_width();
1146            let cell_height = renderer.cell_height();
1147            let width_px = (new_cols as f32 * cell_width) as usize;
1148            let height_px = (new_rows as f32 * cell_height) as usize;
1149
1150            for tab in self.tab_manager.tabs_mut() {
1151                if let Ok(mut term) = tab.terminal.try_lock() {
1152                    term.set_cell_dimensions(cell_width as u32, cell_height as u32);
1153                    let _ = term.resize_with_pixels(new_cols, new_rows, width_px, height_px);
1154                }
1155                tab.cache.cells = None;
1156            }
1157        }
1158    }
1159
1160    // Shader Hot Reload
1161    // ========================================================================
1162
1163    /// Initialize the shader watcher for hot reload support
1164    pub(crate) fn init_shader_watcher(&mut self) {
1165        debug_info!(
1166            "SHADER",
1167            "init_shader_watcher: hot_reload={}",
1168            self.config.shader_hot_reload
1169        );
1170
1171        if !self.config.shader_hot_reload {
1172            log::debug!("Shader hot reload disabled");
1173            return;
1174        }
1175
1176        let background_path = self
1177            .config
1178            .custom_shader
1179            .as_ref()
1180            .filter(|_| self.config.custom_shader_enabled)
1181            .map(|s| Config::shader_path(s));
1182
1183        let cursor_path = self
1184            .config
1185            .cursor_shader
1186            .as_ref()
1187            .filter(|_| self.config.cursor_shader_enabled)
1188            .map(|s| Config::shader_path(s));
1189
1190        debug_info!(
1191            "SHADER",
1192            "Shader paths: background={:?}, cursor={:?}",
1193            background_path,
1194            cursor_path
1195        );
1196
1197        if background_path.is_none() && cursor_path.is_none() {
1198            debug_info!("SHADER", "No shaders to watch for hot reload");
1199            return;
1200        }
1201
1202        match ShaderWatcher::new(
1203            background_path.as_deref(),
1204            cursor_path.as_deref(),
1205            self.config.shader_hot_reload_delay,
1206        ) {
1207            Ok(watcher) => {
1208                debug_info!(
1209                    "SHADER",
1210                    "Shader hot reload initialized (debounce: {}ms)",
1211                    self.config.shader_hot_reload_delay
1212                );
1213                self.shader_watcher = Some(watcher);
1214            }
1215            Err(e) => {
1216                debug_info!("SHADER", "Failed to initialize shader hot reload: {}", e);
1217            }
1218        }
1219    }
1220
1221    /// Reinitialize shader watcher when shader paths change
1222    pub(crate) fn reinit_shader_watcher(&mut self) {
1223        debug_info!(
1224            "SHADER",
1225            "reinit_shader_watcher CALLED: shader={:?}, cursor={:?}",
1226            self.config.custom_shader,
1227            self.config.cursor_shader
1228        );
1229        // Drop existing watcher
1230        self.shader_watcher = None;
1231        self.shader_reload_error = None;
1232
1233        // Reinitialize if hot reload is still enabled
1234        self.init_shader_watcher();
1235    }
1236
1237    /// Initialize the config file watcher for automatic reload.
1238    ///
1239    /// Watches `config.yaml` for changes so that when an ACP agent modifies
1240    /// the config, par-term can auto-reload shader and other settings.
1241    pub(crate) fn init_config_watcher(&mut self) {
1242        let config_path = Config::config_path();
1243        if !config_path.exists() {
1244            debug_info!("CONFIG", "Config file does not exist, skipping watcher");
1245            return;
1246        }
1247        match crate::config::watcher::ConfigWatcher::new(&config_path, 500) {
1248            Ok(watcher) => {
1249                debug_info!("CONFIG", "Config watcher initialized");
1250                self.config_watcher = Some(watcher);
1251            }
1252            Err(e) => {
1253                debug_info!("CONFIG", "Failed to initialize config watcher: {}", e);
1254            }
1255        }
1256    }
1257
1258    /// Initialize the watcher for `.config-update.json` (MCP server config updates).
1259    ///
1260    /// The MCP server (spawned by the ACP agent) writes config updates to this
1261    /// file. We watch it, apply the updates in-memory, and delete it.
1262    pub(crate) fn init_config_update_watcher(&mut self) {
1263        let update_path = Config::config_dir().join(".config-update.json");
1264
1265        // Create the file if it doesn't exist so the watcher can start
1266        if !update_path.exists() {
1267            if let Some(parent) = update_path.parent() {
1268                let _ = std::fs::create_dir_all(parent);
1269            }
1270            let _ = std::fs::write(&update_path, "");
1271        }
1272
1273        match crate::config::watcher::ConfigWatcher::new(&update_path, 200) {
1274            Ok(watcher) => {
1275                debug_info!("CONFIG", "Config-update watcher initialized");
1276                self.config_update_watcher = Some(watcher);
1277            }
1278            Err(e) => {
1279                debug_info!(
1280                    "CONFIG",
1281                    "Failed to initialize config-update watcher: {}",
1282                    e
1283                );
1284            }
1285        }
1286    }
1287
1288    /// Check for pending config update file changes (from MCP server).
1289    ///
1290    /// When the MCP server writes `.config-update.json`, this reads it,
1291    /// applies the updates in-memory, saves to disk, and removes the file.
1292    pub(crate) fn check_config_update_file(&mut self) {
1293        let Some(watcher) = &self.config_update_watcher else {
1294            return;
1295        };
1296        if watcher.try_recv().is_none() {
1297            return;
1298        }
1299
1300        let update_path = Config::config_dir().join(".config-update.json");
1301        let content = match std::fs::read_to_string(&update_path) {
1302            Ok(c) if c.trim().is_empty() => return,
1303            Ok(c) => c,
1304            Err(e) => {
1305                log::warn!("CONFIG: failed to read config-update file: {e}");
1306                return;
1307            }
1308        };
1309
1310        match serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(&content)
1311        {
1312            Ok(updates) => {
1313                log::info!(
1314                    "CONFIG: applying MCP config update ({} keys): {:?}",
1315                    updates.len(),
1316                    updates
1317                );
1318                if let Err(e) = self.apply_agent_config_updates(&updates) {
1319                    log::error!("CONFIG: MCP config update failed: {e}");
1320                } else {
1321                    self.config_changed_by_agent = true;
1322                }
1323                self.needs_redraw = true;
1324            }
1325            Err(e) => {
1326                log::error!("CONFIG: invalid JSON in config-update file: {e}");
1327            }
1328        }
1329
1330        // Clear the file so we don't re-process it
1331        let _ = std::fs::write(&update_path, "");
1332    }
1333
1334    /// Check for pending config file changes and apply them.
1335    ///
1336    /// Called periodically from the event loop. On config change:
1337    /// 1. Reloads config from disk
1338    /// 2. Applies shader-related config changes
1339    /// 3. Reinitializes shader watcher if shader paths changed
1340    pub(crate) fn check_config_reload(&mut self) {
1341        let Some(watcher) = &self.config_watcher else {
1342            return;
1343        };
1344        let Some(_event) = watcher.try_recv() else {
1345            return;
1346        };
1347
1348        log::info!("CONFIG: config file changed, reloading...");
1349
1350        match Config::load() {
1351            Ok(new_config) => {
1352                use crate::app::config_updates::ConfigChanges;
1353
1354                let changes = ConfigChanges::detect(&self.config, &new_config);
1355
1356                // Replace the entire in-memory config so that any subsequent
1357                // config.save() writes the agent's changes, not stale values.
1358                self.config = new_config;
1359
1360                log::info!(
1361                    "CONFIG: shader_changed={} cursor_changed={} shader={:?}",
1362                    changes.any_shader_change(),
1363                    changes.any_cursor_shader_toggle(),
1364                    self.config.custom_shader
1365                );
1366
1367                // Apply shader changes to the renderer
1368                if let Some(renderer) = &mut self.renderer {
1369                    if changes.any_shader_change() || changes.shader_per_shader_config {
1370                        log::info!("CONFIG: applying background shader change to renderer");
1371                        let shader_override = self
1372                            .config
1373                            .custom_shader
1374                            .as_ref()
1375                            .and_then(|name| self.config.shader_configs.get(name));
1376                        let metadata = self
1377                            .config
1378                            .custom_shader
1379                            .as_ref()
1380                            .and_then(|name| self.shader_metadata_cache.get(name).cloned());
1381                        let resolved = crate::config::shader_config::resolve_shader_config(
1382                            shader_override,
1383                            metadata.as_ref(),
1384                            &self.config,
1385                        );
1386                        if let Err(e) = renderer.set_custom_shader_enabled(
1387                            self.config.custom_shader_enabled,
1388                            self.config.custom_shader.as_deref(),
1389                            self.config.window_opacity,
1390                            self.config.custom_shader_animation,
1391                            resolved.animation_speed,
1392                            resolved.full_content,
1393                            resolved.brightness,
1394                            &resolved.channel_paths(),
1395                            resolved.cubemap_path().map(|p| p.as_path()),
1396                        ) {
1397                            log::error!("Config reload: shader load failed: {e}");
1398                        }
1399                    }
1400                    if changes.any_cursor_shader_toggle() {
1401                        log::info!("CONFIG: applying cursor shader change to renderer");
1402                        if let Err(e) = renderer.set_cursor_shader_enabled(
1403                            self.config.cursor_shader_enabled,
1404                            self.config.cursor_shader.as_deref(),
1405                            self.config.window_opacity,
1406                            self.config.cursor_shader_animation,
1407                            self.config.cursor_shader_animation_speed,
1408                        ) {
1409                            log::error!("Config reload: cursor shader load failed: {e}");
1410                        }
1411                    }
1412                }
1413
1414                // Reinit shader watcher if paths changed
1415                if changes.needs_watcher_reinit() {
1416                    self.reinit_shader_watcher();
1417                }
1418
1419                self.needs_redraw = true;
1420                debug_info!("CONFIG", "Config reloaded successfully");
1421            }
1422            Err(e) => {
1423                log::error!("Failed to reload config: {}", e);
1424            }
1425        }
1426    }
1427
1428    /// Apply config updates from the ACP agent.
1429    ///
1430    /// Updates the in-memory config, applies changes to the renderer, and
1431    /// saves to disk. Returns `Ok(())` on success or an error string.
1432    fn apply_agent_config_updates(
1433        &mut self,
1434        updates: &std::collections::HashMap<String, serde_json::Value>,
1435    ) -> Result<(), String> {
1436        let mut errors = Vec::new();
1437        let old_config = self.config.clone();
1438
1439        for (key, value) in updates {
1440            if let Err(e) = self.apply_single_config_update(key, value) {
1441                errors.push(format!("{key}: {e}"));
1442            }
1443        }
1444
1445        if !errors.is_empty() {
1446            return Err(errors.join("; "));
1447        }
1448
1449        // Detect changes and apply to renderer
1450        use crate::app::config_updates::ConfigChanges;
1451        let changes = ConfigChanges::detect(&old_config, &self.config);
1452
1453        log::info!(
1454            "ACP config/update: shader_change={} cursor_change={} old_shader={:?} new_shader={:?}",
1455            changes.any_shader_change(),
1456            changes.any_cursor_shader_toggle(),
1457            old_config.custom_shader,
1458            self.config.custom_shader
1459        );
1460
1461        if let Some(renderer) = &mut self.renderer {
1462            if changes.any_shader_change() || changes.shader_per_shader_config {
1463                log::info!("ACP config/update: applying background shader change to renderer");
1464                let shader_override = self
1465                    .config
1466                    .custom_shader
1467                    .as_ref()
1468                    .and_then(|name| self.config.shader_configs.get(name));
1469                let metadata = self
1470                    .config
1471                    .custom_shader
1472                    .as_ref()
1473                    .and_then(|name| self.shader_metadata_cache.get(name).cloned());
1474                let resolved = crate::config::shader_config::resolve_shader_config(
1475                    shader_override,
1476                    metadata.as_ref(),
1477                    &self.config,
1478                );
1479                if let Err(e) = renderer.set_custom_shader_enabled(
1480                    self.config.custom_shader_enabled,
1481                    self.config.custom_shader.as_deref(),
1482                    self.config.window_opacity,
1483                    self.config.custom_shader_animation,
1484                    resolved.animation_speed,
1485                    resolved.full_content,
1486                    resolved.brightness,
1487                    &resolved.channel_paths(),
1488                    resolved.cubemap_path().map(|p| p.as_path()),
1489                ) {
1490                    log::error!("ACP config/update: shader load failed: {e}");
1491                }
1492            }
1493            if changes.any_cursor_shader_toggle() {
1494                log::info!("ACP config/update: applying cursor shader change to renderer");
1495                if let Err(e) = renderer.set_cursor_shader_enabled(
1496                    self.config.cursor_shader_enabled,
1497                    self.config.cursor_shader.as_deref(),
1498                    self.config.window_opacity,
1499                    self.config.cursor_shader_animation,
1500                    self.config.cursor_shader_animation_speed,
1501                ) {
1502                    log::error!("ACP config/update: cursor shader load failed: {e}");
1503                }
1504            }
1505        }
1506
1507        if changes.needs_watcher_reinit() {
1508            self.reinit_shader_watcher();
1509        }
1510
1511        // Save to disk
1512        if let Err(e) = self.config.save() {
1513            return Err(format!("Failed to save config: {e}"));
1514        }
1515
1516        Ok(())
1517    }
1518
1519    /// Apply a single config key/value update to the in-memory config.
1520    fn apply_single_config_update(
1521        &mut self,
1522        key: &str,
1523        value: &serde_json::Value,
1524    ) -> Result<(), String> {
1525        match key {
1526            // -- Background shader --
1527            "custom_shader" => {
1528                self.config.custom_shader = if value.is_null() {
1529                    None
1530                } else {
1531                    Some(value.as_str().ok_or("expected string or null")?.to_string())
1532                };
1533                Ok(())
1534            }
1535            "custom_shader_enabled" => {
1536                self.config.custom_shader_enabled = value.as_bool().ok_or("expected boolean")?;
1537                Ok(())
1538            }
1539            "custom_shader_animation" => {
1540                self.config.custom_shader_animation = value.as_bool().ok_or("expected boolean")?;
1541                Ok(())
1542            }
1543            "custom_shader_animation_speed" => {
1544                self.config.custom_shader_animation_speed = json_as_f32(value)?;
1545                Ok(())
1546            }
1547            "custom_shader_brightness" => {
1548                self.config.custom_shader_brightness = json_as_f32(value)?;
1549                Ok(())
1550            }
1551            "custom_shader_text_opacity" => {
1552                self.config.custom_shader_text_opacity = json_as_f32(value)?;
1553                Ok(())
1554            }
1555            "custom_shader_full_content" => {
1556                self.config.custom_shader_full_content =
1557                    value.as_bool().ok_or("expected boolean")?;
1558                Ok(())
1559            }
1560
1561            // -- Cursor shader --
1562            "cursor_shader" => {
1563                self.config.cursor_shader = if value.is_null() {
1564                    None
1565                } else {
1566                    Some(value.as_str().ok_or("expected string or null")?.to_string())
1567                };
1568                Ok(())
1569            }
1570            "cursor_shader_enabled" => {
1571                self.config.cursor_shader_enabled = value.as_bool().ok_or("expected boolean")?;
1572                Ok(())
1573            }
1574            "cursor_shader_animation" => {
1575                self.config.cursor_shader_animation = value.as_bool().ok_or("expected boolean")?;
1576                Ok(())
1577            }
1578            "cursor_shader_animation_speed" => {
1579                self.config.cursor_shader_animation_speed = json_as_f32(value)?;
1580                Ok(())
1581            }
1582            "cursor_shader_glow_radius" => {
1583                self.config.cursor_shader_glow_radius = json_as_f32(value)?;
1584                Ok(())
1585            }
1586            "cursor_shader_glow_intensity" => {
1587                self.config.cursor_shader_glow_intensity = json_as_f32(value)?;
1588                Ok(())
1589            }
1590            "cursor_shader_trail_duration" => {
1591                self.config.cursor_shader_trail_duration = json_as_f32(value)?;
1592                Ok(())
1593            }
1594            "cursor_shader_hides_cursor" => {
1595                self.config.cursor_shader_hides_cursor =
1596                    value.as_bool().ok_or("expected boolean")?;
1597                Ok(())
1598            }
1599
1600            // -- Window --
1601            "window_opacity" => {
1602                self.config.window_opacity = json_as_f32(value)?;
1603                Ok(())
1604            }
1605            "font_size" => {
1606                self.config.font_size = json_as_f32(value)?;
1607                Ok(())
1608            }
1609
1610            _ => Err(format!("unknown or read-only config key: {key}")),
1611        }
1612    }
1613
1614    /// Check anti-idle timers and send keep-alive codes when due.
1615    ///
1616    /// Returns the next Instant when anti-idle should run, or None if disabled.
1617    pub(crate) fn handle_anti_idle(
1618        &mut self,
1619        now: std::time::Instant,
1620    ) -> Option<std::time::Instant> {
1621        if !self.config.anti_idle_enabled {
1622            return None;
1623        }
1624
1625        let idle_threshold = std::time::Duration::from_secs(self.config.anti_idle_seconds.max(1));
1626        let keep_alive_code = [self.config.anti_idle_code];
1627        let mut next_due: Option<std::time::Instant> = None;
1628
1629        for tab in self.tab_manager.tabs_mut() {
1630            if let Ok(term) = tab.terminal.try_lock() {
1631                // Treat new terminal output as activity
1632                let current_generation = term.update_generation();
1633                if current_generation > tab.anti_idle_last_generation {
1634                    tab.anti_idle_last_generation = current_generation;
1635                    tab.anti_idle_last_activity = now;
1636                }
1637
1638                // If idle long enough, send keep-alive code
1639                if should_send_keep_alive(tab.anti_idle_last_activity, now, idle_threshold) {
1640                    if let Err(e) = term.write(&keep_alive_code) {
1641                        log::warn!(
1642                            "Failed to send anti-idle keep-alive for tab {}: {}",
1643                            tab.id,
1644                            e
1645                        );
1646                    } else {
1647                        tab.anti_idle_last_activity = now;
1648                    }
1649                }
1650
1651                // Compute next due time for this tab
1652                let elapsed = now.duration_since(tab.anti_idle_last_activity);
1653                let remaining = if elapsed >= idle_threshold {
1654                    idle_threshold
1655                } else {
1656                    idle_threshold - elapsed
1657                };
1658                let candidate = now + remaining;
1659                next_due = Some(next_due.map_or(candidate, |prev| prev.min(candidate)));
1660            }
1661        }
1662
1663        next_due
1664    }
1665
1666    /// Check for and handle shader reload events
1667    ///
1668    /// Should be called periodically (e.g., in about_to_wait or render loop).
1669    /// Returns true if a shader was reloaded.
1670    pub(crate) fn check_shader_reload(&mut self) -> bool {
1671        let Some(watcher) = &self.shader_watcher else {
1672            return false;
1673        };
1674
1675        let Some(event) = watcher.try_recv() else {
1676            return false;
1677        };
1678
1679        self.handle_shader_reload_event(event)
1680    }
1681
1682    /// Handle a shader reload event
1683    ///
1684    /// On success: clears errors, triggers redraw, optionally shows notification
1685    /// On failure: preserves the old working shader, logs error, shows notification
1686    fn handle_shader_reload_event(&mut self, event: ShaderReloadEvent) -> bool {
1687        let shader_name = match event.shader_type {
1688            ShaderType::Background => "Background shader",
1689            ShaderType::Cursor => "Cursor shader",
1690        };
1691        let file_name = event
1692            .path
1693            .file_name()
1694            .and_then(|n| n.to_str())
1695            .unwrap_or("shader");
1696
1697        log::info!("Hot reload: {} from {}", shader_name, event.path.display());
1698
1699        // Read the shader source
1700        let source = match std::fs::read_to_string(&event.path) {
1701            Ok(s) => s,
1702            Err(e) => {
1703                let error_msg = format!("Cannot read '{}': {}", file_name, e);
1704                log::error!("Shader hot reload failed: {}", error_msg);
1705                self.shader_reload_error = Some(error_msg.clone());
1706                // Track error for standalone settings window propagation
1707                match event.shader_type {
1708                    ShaderType::Background => {
1709                        self.background_shader_reload_result = Some(Some(error_msg.clone()));
1710                    }
1711                    ShaderType::Cursor => {
1712                        self.cursor_shader_reload_result = Some(Some(error_msg.clone()));
1713                    }
1714                }
1715                // Notify user of the error
1716                self.deliver_notification(
1717                    "Shader Reload Failed",
1718                    &format!("{} - {}", shader_name, error_msg),
1719                );
1720                // Trigger visual bell if enabled to alert user
1721                if self.config.notification_bell_visual
1722                    && let Some(tab) = self.tab_manager.active_tab_mut()
1723                {
1724                    tab.bell.visual_flash = Some(std::time::Instant::now());
1725                }
1726                return false;
1727            }
1728        };
1729
1730        let Some(renderer) = &mut self.renderer else {
1731            log::error!("Cannot reload shader: no renderer available");
1732            return false;
1733        };
1734
1735        // Attempt to reload the shader
1736        // Note: On compilation failure, the old shader pipeline is preserved
1737        let result = match event.shader_type {
1738            ShaderType::Background => renderer.reload_shader_from_source(&source),
1739            ShaderType::Cursor => renderer.reload_cursor_shader_from_source(&source),
1740        };
1741
1742        match result {
1743            Ok(()) => {
1744                log::info!("{} reloaded successfully from {}", shader_name, file_name);
1745                self.shader_reload_error = None;
1746                // Track success for standalone settings window propagation
1747                match event.shader_type {
1748                    ShaderType::Background => {
1749                        self.background_shader_reload_result = Some(None);
1750                    }
1751                    ShaderType::Cursor => {
1752                        self.cursor_shader_reload_result = Some(None);
1753                    }
1754                }
1755                self.needs_redraw = true;
1756                self.request_redraw();
1757                true
1758            }
1759            Err(e) => {
1760                // Extract the most relevant error message from the chain
1761                let root_cause = e.root_cause().to_string();
1762                let error_msg = if root_cause.len() > 200 {
1763                    // Truncate very long error messages
1764                    format!("{}...", &root_cause[..200])
1765                } else {
1766                    root_cause
1767                };
1768
1769                log::error!(
1770                    "{} compilation failed (old shader preserved): {}",
1771                    shader_name,
1772                    error_msg
1773                );
1774                log::debug!("Full error chain: {:#}", e);
1775
1776                self.shader_reload_error = Some(error_msg.clone());
1777                // Track error for standalone settings window propagation
1778                match event.shader_type {
1779                    ShaderType::Background => {
1780                        self.background_shader_reload_result = Some(Some(error_msg.clone()));
1781                    }
1782                    ShaderType::Cursor => {
1783                        self.cursor_shader_reload_result = Some(Some(error_msg.clone()));
1784                    }
1785                }
1786
1787                // Notify user of the compilation error
1788                self.deliver_notification(
1789                    "Shader Compilation Error",
1790                    &format!("{}: {}", file_name, error_msg),
1791                );
1792
1793                // Trigger visual bell if enabled to alert user
1794                if self.config.notification_bell_visual
1795                    && let Some(tab) = self.tab_manager.active_tab_mut()
1796                {
1797                    tab.bell.visual_flash = Some(std::time::Instant::now());
1798                }
1799
1800                false
1801            }
1802        }
1803    }
1804
1805    /// Check if egui is currently using the pointer (mouse is over an egui UI element)
1806    pub(crate) fn is_egui_using_pointer(&self) -> bool {
1807        // AI Inspector resize handle uses direct pointer tracking (not egui widgets),
1808        // so egui doesn't know about it. Check explicitly to prevent mouse events
1809        // from reaching the terminal during resize drag or initial click on the handle.
1810        if self.ai_inspector.wants_pointer() {
1811            return true;
1812        }
1813        // Before first render, egui state is unreliable - allow mouse events through
1814        if !self.egui_initialized {
1815            return false;
1816        }
1817        // Always check egui context - the tab bar is always rendered via egui
1818        // and can consume pointer events (e.g., close button clicks)
1819        if let Some(ctx) = &self.egui_ctx {
1820            ctx.is_using_pointer() || ctx.wants_pointer_input()
1821        } else {
1822            false
1823        }
1824    }
1825
1826    /// Check if any egui overlay with text input is visible.
1827    /// Used to route clipboard operations (paste/copy/select-all) to egui
1828    /// instead of the terminal when a modal dialog is active.
1829    pub(crate) fn has_egui_overlay_visible(&self) -> bool {
1830        self.search_ui.visible
1831            || self.clipboard_history_ui.visible
1832            || self.command_history_ui.visible
1833            || self.shader_install_ui.visible
1834            || self.integrations_ui.visible
1835            || self.ai_inspector.open
1836    }
1837
1838    /// Check if egui is currently using keyboard input (e.g., text input or ComboBox has focus)
1839    pub(crate) fn is_egui_using_keyboard(&self) -> bool {
1840        // If any UI panel is visible, check if egui wants keyboard input
1841        // Note: Settings are handled by standalone SettingsWindow, not embedded UI
1842        // Note: Profile drawer does NOT block input - only modal dialogs do
1843        let any_ui_visible = self.help_ui.visible
1844            || self.clipboard_history_ui.visible
1845            || self.command_history_ui.visible
1846            || self.shader_install_ui.visible
1847            || self.integrations_ui.visible
1848            || self.ai_inspector.open;
1849        if !any_ui_visible {
1850            return false;
1851        }
1852
1853        // Check egui context for keyboard usage
1854        if let Some(ctx) = &self.egui_ctx {
1855            ctx.wants_keyboard_input()
1856        } else {
1857            false
1858        }
1859    }
1860
1861    /// Determine if scrollbar should be visible based on autohide setting and recent activity
1862    pub(crate) fn should_show_scrollbar(&self) -> bool {
1863        let tab = match self.tab_manager.active_tab() {
1864            Some(t) => t,
1865            None => return false,
1866        };
1867
1868        // No scrollbar needed if no scrollback available
1869        if tab.cache.scrollback_len == 0 {
1870            return false;
1871        }
1872
1873        // Always show when dragging or moving
1874        if tab.scroll_state.dragging {
1875            return true;
1876        }
1877
1878        // If autohide disabled, always show
1879        if self.config.scrollbar_autohide_delay == 0 {
1880            return true;
1881        }
1882
1883        // If scrolled away from bottom, keep visible
1884        if tab.scroll_state.offset > 0 || tab.scroll_state.target_offset > 0 {
1885            return true;
1886        }
1887
1888        // Show when pointer is near the scrollbar edge (hover reveal)
1889        if let Some(window) = &self.window {
1890            let padding = 32.0; // px hover band
1891            let width = window.inner_size().width as f64;
1892            let near_right = self.config.scrollbar_position != "left"
1893                && (width - tab.mouse.position.0) <= padding;
1894            let near_left =
1895                self.config.scrollbar_position == "left" && tab.mouse.position.0 <= padding;
1896            if near_left || near_right {
1897                return true;
1898            }
1899        }
1900
1901        // Otherwise, hide after delay
1902        tab.scroll_state.last_activity.elapsed().as_millis()
1903            < self.config.scrollbar_autohide_delay as u128
1904    }
1905
1906    /// Update cursor blink state based on configured interval and DECSCUSR style
1907    ///
1908    /// The cursor blink state is determined by:
1909    /// 1. If lock_cursor_style is enabled: use config.cursor_blink
1910    /// 2. If lock_cursor_blink is enabled and cursor_blink is false: force no blink
1911    /// 3. Otherwise: terminal's cursor style (set via DECSCUSR escape sequence)
1912    /// 4. Fallback: user's config setting (cursor_blink)
1913    ///
1914    /// DECSCUSR values: odd = blinking, even = steady
1915    /// - 0/1: Blinking block (default)
1916    /// - 2: Steady block
1917    /// - 3: Blinking underline
1918    /// - 4: Steady underline
1919    /// - 5: Blinking bar
1920    /// - 6: Steady bar
1921    pub(crate) fn update_cursor_blink(&mut self) {
1922        // If cursor style is locked, use the config's blink setting directly
1923        if self.config.lock_cursor_style {
1924            if !self.config.cursor_blink {
1925                self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1926                return;
1927            }
1928        } else if self.config.lock_cursor_blink && !self.config.cursor_blink {
1929            // If blink is locked off, don't blink regardless of terminal style
1930            self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1931            return;
1932        }
1933
1934        // Get cursor style from terminal to check if DECSCUSR specified blinking
1935        let cursor_should_blink = if self.config.lock_cursor_style {
1936            // Style is locked, use config's blink setting
1937            self.config.cursor_blink
1938        } else if let Some(tab) = self.tab_manager.active_tab()
1939            && let Ok(term) = tab.terminal.try_lock()
1940        {
1941            use par_term_emu_core_rust::cursor::CursorStyle;
1942            let style = term.cursor_style();
1943            // DECSCUSR: odd values (1,3,5) = blinking, even values (2,4,6) = steady
1944            matches!(
1945                style,
1946                CursorStyle::BlinkingBlock
1947                    | CursorStyle::BlinkingUnderline
1948                    | CursorStyle::BlinkingBar
1949            )
1950        } else {
1951            // Fallback to config setting if terminal lock unavailable
1952            self.config.cursor_blink
1953        };
1954
1955        if !cursor_should_blink {
1956            // Smoothly fade to full visibility if blinking disabled (by DECSCUSR or config)
1957            self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1958            return;
1959        }
1960
1961        let now = std::time::Instant::now();
1962
1963        // If key was pressed recently (within 500ms), smoothly fade in cursor and reset blink timer
1964        if let Some(last_key) = self.last_key_press
1965            && now.duration_since(last_key).as_millis() < 500
1966        {
1967            self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1968            self.last_cursor_blink = Some(now);
1969            return;
1970        }
1971
1972        // Smooth cursor blink animation using sine wave for natural fade
1973        let blink_interval = std::time::Duration::from_millis(self.config.cursor_blink_interval);
1974
1975        if let Some(last_blink) = self.last_cursor_blink {
1976            let elapsed = now.duration_since(last_blink);
1977            let progress = (elapsed.as_millis() as f32) / (blink_interval.as_millis() as f32);
1978
1979            // Use cosine wave for smooth fade in/out (starts at 1.0, fades to 0.0, back to 1.0)
1980            self.cursor_opacity = ((progress * std::f32::consts::PI).cos())
1981                .abs()
1982                .clamp(0.0, 1.0);
1983
1984            // Reset timer after full cycle (2x interval for full on+off)
1985            if elapsed >= blink_interval * 2 {
1986                self.last_cursor_blink = Some(now);
1987            }
1988        } else {
1989            // First time, start the blink timer with cursor fully visible
1990            self.cursor_opacity = 1.0;
1991            self.last_cursor_blink = Some(now);
1992        }
1993    }
1994
1995    /// Main render function for this window
1996    pub(crate) fn render(&mut self) {
1997        // Skip rendering if shutting down
1998        if self.is_shutting_down {
1999            return;
2000        }
2001
2002        // FPS throttling to enforce max_fps (focused) or unfocused_fps (unfocused)
2003        // This ensures rendering is capped even if VSync runs at a higher rate
2004        // or multiple sources are requesting redraws (refresh task, shader animations, etc.)
2005        let target_fps = if self.config.pause_refresh_on_blur && !self.is_focused {
2006            self.config.unfocused_fps
2007        } else {
2008            self.config.max_fps
2009        };
2010        let frame_interval_ms = 1000 / target_fps.max(1);
2011        let frame_interval = std::time::Duration::from_millis(frame_interval_ms as u64);
2012
2013        if let Some(last_render) = self.last_render_time {
2014            let elapsed = last_render.elapsed();
2015            if elapsed < frame_interval {
2016                // Not enough time has passed, skip this render
2017                return;
2018            }
2019        }
2020
2021        // Update last render time for FPS throttling
2022        self.last_render_time = Some(std::time::Instant::now());
2023
2024        let absolute_start = std::time::Instant::now();
2025
2026        // Reset redraw flag after rendering
2027        // This flag will be set again in about_to_wait if another redraw is needed
2028        self.needs_redraw = false;
2029
2030        // Track frame timing
2031        let frame_start = std::time::Instant::now();
2032
2033        // Calculate frame time from last render
2034        if let Some(last_start) = self.debug.last_frame_start {
2035            let frame_time = frame_start.duration_since(last_start);
2036            self.debug.frame_times.push(frame_time);
2037            if self.debug.frame_times.len() > 60 {
2038                self.debug.frame_times.remove(0);
2039            }
2040        }
2041        self.debug.last_frame_start = Some(frame_start);
2042
2043        // Update scroll animation
2044        let animation_running = if let Some(tab) = self.tab_manager.active_tab_mut() {
2045            tab.scroll_state.update_animation()
2046        } else {
2047            false
2048        };
2049
2050        // Update tab titles from terminal OSC sequences
2051        self.tab_manager.update_all_titles();
2052
2053        // Rebuild renderer if font-related settings changed
2054        if self.pending_font_rebuild {
2055            if let Err(e) = self.rebuild_renderer() {
2056                log::error!("Failed to rebuild renderer after font change: {}", e);
2057            }
2058            self.pending_font_rebuild = false;
2059        }
2060
2061        // Sync tab bar offsets with renderer's content offsets
2062        // This ensures the terminal grid correctly accounts for the tab bar position
2063        let tab_count = self.tab_manager.tab_count();
2064        let tab_bar_height = self.tab_bar_ui.get_height(tab_count, &self.config);
2065        let tab_bar_width = self.tab_bar_ui.get_width(tab_count, &self.config);
2066        crate::debug_trace!(
2067            "TAB_SYNC",
2068            "Tab count={}, tab_bar_height={:.0}, tab_bar_width={:.0}, position={:?}, mode={:?}",
2069            tab_count,
2070            tab_bar_height,
2071            tab_bar_width,
2072            self.config.tab_bar_position,
2073            self.config.tab_bar_mode
2074        );
2075        if let Some(renderer) = &mut self.renderer {
2076            let grid_changed = Self::apply_tab_bar_offsets_for_position(
2077                self.config.tab_bar_position,
2078                renderer,
2079                tab_bar_height,
2080                tab_bar_width,
2081            );
2082            if let Some((new_cols, new_rows)) = grid_changed {
2083                let cell_width = renderer.cell_width();
2084                let cell_height = renderer.cell_height();
2085                let width_px = (new_cols as f32 * cell_width) as usize;
2086                let height_px = (new_rows as f32 * cell_height) as usize;
2087
2088                for tab in self.tab_manager.tabs_mut() {
2089                    if let Ok(mut term) = tab.terminal.try_lock() {
2090                        term.set_cell_dimensions(cell_width as u32, cell_height as u32);
2091                        let _ = term.resize_with_pixels(new_cols, new_rows, width_px, height_px);
2092                    }
2093                    tab.cache.cells = None;
2094                }
2095                crate::debug_info!(
2096                    "TAB_SYNC",
2097                    "Tab bar offsets changed (position={:?}), resized terminals to {}x{}",
2098                    self.config.tab_bar_position,
2099                    new_cols,
2100                    new_rows
2101                );
2102            }
2103        }
2104
2105        // Sync status bar inset so the terminal grid does not extend behind it.
2106        // Must happen before cell gathering so the row count is correct.
2107        self.sync_status_bar_inset();
2108
2109        let (renderer_size, visible_lines, grid_cols) = if let Some(renderer) = &self.renderer {
2110            let (cols, rows) = renderer.grid_size();
2111            (renderer.size(), rows, cols)
2112        } else {
2113            return;
2114        };
2115
2116        // Get active tab's terminal and immediate state snapshots (avoid long borrows)
2117        let (
2118            terminal,
2119            scroll_offset,
2120            mouse_selection,
2121            cache_cells,
2122            cache_generation,
2123            cache_scroll_offset,
2124            cache_cursor_pos,
2125            cache_selection,
2126            cached_scrollback_len,
2127            cached_terminal_title,
2128            hovered_url,
2129        ) = match self.tab_manager.active_tab() {
2130            Some(t) => (
2131                t.terminal.clone(),
2132                t.scroll_state.offset,
2133                t.mouse.selection,
2134                t.cache.cells.clone(),
2135                t.cache.generation,
2136                t.cache.scroll_offset,
2137                t.cache.cursor_pos,
2138                t.cache.selection,
2139                t.cache.scrollback_len,
2140                t.cache.terminal_title.clone(),
2141                t.mouse.hovered_url.clone(),
2142            ),
2143            None => return,
2144        };
2145
2146        // Check if shell has exited
2147        let _is_running = if let Ok(term) = terminal.try_lock() {
2148            term.is_running()
2149        } else {
2150            true // Assume running if locked
2151        };
2152
2153        // Request another redraw if animation is still running
2154        if animation_running && let Some(window) = &self.window {
2155            window.request_redraw();
2156        }
2157
2158        // Get scroll offset and selection from active tab
2159
2160        // Get terminal cells for rendering (with dirty tracking optimization)
2161        // Also capture alt screen state to disable cursor shader for TUI apps
2162        let (cells, current_cursor_pos, cursor_style, is_alt_screen) = if let Ok(term) =
2163            terminal.try_lock()
2164        {
2165            // Get current generation to check if terminal content has changed
2166            let current_generation = term.update_generation();
2167
2168            // Normalize selection if it exists and extract mode
2169            let (selection, rectangular) = if let Some(sel) = mouse_selection {
2170                (
2171                    Some(sel.normalized()),
2172                    sel.mode == SelectionMode::Rectangular,
2173                )
2174            } else {
2175                (None, false)
2176            };
2177
2178            // Get cursor position and opacity (only show if we're at the bottom with no scroll offset
2179            // and the cursor is visible - TUI apps hide cursor via DECTCEM escape sequence)
2180            // If lock_cursor_visibility is enabled, ignore the terminal's visibility state
2181            // In copy mode, use the copy mode cursor position instead
2182            let cursor_visible = self.config.lock_cursor_visibility || term.is_cursor_visible();
2183            let current_cursor_pos = if self.copy_mode.active {
2184                self.copy_mode.screen_cursor_pos(scroll_offset)
2185            } else if scroll_offset == 0 && cursor_visible {
2186                Some(term.cursor_position())
2187            } else {
2188                None
2189            };
2190
2191            let cursor = current_cursor_pos.map(|pos| (pos, self.cursor_opacity));
2192
2193            // Get cursor style for geometric rendering
2194            // In copy mode, always use SteadyBlock for clear visibility
2195            // If lock_cursor_style is enabled, use the config's cursor style instead of terminal's
2196            // If lock_cursor_blink is enabled and cursor_blink is false, force steady cursor
2197            let cursor_style = if self.copy_mode.active && current_cursor_pos.is_some() {
2198                Some(TermCursorStyle::SteadyBlock)
2199            } else if current_cursor_pos.is_some() {
2200                if self.config.lock_cursor_style {
2201                    // Convert config cursor style to terminal cursor style
2202                    let style = if self.config.cursor_blink {
2203                        match self.config.cursor_style {
2204                            CursorStyle::Block => TermCursorStyle::BlinkingBlock,
2205                            CursorStyle::Beam => TermCursorStyle::BlinkingBar,
2206                            CursorStyle::Underline => TermCursorStyle::BlinkingUnderline,
2207                        }
2208                    } else {
2209                        match self.config.cursor_style {
2210                            CursorStyle::Block => TermCursorStyle::SteadyBlock,
2211                            CursorStyle::Beam => TermCursorStyle::SteadyBar,
2212                            CursorStyle::Underline => TermCursorStyle::SteadyUnderline,
2213                        }
2214                    };
2215                    Some(style)
2216                } else {
2217                    let mut style = term.cursor_style();
2218                    // If blink is locked off, convert blinking styles to steady
2219                    if self.config.lock_cursor_blink && !self.config.cursor_blink {
2220                        style = match style {
2221                            TermCursorStyle::BlinkingBlock => TermCursorStyle::SteadyBlock,
2222                            TermCursorStyle::BlinkingBar => TermCursorStyle::SteadyBar,
2223                            TermCursorStyle::BlinkingUnderline => TermCursorStyle::SteadyUnderline,
2224                            other => other,
2225                        };
2226                    }
2227                    Some(style)
2228                }
2229            } else {
2230                None
2231            };
2232
2233            log::trace!(
2234                "Cursor: pos={:?}, opacity={:.2}, style={:?}, scroll={}, visible={}",
2235                current_cursor_pos,
2236                self.cursor_opacity,
2237                cursor_style,
2238                scroll_offset,
2239                term.is_cursor_visible()
2240            );
2241
2242            // Check if we need to regenerate cells
2243            // Only regenerate when content actually changes, not on every cursor blink
2244            let needs_regeneration = cache_cells.is_none()
2245                || current_generation != cache_generation
2246                || scroll_offset != cache_scroll_offset
2247                || current_cursor_pos != cache_cursor_pos // Regenerate if cursor position changed
2248                || mouse_selection != cache_selection; // Regenerate if selection changed (including clearing)
2249
2250            let cell_gen_start = std::time::Instant::now();
2251            let (cells, is_cache_hit) = if needs_regeneration {
2252                // Generate fresh cells
2253                let fresh_cells =
2254                    term.get_cells_with_scrollback(scroll_offset, selection, rectangular, cursor);
2255
2256                (fresh_cells, false)
2257            } else {
2258                // Use cached cells - clone is still needed because of apply_url_underlines
2259                // but we track it accurately for debug logging
2260                (cache_cells.as_ref().unwrap().clone(), true)
2261            };
2262            self.debug.cache_hit = is_cache_hit;
2263            self.debug.cell_gen_time = cell_gen_start.elapsed();
2264
2265            // Check if alt screen is active (TUI apps like vim, htop)
2266            let is_alt_screen = term.is_alt_screen_active();
2267
2268            (cells, current_cursor_pos, cursor_style, is_alt_screen)
2269        } else {
2270            return; // Terminal locked, skip this frame
2271        };
2272
2273        // Ensure cursor visibility flag for cell renderer reflects current config every frame
2274        // (so toggling "Hide default cursor" takes effect immediately even if no other changes)
2275        // Resolve hides_cursor: per-shader override -> metadata defaults -> global config
2276        let resolved_hides_cursor = self
2277            .config
2278            .cursor_shader
2279            .as_ref()
2280            .and_then(|name| self.config.cursor_shader_configs.get(name))
2281            .and_then(|override_cfg| override_cfg.hides_cursor)
2282            .or_else(|| {
2283                self.config
2284                    .cursor_shader
2285                    .as_ref()
2286                    .and_then(|name| self.cursor_shader_metadata_cache.get(name))
2287                    .and_then(|meta| meta.defaults.hides_cursor)
2288            })
2289            .unwrap_or(self.config.cursor_shader_hides_cursor);
2290        // Resolve disable_in_alt_screen: per-shader override -> metadata defaults -> global config
2291        let resolved_disable_in_alt_screen = self
2292            .config
2293            .cursor_shader
2294            .as_ref()
2295            .and_then(|name| self.config.cursor_shader_configs.get(name))
2296            .and_then(|override_cfg| override_cfg.disable_in_alt_screen)
2297            .or_else(|| {
2298                self.config
2299                    .cursor_shader
2300                    .as_ref()
2301                    .and_then(|name| self.cursor_shader_metadata_cache.get(name))
2302                    .and_then(|meta| meta.defaults.disable_in_alt_screen)
2303            })
2304            .unwrap_or(self.config.cursor_shader_disable_in_alt_screen);
2305        let hide_cursor_for_shader = self.config.cursor_shader_enabled
2306            && resolved_hides_cursor
2307            && !(resolved_disable_in_alt_screen && is_alt_screen);
2308        if let Some(renderer) = &mut self.renderer {
2309            renderer.set_cursor_hidden_for_shader(hide_cursor_for_shader);
2310        }
2311
2312        // Update cache with regenerated cells (if needed)
2313        // Need to re-borrow as mutable after the terminal lock is released
2314        if !self.debug.cache_hit
2315            && let Some(tab) = self.tab_manager.active_tab_mut()
2316            && let Ok(term) = tab.terminal.try_lock()
2317        {
2318            let current_generation = term.update_generation();
2319            tab.cache.cells = Some(cells.clone());
2320            tab.cache.generation = current_generation;
2321            tab.cache.scroll_offset = tab.scroll_state.offset;
2322            tab.cache.cursor_pos = current_cursor_pos;
2323            tab.cache.selection = tab.mouse.selection;
2324        }
2325
2326        let mut show_scrollbar = self.should_show_scrollbar();
2327
2328        let (scrollback_len, terminal_title) = if let Ok(mut term) = terminal.try_lock() {
2329            // Use cursor row 0 when cursor not visible (e.g., alt screen)
2330            let cursor_row = current_cursor_pos.map(|(_, row)| row).unwrap_or(0);
2331            let sb_len = term.scrollback_len();
2332            term.update_scrollback_metadata(sb_len, cursor_row);
2333
2334            // Feed newly completed commands into persistent history from two sources:
2335            // 1. Scrollback marks (populated via set_mark_command_at from grid text extraction)
2336            // 2. Core library command history (populated by the terminal emulator core)
2337            // Both sources are checked because command text may come from either path
2338            // depending on shell integration quality. The synced_commands set prevents
2339            // duplicate adds across frames and sources.
2340            for mark in term.scrollback_marks() {
2341                if let Some(ref cmd) = mark.command
2342                    && !cmd.is_empty()
2343                    && self.synced_commands.insert(cmd.clone())
2344                {
2345                    self.command_history
2346                        .add(cmd.clone(), mark.exit_code, mark.duration_ms);
2347                }
2348            }
2349            for (cmd, exit_code, duration_ms) in term.core_command_history() {
2350                if !cmd.is_empty() && self.synced_commands.insert(cmd.clone()) {
2351                    self.command_history.add(cmd, exit_code, duration_ms);
2352                }
2353            }
2354
2355            (sb_len, term.get_title())
2356        } else {
2357            (cached_scrollback_len, cached_terminal_title.clone())
2358        };
2359
2360        // Update cache scrollback and clamp scroll state
2361        if let Some(tab) = self.tab_manager.active_tab_mut() {
2362            tab.cache.scrollback_len = scrollback_len;
2363            tab.scroll_state
2364                .clamp_to_scrollback(tab.cache.scrollback_len);
2365        }
2366
2367        // Keep copy mode dimensions in sync with terminal
2368        if self.copy_mode.active
2369            && let Ok(term) = terminal.try_lock()
2370        {
2371            let (cols, rows) = term.dimensions();
2372            self.copy_mode.update_dimensions(cols, rows, scrollback_len);
2373        }
2374
2375        let need_marks =
2376            self.config.scrollbar_command_marks || self.config.command_separator_enabled;
2377        let mut scrollback_marks = if need_marks {
2378            if let Ok(term) = terminal.try_lock() {
2379                term.scrollback_marks()
2380            } else {
2381                Vec::new()
2382            }
2383        } else {
2384            Vec::new()
2385        };
2386
2387        // Append trigger-generated marks
2388        if let Some(tab) = self.tab_manager.active_tab() {
2389            scrollback_marks.extend(tab.trigger_marks.iter().cloned());
2390        }
2391
2392        // Keep scrollbar visible when mark indicators exist (even if no scrollback).
2393        if !scrollback_marks.is_empty() {
2394            show_scrollbar = true;
2395        }
2396
2397        // Update window title if terminal has set one via OSC sequences
2398        // Only if allow_title_change is enabled and we're not showing a URL tooltip
2399        if self.config.allow_title_change
2400            && hovered_url.is_none()
2401            && terminal_title != cached_terminal_title
2402        {
2403            if let Some(tab) = self.tab_manager.active_tab_mut() {
2404                tab.cache.terminal_title = terminal_title.clone();
2405            }
2406            if let Some(window) = &self.window {
2407                if terminal_title.is_empty() {
2408                    // Restore configured title when terminal clears title
2409                    window.set_title(&self.format_title(&self.config.window_title));
2410                } else {
2411                    // Use terminal-set title with window number
2412                    window.set_title(&self.format_title(&terminal_title));
2413                }
2414            }
2415        }
2416
2417        // Total lines = visible lines + actual scrollback content
2418        let total_lines = visible_lines + scrollback_len;
2419
2420        // Detect URLs in visible area (only when content changed)
2421        // This optimization saves ~0.26ms per frame on cache hits
2422        let url_detect_start = std::time::Instant::now();
2423        let debug_url_detect_time = if !self.debug.cache_hit {
2424            // Content changed - re-detect URLs
2425            self.detect_urls();
2426            url_detect_start.elapsed()
2427        } else {
2428            // Content unchanged - use cached URL detection
2429            std::time::Duration::ZERO
2430        };
2431
2432        // Apply URL underlining to cells (always apply, since cells might be regenerated)
2433        let url_underline_start = std::time::Instant::now();
2434        let mut cells = cells; // Make cells mutable
2435        self.apply_url_underlines(&mut cells, &renderer_size);
2436        let _debug_url_underline_time = url_underline_start.elapsed();
2437
2438        // Update search and apply search highlighting
2439        if self.search_ui.visible {
2440            // Get all searchable lines from cells (ensures consistent wide character handling)
2441            if let Some(tab) = self.tab_manager.active_tab()
2442                && let Ok(term) = tab.terminal.try_lock()
2443            {
2444                let lines_iter =
2445                    crate::app::search_highlight::get_all_searchable_lines(&term, visible_lines);
2446                self.search_ui.update_search(lines_iter);
2447            }
2448
2449            // Apply search highlighting to visible cells
2450            let scroll_offset = self
2451                .tab_manager
2452                .active_tab()
2453                .map(|t| t.scroll_state.offset)
2454                .unwrap_or(0);
2455            // Use actual terminal grid columns from renderer
2456            self.apply_search_highlights(
2457                &mut cells,
2458                grid_cols,
2459                scroll_offset,
2460                scrollback_len,
2461                visible_lines,
2462            );
2463        }
2464
2465        // Update cursor blink state
2466        self.update_cursor_blink();
2467
2468        let render_start = std::time::Instant::now();
2469
2470        let mut debug_update_cells_time = std::time::Duration::ZERO;
2471        #[allow(unused_assignments)]
2472        let mut debug_graphics_time = std::time::Duration::ZERO;
2473        #[allow(unused_assignments)]
2474        let mut debug_actual_render_time = std::time::Duration::ZERO;
2475        let _ = &debug_actual_render_time;
2476        // Clipboard action to handle after rendering (declared here to survive renderer borrow)
2477        let mut pending_clipboard_action = ClipboardHistoryAction::None;
2478        // Command history action to handle after rendering
2479        let mut pending_command_history_action = CommandHistoryAction::None;
2480        // Paste special action to handle after rendering
2481        let mut pending_paste_special_action = PasteSpecialAction::None;
2482        // tmux session picker action to handle after rendering
2483        let mut pending_session_picker_action = SessionPickerAction::None;
2484        // Tab bar action to handle after rendering (declared here to survive renderer borrow)
2485        let mut pending_tab_action = TabBarAction::None;
2486        // Shader install response to handle after rendering
2487        let mut pending_shader_install_response = ShaderInstallResponse::None;
2488        // Integrations welcome dialog response to handle after rendering
2489        let mut pending_integrations_response = IntegrationsResponse::default();
2490        // Search action to handle after rendering
2491        let mut pending_search_action = crate::search::SearchAction::None;
2492        // AI Inspector action to handle after rendering
2493        let mut pending_inspector_action = InspectorAction::None;
2494        // Profile drawer action to handle after rendering
2495        let mut pending_profile_drawer_action = ProfileDrawerAction::None;
2496        // Close confirmation action to handle after rendering
2497        let mut pending_close_confirm_action = CloseConfirmAction::None;
2498        // Quit confirmation action to handle after rendering
2499        let mut pending_quit_confirm_action = QuitConfirmAction::None;
2500        let mut pending_remote_install_action = RemoteShellInstallAction::None;
2501        let mut pending_ssh_connect_action = SshConnectAction::None;
2502
2503        // Process agent messages
2504        let msg_count_before = self.ai_inspector.chat.messages.len();
2505        // Config update requests are deferred to avoid double-borrow of self
2506        // while iterating agent_rx.
2507        type ConfigUpdateEntry = (
2508            std::collections::HashMap<String, serde_json::Value>,
2509            tokio::sync::oneshot::Sender<Result<(), String>>,
2510        );
2511        let mut pending_config_updates: Vec<ConfigUpdateEntry> = Vec::new();
2512        if let Some(rx) = &mut self.agent_rx {
2513            while let Ok(msg) = rx.try_recv() {
2514                match msg {
2515                    AgentMessage::StatusChanged(status) => {
2516                        // Flush any pending agent text on status change.
2517                        self.ai_inspector.chat.flush_agent_message();
2518                        self.ai_inspector.agent_status = status;
2519                        self.needs_redraw = true;
2520                    }
2521                    AgentMessage::SessionUpdate(update) => {
2522                        self.ai_inspector.chat.handle_update(update);
2523                        self.needs_redraw = true;
2524                    }
2525                    AgentMessage::PermissionRequest {
2526                        request_id,
2527                        tool_call,
2528                        options,
2529                    } => {
2530                        log::info!(
2531                            "ACP: permission request id={request_id} options={}",
2532                            options.len()
2533                        );
2534                        let description = tool_call
2535                            .get("title")
2536                            .and_then(|t| t.as_str())
2537                            .unwrap_or("Permission requested")
2538                            .to_string();
2539                        self.ai_inspector
2540                            .chat
2541                            .messages
2542                            .push(ChatMessage::Permission {
2543                                request_id,
2544                                description,
2545                                options: options
2546                                    .iter()
2547                                    .map(|o| (o.option_id.clone(), o.name.clone()))
2548                                    .collect(),
2549                                resolved: false,
2550                            });
2551                        self.needs_redraw = true;
2552                    }
2553                    AgentMessage::PromptComplete => {
2554                        self.ai_inspector.chat.flush_agent_message();
2555                        self.needs_redraw = true;
2556                    }
2557                    AgentMessage::ConfigUpdate { updates, reply } => {
2558                        pending_config_updates.push((updates, reply));
2559                    }
2560                    AgentMessage::ClientReady(client) => {
2561                        log::info!("ACP: agent_client ready");
2562                        self.agent_client = Some(client);
2563                    }
2564                }
2565            }
2566        }
2567        // Process deferred config updates now that agent_rx borrow is released.
2568        for (updates, reply) in pending_config_updates {
2569            let result = self.apply_agent_config_updates(&updates);
2570            if result.is_ok() {
2571                self.config_changed_by_agent = true;
2572            }
2573            let _ = reply.send(result);
2574            self.needs_redraw = true;
2575        }
2576
2577        // Auto-execute new CommandSuggestion messages when terminal access is enabled.
2578        if self.config.ai_inspector_agent_terminal_access {
2579            let new_messages = &self.ai_inspector.chat.messages[msg_count_before..];
2580            let commands_to_run: Vec<String> = new_messages
2581                .iter()
2582                .filter_map(|msg| {
2583                    if let ChatMessage::CommandSuggestion(cmd) = msg {
2584                        Some(format!("{cmd}\n"))
2585                    } else {
2586                        None
2587                    }
2588                })
2589                .collect();
2590
2591            if !commands_to_run.is_empty()
2592                && let Some(tab) = self.tab_manager.active_tab()
2593                && let Ok(term) = tab.terminal.try_lock()
2594            {
2595                for cmd in &commands_to_run {
2596                    let _ = term.write(cmd.as_bytes());
2597                }
2598                crate::debug_info!(
2599                    "AI_INSPECTOR",
2600                    "Auto-executed {} command(s) in terminal",
2601                    commands_to_run.len()
2602                );
2603            }
2604        }
2605
2606        // Detect new command completions and auto-refresh the snapshot.
2607        // This is separate from agent auto-context so the panel always shows
2608        // up-to-date command history regardless of agent connection state.
2609        if self.ai_inspector.open
2610            && let Some(tab) = self.tab_manager.active_tab()
2611            && let Ok(term) = tab.terminal.try_lock()
2612        {
2613            let history = term.core_command_history();
2614            let current_count = history.len();
2615
2616            if current_count != self.ai_inspector.last_command_count {
2617                // Command count changed — refresh the snapshot
2618                let had_commands = self.ai_inspector.last_command_count > 0;
2619                self.ai_inspector.last_command_count = current_count;
2620                self.ai_inspector.needs_refresh = true;
2621
2622                // Auto-context feeding: send latest command info to agent
2623                if had_commands
2624                    && current_count > 0
2625                    && self.config.ai_inspector_auto_context
2626                    && self.ai_inspector.agent_status == AgentStatus::Connected
2627                    && let Some((cmd, exit_code, duration_ms)) = history.last()
2628                {
2629                    let exit_code_str = exit_code
2630                        .map(|c| c.to_string())
2631                        .unwrap_or_else(|| "N/A".to_string());
2632                    let duration = duration_ms.unwrap_or(0);
2633
2634                    let cwd = term.shell_integration_cwd().unwrap_or_default();
2635
2636                    let context = format!(
2637                        "Command completed:\n$ {}\nExit code: {}\nDuration: {}ms\nCWD: {}",
2638                        cmd, exit_code_str, duration, cwd
2639                    );
2640
2641                    if let Some(agent) = &self.agent {
2642                        let agent = agent.clone();
2643                        let content = vec![par_term_acp::ContentBlock::Text { text: context }];
2644                        self.runtime.spawn(async move {
2645                            let agent = agent.lock().await;
2646                            let _ = agent.send_prompt(content).await;
2647                        });
2648                    }
2649                }
2650            }
2651        }
2652
2653        // Refresh AI Inspector snapshot if needed
2654        if self.ai_inspector.open
2655            && self.ai_inspector.needs_refresh
2656            && let Some(tab) = self.tab_manager.active_tab()
2657            && let Ok(term) = tab.terminal.try_lock()
2658        {
2659            let snapshot = crate::ai_inspector::snapshot::SnapshotData::gather(
2660                &term,
2661                &self.ai_inspector.scope,
2662                self.config.ai_inspector_context_max_lines,
2663            );
2664            self.ai_inspector.snapshot = Some(snapshot);
2665            self.ai_inspector.needs_refresh = false;
2666        }
2667
2668        // Check tmux gateway state before renderer borrow to avoid borrow conflicts
2669        // When tmux controls the layout, we don't use pane padding
2670        // Note: pane_padding is in logical pixels (config); we defer DPI scaling to
2671        // where it's used with physical pixel coordinates (via sizing.scale_factor).
2672        let is_tmux_gateway = self.is_gateway_active();
2673        let effective_pane_padding = if is_tmux_gateway {
2674            0.0
2675        } else {
2676            self.config.pane_padding
2677        };
2678
2679        // Calculate status bar height before mutable renderer borrow
2680        // Note: This is in logical pixels; it gets scaled to physical in RendererSizing.
2681        let is_tmux_connected = self.is_tmux_connected();
2682        let status_bar_height =
2683            crate::tmux_status_bar_ui::TmuxStatusBarUI::height(&self.config, is_tmux_connected);
2684
2685        // Calculate custom status bar height
2686        let custom_status_bar_height = self.status_bar_ui.height(&self.config, self.is_fullscreen);
2687
2688        // Capture window size before mutable borrow (for badge rendering in egui)
2689        let window_size_for_badge = self.renderer.as_ref().map(|r| r.size());
2690
2691        // Capture progress bar snapshot before mutable borrow
2692        let progress_snapshot = if self.config.progress_bar_enabled {
2693            self.tab_manager.active_tab().and_then(|tab| {
2694                tab.terminal
2695                    .try_lock()
2696                    .ok()
2697                    .map(|term| ProgressBarSnapshot {
2698                        simple: term.progress_bar(),
2699                        named: term.named_progress_bars(),
2700                    })
2701            })
2702        } else {
2703            None
2704        };
2705
2706        // Sync AI Inspector panel width before scrollbar update so the scrollbar
2707        // position uses the current panel width on this frame (not the previous one).
2708        self.sync_ai_inspector_width();
2709
2710        if let Some(renderer) = &mut self.renderer {
2711            // Status bar inset is handled by sync_status_bar_inset() above,
2712            // before cell gathering, so the grid height is correct.
2713
2714            // Disable cursor shader when alt screen is active (TUI apps like vim, htop)
2715            let disable_cursor_shader =
2716                self.config.cursor_shader_disable_in_alt_screen && is_alt_screen;
2717            renderer.set_cursor_shader_disabled_for_alt_screen(disable_cursor_shader);
2718
2719            // Only update renderer with cells if they changed (cache MISS)
2720            // This avoids re-uploading the same cell data to GPU on every frame
2721            if !self.debug.cache_hit {
2722                let t = std::time::Instant::now();
2723                renderer.update_cells(&cells);
2724                debug_update_cells_time = t.elapsed();
2725            }
2726
2727            // Update cursor position and style for geometric rendering
2728            if let (Some(pos), Some(opacity), Some(style)) =
2729                (current_cursor_pos, Some(self.cursor_opacity), cursor_style)
2730            {
2731                renderer.update_cursor(pos, opacity, style);
2732                // Forward cursor state to custom shader for Ghostty-compatible cursor animations
2733                // Use the configured cursor color
2734                let cursor_color = [
2735                    self.config.cursor_color[0] as f32 / 255.0,
2736                    self.config.cursor_color[1] as f32 / 255.0,
2737                    self.config.cursor_color[2] as f32 / 255.0,
2738                    1.0,
2739                ];
2740                renderer.update_shader_cursor(pos.0, pos.1, opacity, cursor_color, style);
2741            } else {
2742                renderer.clear_cursor();
2743            }
2744
2745            // Update progress bar state for shader uniforms
2746            if let Some(ref snap) = progress_snapshot {
2747                use par_term_emu_core_rust::terminal::ProgressState;
2748                let state_val = match snap.simple.state {
2749                    ProgressState::Hidden => 0.0,
2750                    ProgressState::Normal => 1.0,
2751                    ProgressState::Error => 2.0,
2752                    ProgressState::Indeterminate => 3.0,
2753                    ProgressState::Warning => 4.0,
2754                };
2755                let active_count = (if snap.simple.is_active() { 1 } else { 0 })
2756                    + snap.named.values().filter(|b| b.state.is_active()).count();
2757                renderer.update_shader_progress(
2758                    state_val,
2759                    snap.simple.progress as f32 / 100.0,
2760                    if snap.has_active() { 1.0 } else { 0.0 },
2761                    active_count as f32,
2762                );
2763            } else {
2764                renderer.update_shader_progress(0.0, 0.0, 0.0, 0.0);
2765            }
2766
2767            // Update scrollbar
2768            let scroll_offset = self
2769                .tab_manager
2770                .active_tab()
2771                .map(|t| t.scroll_state.offset)
2772                .unwrap_or(0);
2773            renderer.update_scrollbar(scroll_offset, visible_lines, total_lines, &scrollback_marks);
2774
2775            // Compute and set command separator marks for single-pane rendering
2776            if self.config.command_separator_enabled {
2777                let separator_marks = crate::renderer::compute_visible_separator_marks(
2778                    &scrollback_marks,
2779                    scrollback_len,
2780                    scroll_offset,
2781                    visible_lines,
2782                );
2783                renderer.set_separator_marks(separator_marks);
2784            } else {
2785                renderer.set_separator_marks(Vec::new());
2786            }
2787
2788            // Update animations and request redraw if frames changed
2789            let anim_start = std::time::Instant::now();
2790            if let Some(tab) = self.tab_manager.active_tab() {
2791                let terminal = tab.terminal.blocking_lock();
2792                if terminal.update_animations() {
2793                    // Animation frame changed - request continuous redraws while animations are playing
2794                    if let Some(window) = &self.window {
2795                        window.request_redraw();
2796                    }
2797                }
2798            }
2799            let debug_anim_time = anim_start.elapsed();
2800
2801            // Update graphics from terminal (pass scroll_offset for view adjustment)
2802            // Include both current screen graphics and scrollback graphics
2803            // Use get_graphics_with_animations() to get current animation frames
2804            let graphics_start = std::time::Instant::now();
2805            if let Some(tab) = self.tab_manager.active_tab() {
2806                let terminal = tab.terminal.blocking_lock();
2807                let mut graphics = terminal.get_graphics_with_animations();
2808                let scrollback_len = terminal.scrollback_len();
2809
2810                // Always include scrollback graphics (renderer will calculate visibility)
2811                let scrollback_graphics = terminal.get_scrollback_graphics();
2812                let scrollback_count = scrollback_graphics.len();
2813                graphics.extend(scrollback_graphics);
2814
2815                debug_info!(
2816                    "APP",
2817                    "Got {} graphics ({} scrollback) from terminal (scroll_offset={}, scrollback_len={})",
2818                    graphics.len(),
2819                    scrollback_count,
2820                    scroll_offset,
2821                    scrollback_len
2822                );
2823                if let Err(e) = renderer.update_graphics(
2824                    &graphics,
2825                    scroll_offset,
2826                    scrollback_len,
2827                    visible_lines,
2828                ) {
2829                    log::error!("Failed to update graphics: {}", e);
2830                }
2831            }
2832            debug_graphics_time = graphics_start.elapsed();
2833
2834            // Calculate visual bell flash intensity (0.0 = no flash, 1.0 = full flash)
2835            let visual_bell_flash = self
2836                .tab_manager
2837                .active_tab()
2838                .and_then(|t| t.bell.visual_flash);
2839            let visual_bell_intensity = if let Some(flash_start) = visual_bell_flash {
2840                const FLASH_DURATION_MS: u128 = 150;
2841                let elapsed = flash_start.elapsed().as_millis();
2842                if elapsed < FLASH_DURATION_MS {
2843                    // Request continuous redraws while flash is active
2844                    if let Some(window) = &self.window {
2845                        window.request_redraw();
2846                    }
2847                    // Fade out: start at 0.3 intensity, fade to 0
2848                    0.3 * (1.0 - (elapsed as f32 / FLASH_DURATION_MS as f32))
2849                } else {
2850                    // Flash complete - clear it
2851                    if let Some(tab) = self.tab_manager.active_tab_mut() {
2852                        tab.bell.visual_flash = None;
2853                    }
2854                    0.0
2855                }
2856            } else {
2857                0.0
2858            };
2859
2860            // Update renderer with visual bell intensity
2861            renderer.set_visual_bell_intensity(visual_bell_intensity);
2862
2863            // Prepare egui output if settings UI is visible
2864            let egui_start = std::time::Instant::now();
2865
2866            // Capture values for FPS overlay before closure
2867            let show_fps = self.debug.show_fps_overlay;
2868            let fps_value = self.debug.fps_value;
2869            let frame_time_ms = if !self.debug.frame_times.is_empty() {
2870                let avg = self.debug.frame_times.iter().sum::<std::time::Duration>()
2871                    / self.debug.frame_times.len() as u32;
2872                avg.as_secs_f64() * 1000.0
2873            } else {
2874                0.0
2875            };
2876
2877            // Capture badge state for closure (uses window_size_for_badge captured earlier)
2878            let badge_enabled = self.badge_state.enabled;
2879            let badge_state = if badge_enabled {
2880                // Update variables if dirty
2881                if self.badge_state.is_dirty() {
2882                    self.badge_state.interpolate();
2883                }
2884                Some(self.badge_state.clone())
2885            } else {
2886                None
2887            };
2888
2889            // Capture session variables for status bar rendering
2890            let status_bar_session_vars = if self.config.status_bar_enabled {
2891                Some(self.badge_state.variables.read().clone())
2892            } else {
2893                None
2894            };
2895
2896            // Capture hovered scrollbar mark for tooltip display
2897            let hovered_mark: Option<crate::scrollback_metadata::ScrollbackMark> =
2898                if self.config.scrollbar_mark_tooltips && self.config.scrollbar_command_marks {
2899                    self.tab_manager
2900                        .active_tab()
2901                        .map(|tab| tab.mouse.position)
2902                        .and_then(|(mx, my)| {
2903                            renderer.scrollbar_mark_at_position(mx as f32, my as f32, 8.0)
2904                        })
2905                        .cloned()
2906                } else {
2907                    None
2908                };
2909
2910            // Collect pane bounds for identify overlay (before egui borrow)
2911            let pane_identify_bounds: Vec<(usize, crate::pane::PaneBounds)> =
2912                if self.pane_identify_hide_time.is_some() {
2913                    self.tab_manager
2914                        .active_tab()
2915                        .and_then(|tab| tab.pane_manager())
2916                        .map(|pm| {
2917                            pm.all_panes()
2918                                .iter()
2919                                .enumerate()
2920                                .map(|(i, pane)| (i, pane.bounds))
2921                                .collect()
2922                        })
2923                        .unwrap_or_default()
2924                } else {
2925                    Vec::new()
2926                };
2927
2928            let egui_data = if let (Some(egui_ctx), Some(egui_state)) =
2929                (&self.egui_ctx, &mut self.egui_state)
2930            {
2931                let mut raw_input = egui_state.take_egui_input(self.window.as_ref().unwrap());
2932
2933                // Inject pending events from menu accelerators (Cmd+V/C/A intercepted by muda)
2934                // when egui overlays (profile modal, search, etc.) are active
2935                raw_input.events.append(&mut self.pending_egui_events);
2936
2937                // When no modal UI overlay is visible, filter out Tab key events to prevent
2938                // egui's default focus navigation from stealing Tab/Shift+Tab from the terminal.
2939                // Tab/Shift+Tab should only cycle focus between egui widgets when a modal is open.
2940                // Note: Side panels (ai_inspector, profile drawer) are NOT modals — the terminal
2941                // should still receive Tab/Shift+Tab when they are open.
2942                let any_modal_visible = self.help_ui.visible
2943                    || self.clipboard_history_ui.visible
2944                    || self.command_history_ui.visible
2945                    || self.shader_install_ui.visible
2946                    || self.integrations_ui.visible
2947                    || self.search_ui.visible
2948                    || self.tmux_session_picker_ui.visible
2949                    || self.ssh_connect_ui.is_visible()
2950                    || self.quit_confirmation_ui.is_visible();
2951                if !any_modal_visible {
2952                    raw_input.events.retain(|e| {
2953                        !matches!(
2954                            e,
2955                            egui::Event::Key {
2956                                key: egui::Key::Tab,
2957                                ..
2958                            }
2959                        )
2960                    });
2961                }
2962
2963                let egui_output = egui_ctx.run(raw_input, |ctx| {
2964                    // Show FPS overlay if enabled (top-right corner)
2965                    if show_fps {
2966                        egui::Area::new(egui::Id::new("fps_overlay"))
2967                            .anchor(egui::Align2::RIGHT_TOP, egui::vec2(-30.0, 10.0))
2968                            .order(egui::Order::Foreground)
2969                            .show(ctx, |ui| {
2970                                egui::Frame::NONE
2971                                    .fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 200))
2972                                    .inner_margin(egui::Margin::same(8))
2973                                    .corner_radius(4.0)
2974                                    .show(ui, |ui| {
2975                                        ui.style_mut().visuals.override_text_color =
2976                                            Some(egui::Color32::from_rgb(0, 255, 0));
2977                                        ui.label(
2978                                            egui::RichText::new(format!(
2979                                                "FPS: {:.1}\nFrame: {:.2}ms",
2980                                                fps_value, frame_time_ms
2981                                            ))
2982                                            .monospace()
2983                                            .size(14.0),
2984                                        );
2985                                    });
2986                            });
2987                    }
2988
2989                    // Show resize overlay if active (centered)
2990                    if self.resize_overlay_visible
2991                        && let Some((width_px, height_px, cols, rows)) = self.resize_dimensions
2992                    {
2993                        egui::Area::new(egui::Id::new("resize_overlay"))
2994                            .anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
2995                            .order(egui::Order::Foreground)
2996                            .show(ctx, |ui| {
2997                                egui::Frame::NONE
2998                                    .fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 220))
2999                                    .inner_margin(egui::Margin::same(16))
3000                                    .corner_radius(8.0)
3001                                    .show(ui, |ui| {
3002                                        ui.style_mut().visuals.override_text_color =
3003                                            Some(egui::Color32::from_rgb(255, 255, 255));
3004                                        ui.label(
3005                                            egui::RichText::new(format!(
3006                                                "{}×{}\n{}×{} px",
3007                                                cols, rows, width_px, height_px
3008                                            ))
3009                                            .monospace()
3010                                            .size(24.0),
3011                                        );
3012                                    });
3013                            });
3014                    }
3015
3016                    // Show copy mode status bar overlay (when enabled in config)
3017                    if self.copy_mode.active && self.config.copy_mode_show_status {
3018                        let status = self.copy_mode.status_text();
3019                        let (mode_text, color) = if self.copy_mode.is_searching {
3020                            ("SEARCH", egui::Color32::from_rgb(255, 165, 0))
3021                        } else {
3022                            match self.copy_mode.visual_mode {
3023                                crate::copy_mode::VisualMode::None => {
3024                                    ("COPY", egui::Color32::from_rgb(100, 200, 100))
3025                                }
3026                                crate::copy_mode::VisualMode::Char => {
3027                                    ("VISUAL", egui::Color32::from_rgb(100, 150, 255))
3028                                }
3029                                crate::copy_mode::VisualMode::Line => {
3030                                    ("V-LINE", egui::Color32::from_rgb(100, 150, 255))
3031                                }
3032                                crate::copy_mode::VisualMode::Block => {
3033                                    ("V-BLOCK", egui::Color32::from_rgb(100, 150, 255))
3034                                }
3035                            }
3036                        };
3037
3038                        egui::Area::new(egui::Id::new("copy_mode_status_bar"))
3039                            .anchor(egui::Align2::LEFT_BOTTOM, egui::vec2(0.0, 0.0))
3040                            .order(egui::Order::Foreground)
3041                            .show(ctx, |ui| {
3042                                let available_width = ui.available_width();
3043                                egui::Frame::NONE
3044                                    .fill(egui::Color32::from_rgba_unmultiplied(40, 40, 40, 230))
3045                                    .inner_margin(egui::Margin::symmetric(12, 6))
3046                                    .show(ui, |ui| {
3047                                        ui.set_min_width(available_width);
3048                                        ui.horizontal(|ui| {
3049                                            ui.label(
3050                                                egui::RichText::new(mode_text)
3051                                                    .monospace()
3052                                                    .size(13.0)
3053                                                    .color(color)
3054                                                    .strong(),
3055                                            );
3056                                            ui.separator();
3057                                            ui.label(
3058                                                egui::RichText::new(&status)
3059                                                    .monospace()
3060                                                    .size(12.0)
3061                                                    .color(egui::Color32::from_rgb(200, 200, 200)),
3062                                            );
3063                                        });
3064                                    });
3065                            });
3066                    }
3067
3068                    // Show toast notification if active (top center)
3069                    if let Some(ref message) = self.toast_message {
3070                        egui::Area::new(egui::Id::new("toast_notification"))
3071                            .anchor(egui::Align2::CENTER_TOP, egui::vec2(0.0, 60.0))
3072                            .order(egui::Order::Foreground)
3073                            .show(ctx, |ui| {
3074                                egui::Frame::NONE
3075                                    .fill(egui::Color32::from_rgba_unmultiplied(30, 30, 30, 240))
3076                                    .inner_margin(egui::Margin::symmetric(20, 12))
3077                                    .corner_radius(8.0)
3078                                    .stroke(egui::Stroke::new(
3079                                        1.0,
3080                                        egui::Color32::from_rgb(80, 80, 80),
3081                                    ))
3082                                    .show(ui, |ui| {
3083                                        ui.style_mut().visuals.override_text_color =
3084                                            Some(egui::Color32::from_rgb(255, 255, 255));
3085                                        ui.label(egui::RichText::new(message).size(16.0));
3086                                    });
3087                            });
3088                    }
3089
3090                    // Show scrollbar mark tooltip if hovering over a mark
3091                    if let Some(ref mark) = hovered_mark {
3092                        // Format the tooltip content
3093                        let mut lines = Vec::new();
3094
3095                        if let Some(ref cmd) = mark.command {
3096                            let truncated = if cmd.len() > 50 {
3097                                format!("{}...", &cmd[..47])
3098                            } else {
3099                                cmd.clone()
3100                            };
3101                            lines.push(format!("Command: {}", truncated));
3102                        }
3103
3104                        if let Some(start_time) = mark.start_time {
3105                            use chrono::{DateTime, Local, Utc};
3106                            let dt =
3107                                DateTime::<Utc>::from_timestamp_millis(start_time as i64).unwrap();
3108                            let local: DateTime<Local> = dt.into();
3109                            lines.push(format!("Time: {}", local.format("%H:%M:%S")));
3110                        }
3111
3112                        if let Some(duration_ms) = mark.duration_ms {
3113                            if duration_ms < 1000 {
3114                                lines.push(format!("Duration: {}ms", duration_ms));
3115                            } else if duration_ms < 60000 {
3116                                lines
3117                                    .push(format!("Duration: {:.1}s", duration_ms as f64 / 1000.0));
3118                            } else {
3119                                let mins = duration_ms / 60000;
3120                                let secs = (duration_ms % 60000) / 1000;
3121                                lines.push(format!("Duration: {}m {}s", mins, secs));
3122                            }
3123                        }
3124
3125                        if let Some(exit_code) = mark.exit_code {
3126                            lines.push(format!("Exit: {}", exit_code));
3127                        }
3128
3129                        let tooltip_text = lines.join("\n");
3130
3131                        // Calculate tooltip position, clamped to stay on screen
3132                        let mouse_pos = ctx.pointer_hover_pos().unwrap_or(egui::pos2(100.0, 100.0));
3133                        let tooltip_x = (mouse_pos.x - 180.0).max(10.0);
3134                        let tooltip_y = (mouse_pos.y - 20.0).max(10.0);
3135
3136                        // Show tooltip near mouse position (offset to the left of scrollbar)
3137                        egui::Area::new(egui::Id::new("scrollbar_mark_tooltip"))
3138                            .order(egui::Order::Tooltip)
3139                            .fixed_pos(egui::pos2(tooltip_x, tooltip_y))
3140                            .show(ctx, |ui| {
3141                                ui.set_min_width(150.0);
3142                                egui::Frame::NONE
3143                                    .fill(egui::Color32::from_rgba_unmultiplied(30, 30, 30, 240))
3144                                    .inner_margin(egui::Margin::same(8))
3145                                    .corner_radius(4.0)
3146                                    .stroke(egui::Stroke::new(
3147                                        1.0,
3148                                        egui::Color32::from_rgb(80, 80, 80),
3149                                    ))
3150                                    .show(ui, |ui| {
3151                                        ui.set_min_width(140.0);
3152                                        ui.style_mut().visuals.override_text_color =
3153                                            Some(egui::Color32::from_rgb(220, 220, 220));
3154                                        ui.label(
3155                                            egui::RichText::new(&tooltip_text)
3156                                                .monospace()
3157                                                .size(12.0),
3158                                        );
3159                                    });
3160                            });
3161                    }
3162
3163                    // Render tab bar if visible (action handled after closure)
3164                    pending_tab_action = self.tab_bar_ui.render(
3165                        ctx,
3166                        &self.tab_manager,
3167                        &self.config,
3168                        &self.profile_manager,
3169                    );
3170
3171                    // Render tmux status bar if connected
3172                    self.tmux_status_bar_ui.render(
3173                        ctx,
3174                        &self.config,
3175                        self.tmux_session.as_ref(),
3176                        self.tmux_session_name.as_deref(),
3177                    );
3178
3179                    // Render custom status bar
3180                    if let Some(ref session_vars) = status_bar_session_vars {
3181                        self.status_bar_ui.render(
3182                            ctx,
3183                            &self.config,
3184                            session_vars,
3185                            self.is_fullscreen,
3186                        );
3187                    }
3188
3189                    // Settings are now handled by standalone SettingsWindow only
3190                    // No overlay settings UI rendering needed
3191
3192                    // Show help UI
3193                    self.help_ui.show(ctx);
3194
3195                    // Show clipboard history UI and collect action
3196                    pending_clipboard_action = self.clipboard_history_ui.show(ctx);
3197
3198                    // Show command history UI and collect action
3199                    pending_command_history_action = self.command_history_ui.show(ctx);
3200
3201                    // Show paste special UI and collect action
3202                    pending_paste_special_action = self.paste_special_ui.show(ctx);
3203
3204                    // Show search UI and collect action
3205                    pending_search_action = self.search_ui.show(ctx, visible_lines, scrollback_len);
3206
3207                    // Show AI Inspector panel and collect action
3208                    pending_inspector_action = self.ai_inspector.show(ctx, &self.available_agents);
3209
3210                    // Show tmux session picker UI and collect action
3211                    let tmux_path = self.config.resolve_tmux_path();
3212                    pending_session_picker_action =
3213                        self.tmux_session_picker_ui.show(ctx, &tmux_path);
3214
3215                    // Show shader install dialog if visible
3216                    pending_shader_install_response = self.shader_install_ui.show(ctx);
3217
3218                    // Show integrations welcome dialog if visible
3219                    pending_integrations_response = self.integrations_ui.show(ctx);
3220
3221                    // Show close confirmation dialog if visible
3222                    pending_close_confirm_action = self.close_confirmation_ui.show(ctx);
3223
3224                    // Show quit confirmation dialog if visible
3225                    pending_quit_confirm_action = self.quit_confirmation_ui.show(ctx);
3226
3227                    // Show remote shell install dialog if visible
3228                    pending_remote_install_action = self.remote_shell_install_ui.show(ctx);
3229
3230                    // Show SSH Quick Connect dialog if visible
3231                    pending_ssh_connect_action = self.ssh_connect_ui.show(ctx);
3232
3233                    // Render profile drawer (right side panel)
3234                    pending_profile_drawer_action = self.profile_drawer_ui.render(
3235                        ctx,
3236                        &self.profile_manager,
3237                        &self.config,
3238                        false, // profile modal is no longer in the terminal window
3239                    );
3240
3241                    // Render progress bar overlay
3242                    if let (Some(snap), Some(size)) = (&progress_snapshot, window_size_for_badge) {
3243                        render_progress_bars(
3244                            ctx,
3245                            snap,
3246                            &self.config,
3247                            size.width as f32,
3248                            size.height as f32,
3249                        );
3250                    }
3251
3252                    // Render pane identify overlay (large index numbers centered on each pane)
3253                    if !pane_identify_bounds.is_empty() {
3254                        for (index, bounds) in &pane_identify_bounds {
3255                            let center_x = bounds.x + bounds.width / 2.0;
3256                            let center_y = bounds.y + bounds.height / 2.0;
3257                            egui::Area::new(egui::Id::new(format!("pane_identify_{}", index)))
3258                                .fixed_pos(egui::pos2(center_x - 30.0, center_y - 30.0))
3259                                .order(egui::Order::Foreground)
3260                                .interactable(false)
3261                                .show(ctx, |ui| {
3262                                    egui::Frame::NONE
3263                                        .fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 200))
3264                                        .inner_margin(egui::Margin::symmetric(16, 8))
3265                                        .corner_radius(8.0)
3266                                        .stroke(egui::Stroke::new(
3267                                            2.0,
3268                                            egui::Color32::from_rgb(100, 200, 255),
3269                                        ))
3270                                        .show(ui, |ui| {
3271                                            ui.label(
3272                                                egui::RichText::new(format!("Pane {}", index))
3273                                                    .monospace()
3274                                                    .size(28.0)
3275                                                    .color(egui::Color32::from_rgb(100, 200, 255)),
3276                                            );
3277                                        });
3278                                });
3279                        }
3280                    }
3281
3282                    // Render file transfer progress overlay (bottom-right corner)
3283                    crate::app::file_transfers::render_file_transfer_overlay(
3284                        &self.file_transfer_state,
3285                        ctx,
3286                    );
3287
3288                    // Render badge overlay (top-right corner)
3289                    if let (Some(badge), Some(size)) = (&badge_state, window_size_for_badge) {
3290                        render_badge(ctx, badge, size.width as f32, size.height as f32);
3291                    }
3292                });
3293
3294                // Handle egui platform output (clipboard, cursor changes, etc.)
3295                // This enables cut/copy/paste in egui text editors
3296                egui_state.handle_platform_output(
3297                    self.window.as_ref().unwrap(),
3298                    egui_output.platform_output.clone(),
3299                );
3300
3301                Some((egui_output, egui_ctx))
3302            } else {
3303                None
3304            };
3305
3306            // Mark egui as initialized after first ctx.run() - makes is_using_pointer() reliable
3307            if !self.egui_initialized && egui_data.is_some() {
3308                self.egui_initialized = true;
3309            }
3310
3311            // Settings are now handled exclusively by standalone SettingsWindow
3312            // Config changes are applied via window_manager.apply_config_to_windows()
3313
3314            let debug_egui_time = egui_start.elapsed();
3315
3316            // Calculate FPS and timing stats
3317            let avg_frame_time = if !self.debug.frame_times.is_empty() {
3318                self.debug.frame_times.iter().sum::<std::time::Duration>()
3319                    / self.debug.frame_times.len() as u32
3320            } else {
3321                std::time::Duration::ZERO
3322            };
3323            let fps = if avg_frame_time.as_secs_f64() > 0.0 {
3324                1.0 / avg_frame_time.as_secs_f64()
3325            } else {
3326                0.0
3327            };
3328
3329            // Update FPS value for overlay display
3330            self.debug.fps_value = fps;
3331
3332            // Log debug info every 60 frames (about once per second at 60 FPS)
3333            if self.debug.frame_times.len() >= 60 {
3334                let (cache_gen, cache_has_cells) = self
3335                    .tab_manager
3336                    .active_tab()
3337                    .map(|t| (t.cache.generation, t.cache.cells.is_some()))
3338                    .unwrap_or((0, false));
3339                log::info!(
3340                    "PERF: FPS={:.1} Frame={:.2}ms CellGen={:.2}ms({}) URLDetect={:.2}ms Anim={:.2}ms Graphics={:.2}ms egui={:.2}ms UpdateCells={:.2}ms ActualRender={:.2}ms Total={:.2}ms Cells={} Gen={} Cache={}",
3341                    fps,
3342                    avg_frame_time.as_secs_f64() * 1000.0,
3343                    self.debug.cell_gen_time.as_secs_f64() * 1000.0,
3344                    if self.debug.cache_hit { "HIT" } else { "MISS" },
3345                    debug_url_detect_time.as_secs_f64() * 1000.0,
3346                    debug_anim_time.as_secs_f64() * 1000.0,
3347                    debug_graphics_time.as_secs_f64() * 1000.0,
3348                    debug_egui_time.as_secs_f64() * 1000.0,
3349                    debug_update_cells_time.as_secs_f64() * 1000.0,
3350                    debug_actual_render_time.as_secs_f64() * 1000.0,
3351                    self.debug.render_time.as_secs_f64() * 1000.0,
3352                    cells.len(),
3353                    cache_gen,
3354                    if cache_has_cells { "YES" } else { "NO" }
3355                );
3356            }
3357
3358            // Render (with dirty tracking optimization)
3359            let actual_render_start = std::time::Instant::now();
3360            // Settings are handled by standalone SettingsWindow, not embedded UI
3361
3362            // Extract renderer sizing info for split pane calculations
3363            let sizing = RendererSizing {
3364                size: renderer.size(),
3365                content_offset_y: renderer.content_offset_y(),
3366                content_offset_x: renderer.content_offset_x(),
3367                content_inset_bottom: renderer.content_inset_bottom(),
3368                content_inset_right: renderer.content_inset_right(),
3369                cell_width: renderer.cell_width(),
3370                cell_height: renderer.cell_height(),
3371                padding: renderer.window_padding(),
3372                status_bar_height: (status_bar_height + custom_status_bar_height)
3373                    * renderer.scale_factor(),
3374                scale_factor: renderer.scale_factor(),
3375            };
3376
3377            // Check if we have a pane manager with panes - this just checks without modifying
3378            // We use pane_count() > 0 instead of has_multiple_panes() because even with a
3379            // single pane in the manager (e.g., after closing one tmux split), we need to
3380            // render via the pane manager path since cells are in the pane's terminal,
3381            // not the main renderer buffer.
3382            let (has_pane_manager, pane_count) = self
3383                .tab_manager
3384                .active_tab()
3385                .and_then(|t| t.pane_manager.as_ref())
3386                .map(|pm| (pm.pane_count() > 0, pm.pane_count()))
3387                .unwrap_or((false, 0));
3388
3389            crate::debug_trace!(
3390                "RENDER",
3391                "has_pane_manager={}, pane_count={}",
3392                has_pane_manager,
3393                pane_count
3394            );
3395
3396            // Per-pane backgrounds only take effect when splits are active.
3397            // In single-pane mode, skip per-pane background lookup.
3398            let pane_0_bg: Option<crate::pane::PaneBackground> = None;
3399
3400            let render_result = if has_pane_manager {
3401                // Render panes from pane manager - inline data gathering to avoid borrow conflicts
3402                let content_width = sizing.size.width as f32
3403                    - sizing.padding * 2.0
3404                    - sizing.content_offset_x
3405                    - sizing.content_inset_right;
3406                let content_height = sizing.size.height as f32
3407                    - sizing.content_offset_y
3408                    - sizing.content_inset_bottom
3409                    - sizing.padding
3410                    - sizing.status_bar_height;
3411
3412                // Gather all necessary data upfront while we can borrow tab_manager
3413                #[allow(clippy::type_complexity)]
3414                let pane_render_data: Option<(
3415                    Vec<PaneRenderData>,
3416                    Vec<crate::pane::DividerRect>,
3417                    Vec<PaneTitleInfo>,
3418                    Option<PaneViewport>,
3419                )> = {
3420                    let tab = self.tab_manager.active_tab_mut();
3421                    if let Some(tab) = tab {
3422                        if let Some(pm) = &mut tab.pane_manager {
3423                            // Update bounds
3424                            let bounds = crate::pane::PaneBounds::new(
3425                                sizing.padding + sizing.content_offset_x,
3426                                sizing.content_offset_y,
3427                                content_width,
3428                                content_height,
3429                            );
3430                            pm.set_bounds(bounds);
3431
3432                            // Calculate title bar height offset for terminal sizing
3433                            // Scale from logical pixels (config) to physical pixels
3434                            let title_height_offset = if self.config.show_pane_titles {
3435                                self.config.pane_title_height * sizing.scale_factor
3436                            } else {
3437                                0.0
3438                            };
3439
3440                            // Resize all pane terminals to match their new bounds
3441                            // Scale pane_padding from logical to physical pixels
3442                            pm.resize_all_terminals_with_padding(
3443                                sizing.cell_width,
3444                                sizing.cell_height,
3445                                effective_pane_padding * sizing.scale_factor,
3446                                title_height_offset,
3447                            );
3448
3449                            // Gather pane info
3450                            let focused_pane_id = pm.focused_pane_id();
3451                            let all_pane_ids: Vec<_> =
3452                                pm.all_panes().iter().map(|p| p.id).collect();
3453                            let dividers = pm.get_dividers();
3454
3455                            let pane_bg_opacity = self.config.pane_background_opacity;
3456                            let inactive_opacity = if self.config.dim_inactive_panes {
3457                                self.config.inactive_pane_opacity
3458                            } else {
3459                                1.0
3460                            };
3461                            let cursor_opacity = self.cursor_opacity;
3462
3463                            // Pane title settings
3464                            // Scale from logical pixels (config) to physical pixels
3465                            let show_titles = self.config.show_pane_titles;
3466                            let title_height = self.config.pane_title_height * sizing.scale_factor;
3467                            let title_position = self.config.pane_title_position;
3468                            let title_text_color = [
3469                                self.config.pane_title_color[0] as f32 / 255.0,
3470                                self.config.pane_title_color[1] as f32 / 255.0,
3471                                self.config.pane_title_color[2] as f32 / 255.0,
3472                            ];
3473                            let title_bg_color = [
3474                                self.config.pane_title_bg_color[0] as f32 / 255.0,
3475                                self.config.pane_title_bg_color[1] as f32 / 255.0,
3476                                self.config.pane_title_bg_color[2] as f32 / 255.0,
3477                            ];
3478
3479                            let mut pane_data = Vec::new();
3480                            let mut pane_titles = Vec::new();
3481                            let mut focused_viewport: Option<PaneViewport> = None;
3482
3483                            for pane_id in &all_pane_ids {
3484                                if let Some(pane) = pm.get_pane(*pane_id) {
3485                                    let is_focused = Some(*pane_id) == focused_pane_id;
3486                                    let bounds = pane.bounds;
3487
3488                                    // Calculate viewport, adjusting for title bar if shown
3489                                    let (viewport_y, viewport_height) = if show_titles {
3490                                        use crate::config::PaneTitlePosition;
3491                                        match title_position {
3492                                            PaneTitlePosition::Top => (
3493                                                bounds.y + title_height,
3494                                                (bounds.height - title_height).max(0.0),
3495                                            ),
3496                                            PaneTitlePosition::Bottom => {
3497                                                (bounds.y, (bounds.height - title_height).max(0.0))
3498                                            }
3499                                        }
3500                                    } else {
3501                                        (bounds.y, bounds.height)
3502                                    };
3503
3504                                    // Create viewport with padding for content inset
3505                                    // Scale pane_padding from logical to physical pixels
3506                                    let physical_pane_padding =
3507                                        effective_pane_padding * sizing.scale_factor;
3508                                    let viewport = PaneViewport::with_padding(
3509                                        bounds.x,
3510                                        viewport_y,
3511                                        bounds.width,
3512                                        viewport_height,
3513                                        is_focused,
3514                                        if is_focused {
3515                                            pane_bg_opacity
3516                                        } else {
3517                                            pane_bg_opacity * inactive_opacity
3518                                        },
3519                                        physical_pane_padding,
3520                                    );
3521
3522                                    if is_focused {
3523                                        focused_viewport = Some(viewport);
3524                                    }
3525
3526                                    // Build pane title info
3527                                    if show_titles {
3528                                        use crate::config::PaneTitlePosition;
3529                                        let title_y = match title_position {
3530                                            PaneTitlePosition::Top => bounds.y,
3531                                            PaneTitlePosition::Bottom => {
3532                                                bounds.y + bounds.height - title_height
3533                                            }
3534                                        };
3535                                        pane_titles.push(PaneTitleInfo {
3536                                            x: bounds.x,
3537                                            y: title_y,
3538                                            width: bounds.width,
3539                                            height: title_height,
3540                                            title: pane.get_title(),
3541                                            focused: is_focused,
3542                                            text_color: title_text_color,
3543                                            bg_color: title_bg_color,
3544                                        });
3545                                    }
3546
3547                                    let cells = if let Ok(term) = pane.terminal.try_lock() {
3548                                        let scroll_offset = pane.scroll_state.offset;
3549                                        let selection =
3550                                            pane.mouse.selection.map(|sel| sel.normalized());
3551                                        let rectangular = pane
3552                                            .mouse
3553                                            .selection
3554                                            .map(|sel| sel.mode == SelectionMode::Rectangular)
3555                                            .unwrap_or(false);
3556                                        term.get_cells_with_scrollback(
3557                                            scroll_offset,
3558                                            selection,
3559                                            rectangular,
3560                                            None,
3561                                        )
3562                                    } else {
3563                                        Vec::new()
3564                                    };
3565
3566                                    let need_marks = self.config.scrollbar_command_marks
3567                                        || self.config.command_separator_enabled;
3568                                    let (marks, pane_scrollback_len) = if need_marks {
3569                                        if let Ok(mut term) = pane.terminal.try_lock() {
3570                                            // Use cursor row 0 when unknown in split panes
3571                                            let sb_len = term.scrollback_len();
3572                                            term.update_scrollback_metadata(sb_len, 0);
3573                                            (term.scrollback_marks(), sb_len)
3574                                        } else {
3575                                            (Vec::new(), 0)
3576                                        }
3577                                    } else {
3578                                        (Vec::new(), 0)
3579                                    };
3580                                    let pane_scroll_offset = pane.scroll_state.offset;
3581
3582                                    // Per-pane backgrounds only apply when multiple panes exist
3583                                    let pane_background = if all_pane_ids.len() > 1
3584                                        && pane.background().has_image()
3585                                    {
3586                                        Some(pane.background().clone())
3587                                    } else {
3588                                        None
3589                                    };
3590
3591                                    let cursor_pos = if let Ok(term) = pane.terminal.try_lock() {
3592                                        if term.is_cursor_visible() {
3593                                            Some(term.cursor_position())
3594                                        } else {
3595                                            None
3596                                        }
3597                                    } else {
3598                                        None
3599                                    };
3600
3601                                    // Grid size must match the terminal's actual size
3602                                    // (accounting for padding and title bar, same as resize_all_terminals_with_padding)
3603                                    let content_width = (bounds.width
3604                                        - physical_pane_padding * 2.0)
3605                                        .max(sizing.cell_width);
3606                                    let content_height = (viewport_height
3607                                        - physical_pane_padding * 2.0)
3608                                        .max(sizing.cell_height);
3609                                    let cols = (content_width / sizing.cell_width).floor() as usize;
3610                                    let rows =
3611                                        (content_height / sizing.cell_height).floor() as usize;
3612                                    let cols = cols.max(1);
3613                                    let rows = rows.max(1);
3614
3615                                    pane_data.push((
3616                                        viewport,
3617                                        cells,
3618                                        (cols, rows),
3619                                        cursor_pos,
3620                                        if is_focused { cursor_opacity } else { 0.0 },
3621                                        marks,
3622                                        pane_scrollback_len,
3623                                        pane_scroll_offset,
3624                                        pane_background,
3625                                    ));
3626                                }
3627                            }
3628
3629                            Some((pane_data, dividers, pane_titles, focused_viewport))
3630                        } else {
3631                            None
3632                        }
3633                    } else {
3634                        None
3635                    }
3636                };
3637
3638                if let Some((pane_data, dividers, pane_titles, focused_viewport)) = pane_render_data
3639                {
3640                    // Get hovered divider index for hover color rendering
3641                    let hovered_divider_index = self
3642                        .tab_manager
3643                        .active_tab()
3644                        .and_then(|t| t.mouse.hovered_divider_index);
3645
3646                    // Render split panes
3647                    Self::render_split_panes_with_data(
3648                        renderer,
3649                        pane_data,
3650                        dividers,
3651                        pane_titles,
3652                        focused_viewport,
3653                        &self.config,
3654                        egui_data,
3655                        hovered_divider_index,
3656                    )
3657                } else {
3658                    // Fallback to single pane render
3659                    renderer.render(egui_data, false, show_scrollbar, pane_0_bg.as_ref())
3660                }
3661            } else {
3662                // Single pane - use standard render path
3663                renderer.render(egui_data, false, show_scrollbar, pane_0_bg.as_ref())
3664            };
3665
3666            match render_result {
3667                Ok(rendered) => {
3668                    if !rendered {
3669                        log::trace!("Skipped rendering - no changes");
3670                    }
3671                }
3672                Err(e) => {
3673                    // Check if this is a wgpu surface error that requires reconfiguration
3674                    // This commonly happens when dragging windows between displays
3675                    if let Some(surface_error) = e.downcast_ref::<SurfaceError>() {
3676                        match surface_error {
3677                            SurfaceError::Outdated | SurfaceError::Lost => {
3678                                log::warn!(
3679                                    "Surface error detected ({:?}), reconfiguring...",
3680                                    surface_error
3681                                );
3682                                self.force_surface_reconfigure();
3683                            }
3684                            SurfaceError::Timeout => {
3685                                log::warn!("Surface timeout, will retry next frame");
3686                                if let Some(window) = &self.window {
3687                                    window.request_redraw();
3688                                }
3689                            }
3690                            SurfaceError::OutOfMemory => {
3691                                log::error!("Surface out of memory: {:?}", surface_error);
3692                            }
3693                            _ => {
3694                                log::error!("Surface error: {:?}", surface_error);
3695                            }
3696                        }
3697                    } else {
3698                        log::error!("Render error: {}", e);
3699                    }
3700                }
3701            }
3702            debug_actual_render_time = actual_render_start.elapsed();
3703            let _ = debug_actual_render_time;
3704
3705            self.debug.render_time = render_start.elapsed();
3706        }
3707
3708        // Sync AI Inspector panel width after the render pass.
3709        // This catches drag-resize changes that update self.ai_inspector.width during show().
3710        // Done here to avoid borrow conflicts with the renderer block above.
3711        self.sync_ai_inspector_width();
3712
3713        // Handle tab bar actions collected during egui rendering
3714        // (done here to avoid borrow conflicts with renderer)
3715        match pending_tab_action {
3716            TabBarAction::SwitchTo(id) => {
3717                self.tab_manager.switch_to(id);
3718                // Clear renderer cells and invalidate cache to ensure clean switch
3719                if let Some(renderer) = &mut self.renderer {
3720                    renderer.clear_all_cells();
3721                }
3722                if let Some(tab) = self.tab_manager.active_tab_mut() {
3723                    tab.cache.cells = None;
3724                }
3725                self.needs_redraw = true;
3726                if let Some(window) = &self.window {
3727                    window.request_redraw();
3728                }
3729            }
3730            TabBarAction::Close(id) => {
3731                // Switch to the tab first so close_current_tab() operates on it.
3732                // This routes through the full close path: running-jobs confirmation,
3733                // session undo capture, and preserve-shell logic.
3734                self.tab_manager.switch_to(id);
3735                let was_last = self.close_current_tab();
3736                if was_last {
3737                    self.is_shutting_down = true;
3738                }
3739                if let Some(window) = &self.window {
3740                    window.request_redraw();
3741                }
3742            }
3743            TabBarAction::NewTab => {
3744                self.new_tab();
3745                if let Some(window) = &self.window {
3746                    window.request_redraw();
3747                }
3748            }
3749            TabBarAction::SetColor(id, color) => {
3750                if let Some(tab) = self.tab_manager.get_tab_mut(id) {
3751                    tab.set_custom_color(color);
3752                    log::info!(
3753                        "Set custom color for tab {}: RGB({}, {}, {})",
3754                        id,
3755                        color[0],
3756                        color[1],
3757                        color[2]
3758                    );
3759                }
3760                if let Some(window) = &self.window {
3761                    window.request_redraw();
3762                }
3763            }
3764            TabBarAction::ClearColor(id) => {
3765                if let Some(tab) = self.tab_manager.get_tab_mut(id) {
3766                    tab.clear_custom_color();
3767                    log::info!("Cleared custom color for tab {}", id);
3768                }
3769                if let Some(window) = &self.window {
3770                    window.request_redraw();
3771                }
3772            }
3773            TabBarAction::Reorder(id, target_index) => {
3774                if self.tab_manager.move_tab_to_index(id, target_index) {
3775                    self.needs_redraw = true;
3776                    if let Some(window) = &self.window {
3777                        window.request_redraw();
3778                    }
3779                }
3780            }
3781            TabBarAction::NewTabWithProfile(profile_id) => {
3782                self.open_profile(profile_id);
3783                if let Some(window) = &self.window {
3784                    window.request_redraw();
3785                }
3786            }
3787            TabBarAction::Duplicate(id) => {
3788                self.duplicate_tab_by_id(id);
3789                if let Some(window) = &self.window {
3790                    window.request_redraw();
3791                }
3792            }
3793            TabBarAction::None => {}
3794        }
3795
3796        // Handle clipboard actions collected during egui rendering
3797        // (done here to avoid borrow conflicts with renderer)
3798        match pending_clipboard_action {
3799            ClipboardHistoryAction::Paste(content) => {
3800                self.paste_text(&content);
3801            }
3802            ClipboardHistoryAction::ClearAll => {
3803                if let Some(tab) = self.tab_manager.active_tab()
3804                    && let Ok(term) = tab.terminal.try_lock()
3805                {
3806                    term.clear_all_clipboard_history();
3807                    log::info!("Cleared all clipboard history");
3808                }
3809                self.clipboard_history_ui.update_entries(Vec::new());
3810            }
3811            ClipboardHistoryAction::ClearSlot(slot) => {
3812                if let Some(tab) = self.tab_manager.active_tab()
3813                    && let Ok(term) = tab.terminal.try_lock()
3814                {
3815                    term.clear_clipboard_history(slot);
3816                    log::info!("Cleared clipboard history for slot {:?}", slot);
3817                }
3818            }
3819            ClipboardHistoryAction::None => {}
3820        }
3821
3822        // Handle command history actions collected during egui rendering
3823        match pending_command_history_action {
3824            CommandHistoryAction::Insert(command) => {
3825                self.paste_text(&command);
3826                log::info!(
3827                    "Inserted command from history: {}",
3828                    &command[..command.len().min(60)]
3829                );
3830            }
3831            CommandHistoryAction::None => {}
3832        }
3833
3834        // Handle close confirmation dialog actions
3835        match pending_close_confirm_action {
3836            CloseConfirmAction::Close { tab_id, pane_id } => {
3837                // User confirmed close - close the tab/pane
3838                if let Some(pane_id) = pane_id {
3839                    // Close specific pane
3840                    if let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
3841                        && let Some(pm) = tab.pane_manager_mut()
3842                    {
3843                        pm.close_pane(pane_id);
3844                        log::info!("Force-closed pane {} in tab {}", pane_id, tab_id);
3845                    }
3846                } else {
3847                    // Close entire tab
3848                    self.tab_manager.close_tab(tab_id);
3849                    log::info!("Force-closed tab {}", tab_id);
3850                }
3851                self.needs_redraw = true;
3852                if let Some(window) = &self.window {
3853                    window.request_redraw();
3854                }
3855            }
3856            CloseConfirmAction::Cancel => {
3857                // User cancelled - do nothing, dialog already hidden
3858                log::debug!("Close confirmation cancelled");
3859            }
3860            CloseConfirmAction::None => {}
3861        }
3862
3863        // Handle quit confirmation dialog actions
3864        match pending_quit_confirm_action {
3865            QuitConfirmAction::Quit => {
3866                // User confirmed quit - proceed with shutdown
3867                log::info!("Quit confirmed by user");
3868                self.perform_shutdown();
3869            }
3870            QuitConfirmAction::Cancel => {
3871                log::debug!("Quit confirmation cancelled");
3872            }
3873            QuitConfirmAction::None => {}
3874        }
3875
3876        // Handle remote shell integration install action
3877        match pending_remote_install_action {
3878            RemoteShellInstallAction::Install => {
3879                // Send the install command to the active terminal
3880                let command = RemoteShellInstallUI::install_command();
3881                if let Some(tab) = self.tab_manager.active_tab()
3882                    && let Ok(term) = tab.terminal.try_lock()
3883                {
3884                    let _ = term.write_str(&format!("{}\r", command));
3885                }
3886            }
3887            RemoteShellInstallAction::Cancel => {
3888                // Nothing to do - dialog already hidden
3889            }
3890            RemoteShellInstallAction::None => {}
3891        }
3892
3893        // Handle SSH Quick Connect actions
3894        match pending_ssh_connect_action {
3895            SshConnectAction::Connect {
3896                host,
3897                profile_override: _,
3898            } => {
3899                // Build SSH command and write it to the active terminal's PTY
3900                let args = host.ssh_args();
3901                let ssh_cmd = format!("ssh {}\n", args.join(" "));
3902                if let Some(tab) = self.tab_manager.active_tab()
3903                    && let Ok(term) = tab.terminal.try_lock()
3904                {
3905                    let _ = term.write_str(&ssh_cmd);
3906                }
3907                log::info!(
3908                    "SSH Quick Connect: connecting to {}",
3909                    host.connection_string()
3910                );
3911                if let Some(window) = &self.window {
3912                    window.request_redraw();
3913                }
3914            }
3915            SshConnectAction::Cancel => {
3916                if let Some(window) = &self.window {
3917                    window.request_redraw();
3918                }
3919            }
3920            SshConnectAction::None => {}
3921        }
3922
3923        // Handle paste special actions collected during egui rendering
3924        match pending_paste_special_action {
3925            PasteSpecialAction::Paste(content) => {
3926                self.paste_text(&content);
3927                log::debug!("Pasted transformed text ({} chars)", content.len());
3928            }
3929            PasteSpecialAction::None => {}
3930        }
3931
3932        // Handle search actions collected during egui rendering
3933        match pending_search_action {
3934            crate::search::SearchAction::ScrollToMatch(offset) => {
3935                self.set_scroll_target(offset);
3936                self.needs_redraw = true;
3937                if let Some(window) = &self.window {
3938                    window.request_redraw();
3939                }
3940            }
3941            crate::search::SearchAction::Close => {
3942                self.needs_redraw = true;
3943                if let Some(window) = &self.window {
3944                    window.request_redraw();
3945                }
3946            }
3947            crate::search::SearchAction::None => {}
3948        }
3949
3950        // Handle AI Inspector actions collected during egui rendering
3951        match pending_inspector_action {
3952            InspectorAction::Close => {
3953                self.ai_inspector.open = false;
3954                self.sync_ai_inspector_width();
3955            }
3956            InspectorAction::CopyJson(json) => {
3957                if let Ok(mut clipboard) = arboard::Clipboard::new() {
3958                    let _ = clipboard.set_text(json);
3959                }
3960            }
3961            InspectorAction::SaveToFile(json) => {
3962                if let Some(path) = rfd::FileDialog::new()
3963                    .set_file_name(format!(
3964                        "par-term-snapshot-{}.json",
3965                        chrono::Local::now().format("%Y-%m-%d-%H%M%S")
3966                    ))
3967                    .add_filter("JSON", &["json"])
3968                    .save_file()
3969                {
3970                    let _ = std::fs::write(path, json);
3971                }
3972            }
3973            InspectorAction::WriteToTerminal(cmd) => {
3974                if let Some(tab) = self.tab_manager.active_tab()
3975                    && let Ok(term) = tab.terminal.try_lock()
3976                {
3977                    let _ = term.write(cmd.as_bytes());
3978                }
3979            }
3980            InspectorAction::RunCommandAndNotify(cmd) => {
3981                // Write command + Enter to terminal
3982                if let Some(tab) = self.tab_manager.active_tab()
3983                    && let Ok(term) = tab.terminal.try_lock()
3984                {
3985                    let _ = term.write(format!("{cmd}\n").as_bytes());
3986                }
3987                // Record command count before execution so we can detect completion
3988                let history_len = self
3989                    .tab_manager
3990                    .active_tab()
3991                    .and_then(|tab| tab.terminal.try_lock().ok())
3992                    .map(|term| term.core_command_history().len())
3993                    .unwrap_or(0);
3994                // Spawn a task that polls for command completion and notifies the agent
3995                if let Some(agent) = &self.agent {
3996                    let agent = agent.clone();
3997                    let tx = self.agent_tx.clone();
3998                    let terminal = self
3999                        .tab_manager
4000                        .active_tab()
4001                        .map(|tab| tab.terminal.clone());
4002                    let cmd_for_msg = cmd.clone();
4003                    self.runtime.spawn(async move {
4004                        // Poll for command completion (up to 30 seconds)
4005                        let mut exit_code: Option<i32> = None;
4006                        for _ in 0..300 {
4007                            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
4008                            if let Some(ref terminal) = terminal
4009                                && let Ok(term) = terminal.try_lock()
4010                            {
4011                                let history = term.core_command_history();
4012                                if history.len() > history_len {
4013                                    // New command finished
4014                                    if let Some(last) = history.last() {
4015                                        exit_code = last.1;
4016                                    }
4017                                    break;
4018                                }
4019                            }
4020                        }
4021                        // Send feedback to agent
4022                        let exit_str = exit_code
4023                            .map(|c| format!("exit code {c}"))
4024                            .unwrap_or_else(|| "unknown exit code".to_string());
4025                        let feedback = format!(
4026                            "[System: The user executed `{cmd_for_msg}` in their terminal ({exit_str}). \
4027                             The output is available through the normal terminal capture.]"
4028                        );
4029                        let content = vec![par_term_acp::ContentBlock::Text {
4030                            text: feedback,
4031                        }];
4032                        let agent = agent.lock().await;
4033                        let _ = agent.send_prompt(content).await;
4034                        if let Some(tx) = tx {
4035                            let _ = tx.send(par_term_acp::AgentMessage::PromptComplete);
4036                        }
4037                    });
4038                }
4039                self.needs_redraw = true;
4040            }
4041            InspectorAction::ConnectAgent(identity) => {
4042                self.connect_agent(&identity);
4043            }
4044            InspectorAction::DisconnectAgent => {
4045                if let Some(agent) = self.agent.take() {
4046                    self.runtime.spawn(async move {
4047                        let mut agent = agent.lock().await;
4048                        agent.disconnect().await;
4049                    });
4050                }
4051                self.agent_rx = None;
4052                self.agent_tx = None;
4053                self.agent_client = None;
4054                self.ai_inspector.agent_status = AgentStatus::Disconnected;
4055                self.needs_redraw = true;
4056            }
4057            InspectorAction::SendPrompt(text) => {
4058                self.ai_inspector.chat.add_user_message(text.clone());
4059                if let Some(agent) = &self.agent {
4060                    let agent = agent.clone();
4061                    // Build the prompt with optional system guidance and shader context.
4062                    let mut prompt_text = String::new();
4063
4064                    // Prepend system guidance on the first prompt so the agent
4065                    // knows to wrap commands in fenced code blocks.
4066                    if !self.ai_inspector.chat.system_prompt_sent {
4067                        self.ai_inspector.chat.system_prompt_sent = true;
4068                        prompt_text.push_str(crate::ai_inspector::chat::AGENT_SYSTEM_GUIDANCE);
4069                    }
4070
4071                    // Inject shader context when relevant (keyword match or active shaders).
4072                    if crate::ai_inspector::shader_context::should_inject_shader_context(
4073                        &text,
4074                        &self.config,
4075                    ) {
4076                        prompt_text.push_str(
4077                            &crate::ai_inspector::shader_context::build_shader_context(
4078                                &self.config,
4079                            ),
4080                        );
4081                    }
4082
4083                    prompt_text.push_str(&text);
4084
4085                    let content = vec![par_term_acp::ContentBlock::Text { text: prompt_text }];
4086                    let tx = self.agent_tx.clone();
4087                    self.runtime.spawn(async move {
4088                        let agent = agent.lock().await;
4089                        let _ = agent.send_prompt(content).await;
4090                        // Signal the UI to flush the agent text buffer so
4091                        // command suggestions are extracted.
4092                        if let Some(tx) = tx {
4093                            let _ = tx.send(AgentMessage::PromptComplete);
4094                        }
4095                    });
4096                }
4097                self.needs_redraw = true;
4098            }
4099            InspectorAction::SetTerminalAccess(enabled) => {
4100                self.config.ai_inspector_agent_terminal_access = enabled;
4101                self.needs_redraw = true;
4102            }
4103            InspectorAction::RespondPermission {
4104                request_id,
4105                option_id,
4106                cancelled,
4107            } => {
4108                if let Some(client) = &self.agent_client {
4109                    let client = client.clone();
4110                    let action = if cancelled { "cancelled" } else { "selected" };
4111                    log::info!("ACP: sending permission response id={request_id} action={action}");
4112                    self.runtime.spawn(async move {
4113                        use par_term_acp::{PermissionOutcome, RequestPermissionResponse};
4114                        let outcome = if cancelled {
4115                            PermissionOutcome {
4116                                outcome: "cancelled".to_string(),
4117                                option_id: None,
4118                            }
4119                        } else {
4120                            PermissionOutcome {
4121                                outcome: "selected".to_string(),
4122                                option_id: Some(option_id),
4123                            }
4124                        };
4125                        let result = RequestPermissionResponse { outcome };
4126                        if let Err(e) = client
4127                            .respond(
4128                                request_id,
4129                                Some(serde_json::to_value(&result).unwrap()),
4130                                None,
4131                            )
4132                            .await
4133                        {
4134                            log::error!("ACP: failed to send permission response: {e}");
4135                        }
4136                    });
4137                } else {
4138                    log::error!(
4139                        "ACP: cannot send permission response id={request_id} — agent_client is None!"
4140                    );
4141                }
4142                // Mark the permission as resolved in the chat.
4143                for msg in &mut self.ai_inspector.chat.messages {
4144                    if let ChatMessage::Permission {
4145                        request_id: rid,
4146                        resolved,
4147                        ..
4148                    } = msg
4149                        && *rid == request_id
4150                    {
4151                        *resolved = true;
4152                        break;
4153                    }
4154                }
4155                self.needs_redraw = true;
4156            }
4157            InspectorAction::SetAgentMode(mode_id) => {
4158                let is_yolo = mode_id == "bypassPermissions";
4159                self.config.ai_inspector_auto_approve = is_yolo;
4160                if let Some(agent) = &self.agent {
4161                    let agent = agent.clone();
4162                    self.runtime.spawn(async move {
4163                        let agent = agent.lock().await;
4164                        agent
4165                            .auto_approve
4166                            .store(is_yolo, std::sync::atomic::Ordering::Relaxed);
4167                        if let Err(e) = agent.set_mode(&mode_id).await {
4168                            log::error!("ACP: failed to set mode '{mode_id}': {e}");
4169                        }
4170                    });
4171                }
4172                self.needs_redraw = true;
4173            }
4174            InspectorAction::None => {}
4175        }
4176
4177        // Handle tmux session picker actions collected during egui rendering
4178        // Uses gateway mode: writes tmux commands to existing PTY instead of spawning process
4179        match pending_session_picker_action {
4180            SessionPickerAction::Attach(session_name) => {
4181                crate::debug_info!(
4182                    "TMUX",
4183                    "Session picker: attaching to '{}' via gateway",
4184                    session_name
4185                );
4186                if let Err(e) = self.attach_tmux_gateway(&session_name) {
4187                    log::error!("Failed to attach to tmux session '{}': {}", session_name, e);
4188                    self.show_toast(format!("Failed to attach: {}", e));
4189                } else {
4190                    crate::debug_info!("TMUX", "Gateway initiated for session '{}'", session_name);
4191                    self.show_toast(format!("Connecting to session '{}'...", session_name));
4192                }
4193                self.needs_redraw = true;
4194            }
4195            SessionPickerAction::CreateNew(name) => {
4196                crate::debug_info!(
4197                    "TMUX",
4198                    "Session picker: creating new session {:?} via gateway",
4199                    name
4200                );
4201                if let Err(e) = self.initiate_tmux_gateway(name.as_deref()) {
4202                    log::error!("Failed to create tmux session: {}", e);
4203                    crate::debug_error!("TMUX", "Failed to initiate gateway: {}", e);
4204                    self.show_toast(format!("Failed to create session: {}", e));
4205                } else {
4206                    let msg = match name {
4207                        Some(ref n) => format!("Creating session '{}'...", n),
4208                        None => "Creating new tmux session...".to_string(),
4209                    };
4210                    crate::debug_info!("TMUX", "Gateway initiated: {}", msg);
4211                    self.show_toast(msg);
4212                }
4213                self.needs_redraw = true;
4214            }
4215            SessionPickerAction::None => {}
4216        }
4217
4218        // Check for shader installation completion from background thread
4219        if let Some(ref rx) = self.shader_install_receiver
4220            && let Ok(result) = rx.try_recv()
4221        {
4222            match result {
4223                Ok(count) => {
4224                    log::info!("Successfully installed {} shaders", count);
4225                    self.shader_install_ui
4226                        .set_success(&format!("Installed {} shaders!", count));
4227
4228                    // Update config to mark as installed
4229                    self.config.shader_install_prompt = ShaderInstallPrompt::Installed;
4230                    if let Err(e) = self.config.save() {
4231                        log::error!("Failed to save config after shader install: {}", e);
4232                    }
4233                }
4234                Err(e) => {
4235                    log::error!("Failed to install shaders: {}", e);
4236                    self.shader_install_ui.set_error(&e);
4237                }
4238            }
4239            self.shader_install_receiver = None;
4240            self.needs_redraw = true;
4241        }
4242
4243        // Handle shader install responses
4244        match pending_shader_install_response {
4245            ShaderInstallResponse::Install => {
4246                log::info!("User requested shader installation");
4247                self.shader_install_ui
4248                    .set_installing("Downloading shaders...");
4249                self.needs_redraw = true;
4250
4251                // Spawn installation in background thread so UI can show progress
4252                let (tx, rx) = std::sync::mpsc::channel();
4253                self.shader_install_receiver = Some(rx);
4254
4255                std::thread::spawn(move || {
4256                    let result = crate::shader_install_ui::install_shaders_headless();
4257                    let _ = tx.send(result);
4258                });
4259
4260                // Request redraw so the spinner shows
4261                if let Some(window) = &self.window {
4262                    window.request_redraw();
4263                }
4264            }
4265            ShaderInstallResponse::Never => {
4266                log::info!("User declined shader installation (never ask again)");
4267                self.shader_install_ui.hide();
4268
4269                // Update config to never ask again
4270                self.config.shader_install_prompt = ShaderInstallPrompt::Never;
4271                if let Err(e) = self.config.save() {
4272                    log::error!("Failed to save config after declining shaders: {}", e);
4273                }
4274            }
4275            ShaderInstallResponse::Later => {
4276                log::info!("User deferred shader installation");
4277                self.shader_install_ui.hide();
4278                // Config remains "ask" - will prompt again on next startup
4279            }
4280            ShaderInstallResponse::None => {}
4281        }
4282
4283        // Handle integrations welcome dialog responses
4284        self.handle_integrations_response(&pending_integrations_response);
4285
4286        // Handle profile drawer actions
4287        match pending_profile_drawer_action {
4288            ProfileDrawerAction::OpenProfile(id) => {
4289                self.open_profile(id);
4290            }
4291            ProfileDrawerAction::ManageProfiles => {
4292                // Open settings window to Profiles tab instead of terminal-embedded modal
4293                self.open_settings_window_requested = true;
4294                self.open_settings_profiles_tab = true;
4295            }
4296            ProfileDrawerAction::None => {}
4297        }
4298
4299        let absolute_total = absolute_start.elapsed();
4300        if absolute_total.as_millis() > 10 {
4301            log::debug!(
4302                "TIMING: AbsoluteTotal={:.2}ms (from function start to end)",
4303                absolute_total.as_secs_f64() * 1000.0
4304            );
4305        }
4306    }
4307
4308    /// Render split panes when the active tab has multiple panes
4309    #[allow(clippy::too_many_arguments)]
4310    fn render_split_panes_with_data(
4311        renderer: &mut Renderer,
4312        pane_data: Vec<PaneRenderData>,
4313        dividers: Vec<crate::pane::DividerRect>,
4314        pane_titles: Vec<PaneTitleInfo>,
4315        focused_viewport: Option<PaneViewport>,
4316        config: &Config,
4317        egui_data: Option<(egui::FullOutput, &egui::Context)>,
4318        hovered_divider_index: Option<usize>,
4319    ) -> Result<bool> {
4320        // Build pane render infos - we need to leak the cells temporarily
4321        let mut pane_render_infos: Vec<PaneRenderInfo> = Vec::new();
4322        let mut leaked_cells: Vec<*mut [crate::cell_renderer::Cell]> = Vec::new();
4323
4324        for (
4325            viewport,
4326            cells,
4327            grid_size,
4328            cursor_pos,
4329            cursor_opacity,
4330            marks,
4331            scrollback_len,
4332            scroll_offset,
4333            pane_background,
4334        ) in pane_data
4335        {
4336            let cells_boxed = cells.into_boxed_slice();
4337            let cells_ptr = Box::into_raw(cells_boxed);
4338            leaked_cells.push(cells_ptr);
4339
4340            pane_render_infos.push(PaneRenderInfo {
4341                viewport,
4342                // SAFETY: We just allocated this, and we'll free it after rendering
4343                cells: unsafe { &*cells_ptr },
4344                grid_size,
4345                cursor_pos,
4346                cursor_opacity,
4347                show_scrollbar: false,
4348                marks,
4349                scrollback_len,
4350                scroll_offset,
4351                background: pane_background,
4352            });
4353        }
4354
4355        // Build divider render info
4356        let divider_render_infos: Vec<DividerRenderInfo> = dividers
4357            .iter()
4358            .enumerate()
4359            .map(|(i, d)| DividerRenderInfo::from_rect(d, hovered_divider_index == Some(i)))
4360            .collect();
4361
4362        // Build divider settings from config
4363        let divider_settings = PaneDividerSettings {
4364            divider_color: [
4365                config.pane_divider_color[0] as f32 / 255.0,
4366                config.pane_divider_color[1] as f32 / 255.0,
4367                config.pane_divider_color[2] as f32 / 255.0,
4368            ],
4369            hover_color: [
4370                config.pane_divider_hover_color[0] as f32 / 255.0,
4371                config.pane_divider_hover_color[1] as f32 / 255.0,
4372                config.pane_divider_hover_color[2] as f32 / 255.0,
4373            ],
4374            show_focus_indicator: config.pane_focus_indicator,
4375            focus_color: [
4376                config.pane_focus_color[0] as f32 / 255.0,
4377                config.pane_focus_color[1] as f32 / 255.0,
4378                config.pane_focus_color[2] as f32 / 255.0,
4379            ],
4380            focus_width: config.pane_focus_width * renderer.scale_factor(),
4381            divider_style: config.pane_divider_style,
4382        };
4383
4384        // Call the split pane renderer
4385        let result = renderer.render_split_panes(
4386            &pane_render_infos,
4387            &divider_render_infos,
4388            &pane_titles,
4389            focused_viewport.as_ref(),
4390            &divider_settings,
4391            egui_data,
4392            false,
4393        );
4394
4395        // Clean up leaked cell memory
4396        for ptr in leaked_cells {
4397            // SAFETY: We just allocated these above
4398            let _ = unsafe { Box::from_raw(ptr) };
4399        }
4400
4401        result
4402    }
4403
4404    /// Handle responses from the integrations welcome dialog
4405    fn handle_integrations_response(&mut self, response: &IntegrationsResponse) {
4406        // Nothing to do if dialog wasn't interacted with
4407        if !response.install_shaders
4408            && !response.install_shell_integration
4409            && !response.skipped
4410            && !response.never_ask
4411            && !response.closed
4412            && response.shader_conflict_action.is_none()
4413        {
4414            return;
4415        }
4416
4417        let current_version = env!("CARGO_PKG_VERSION").to_string();
4418
4419        // Determine install intent and overwrite behavior
4420        let mut install_shaders = false;
4421        let mut install_shell_integration = false;
4422        let mut force_overwrite_modified_shaders = false;
4423        let mut triggered_install = false;
4424
4425        // If we're waiting on a shader overwrite decision, handle that first
4426        if let Some(action) = response.shader_conflict_action {
4427            triggered_install = true;
4428            install_shaders = self.integrations_ui.pending_install_shaders;
4429            install_shell_integration = self.integrations_ui.pending_install_shell_integration;
4430
4431            match action {
4432                crate::integrations_ui::ShaderConflictAction::Overwrite => {
4433                    force_overwrite_modified_shaders = true;
4434                }
4435                crate::integrations_ui::ShaderConflictAction::SkipModified => {
4436                    force_overwrite_modified_shaders = false;
4437                }
4438                crate::integrations_ui::ShaderConflictAction::Cancel => {
4439                    // Reset pending state and exit without installing
4440                    self.integrations_ui.awaiting_shader_overwrite = false;
4441                    self.integrations_ui.shader_conflicts.clear();
4442                    self.integrations_ui.pending_install_shaders = false;
4443                    self.integrations_ui.pending_install_shell_integration = false;
4444                    self.integrations_ui.error_message = None;
4445                    self.integrations_ui.success_message = None;
4446                    self.needs_redraw = true;
4447                    return;
4448                }
4449            }
4450
4451            // Clear the conflict prompt regardless of choice
4452            self.integrations_ui.awaiting_shader_overwrite = false;
4453            self.integrations_ui.shader_conflicts.clear();
4454            self.integrations_ui.error_message = None;
4455            self.integrations_ui.success_message = None;
4456            self.integrations_ui.installing = false;
4457        } else if response.install_shaders || response.install_shell_integration {
4458            triggered_install = true;
4459            install_shaders = response.install_shaders;
4460            install_shell_integration = response.install_shell_integration;
4461
4462            if install_shaders {
4463                match crate::shader_installer::detect_modified_bundled_shaders() {
4464                    Ok(conflicts) if !conflicts.is_empty() => {
4465                        log::info!(
4466                            "Detected {} modified bundled shaders; prompting for overwrite",
4467                            conflicts.len()
4468                        );
4469                        self.integrations_ui.awaiting_shader_overwrite = true;
4470                        self.integrations_ui.shader_conflicts = conflicts;
4471                        self.integrations_ui.pending_install_shaders = install_shaders;
4472                        self.integrations_ui.pending_install_shell_integration =
4473                            install_shell_integration;
4474                        self.integrations_ui.installing = false;
4475                        self.integrations_ui.error_message = None;
4476                        self.integrations_ui.success_message = None;
4477                        self.needs_redraw = true;
4478                        return; // Wait for user decision
4479                    }
4480                    Ok(_) => {}
4481                    Err(e) => {
4482                        log::warn!(
4483                            "Unable to check existing shaders for modifications: {}. Proceeding without overwrite prompt.",
4484                            e
4485                        );
4486                    }
4487                }
4488            }
4489        }
4490
4491        // Handle "Install Selected" - user wants to install one or both integrations
4492        if triggered_install {
4493            log::info!(
4494                "User requested installations: shaders={}, shell_integration={}, overwrite_modified={}",
4495                install_shaders,
4496                install_shell_integration,
4497                force_overwrite_modified_shaders
4498            );
4499
4500            let mut success_parts = Vec::new();
4501            let mut error_parts = Vec::new();
4502
4503            // Install shaders if requested
4504            if install_shaders {
4505                self.integrations_ui.set_installing("Installing shaders...");
4506                self.needs_redraw = true;
4507                self.request_redraw();
4508
4509                match crate::shader_installer::install_shaders_with_manifest(
4510                    force_overwrite_modified_shaders,
4511                ) {
4512                    Ok(result) => {
4513                        log::info!(
4514                            "Installed {} shader files ({} skipped, {} removed)",
4515                            result.installed,
4516                            result.skipped,
4517                            result.removed
4518                        );
4519                        let detail = if result.skipped > 0 {
4520                            format!("{} shaders ({} skipped)", result.installed, result.skipped)
4521                        } else {
4522                            format!("{} shaders", result.installed)
4523                        };
4524                        success_parts.push(detail);
4525                        self.config.integration_versions.shaders_installed_version =
4526                            Some(current_version.clone());
4527                        self.config.integration_versions.shaders_prompted_version =
4528                            Some(current_version.clone());
4529                    }
4530                    Err(e) => {
4531                        log::error!("Failed to install shaders: {}", e);
4532                        error_parts.push(format!("Shaders: {}", e));
4533                    }
4534                }
4535            }
4536
4537            // Install shell integration if requested
4538            if install_shell_integration {
4539                self.integrations_ui
4540                    .set_installing("Installing shell integration...");
4541                self.needs_redraw = true;
4542                self.request_redraw();
4543
4544                match crate::shell_integration_installer::install(None) {
4545                    Ok(result) => {
4546                        log::info!(
4547                            "Installed shell integration for {}",
4548                            result.shell.display_name()
4549                        );
4550                        success_parts.push(format!(
4551                            "shell integration ({})",
4552                            result.shell.display_name()
4553                        ));
4554                        self.config
4555                            .integration_versions
4556                            .shell_integration_installed_version = Some(current_version.clone());
4557                        self.config
4558                            .integration_versions
4559                            .shell_integration_prompted_version = Some(current_version.clone());
4560                    }
4561                    Err(e) => {
4562                        log::error!("Failed to install shell integration: {}", e);
4563                        error_parts.push(format!("Shell: {}", e));
4564                    }
4565                }
4566            }
4567
4568            // Show result
4569            if error_parts.is_empty() {
4570                self.integrations_ui
4571                    .set_success(&format!("Installed: {}", success_parts.join(", ")));
4572            } else if success_parts.is_empty() {
4573                self.integrations_ui
4574                    .set_error(&format!("Installation failed: {}", error_parts.join("; ")));
4575            } else {
4576                // Partial success
4577                self.integrations_ui.set_success(&format!(
4578                    "Installed: {}. Errors: {}",
4579                    success_parts.join(", "),
4580                    error_parts.join("; ")
4581                ));
4582            }
4583
4584            // Save config
4585            if let Err(e) = self.config.save() {
4586                log::error!("Failed to save config after integration install: {}", e);
4587            }
4588
4589            // Clear pending flags
4590            self.integrations_ui.pending_install_shaders = false;
4591            self.integrations_ui.pending_install_shell_integration = false;
4592
4593            self.needs_redraw = true;
4594        }
4595
4596        // Handle "Skip" - just close the dialog for this session
4597        if response.skipped {
4598            log::info!("User skipped integrations dialog for this session");
4599            self.integrations_ui.hide();
4600            // Update prompted versions so we don't ask again this version
4601            self.config.integration_versions.shaders_prompted_version =
4602                Some(current_version.clone());
4603            self.config
4604                .integration_versions
4605                .shell_integration_prompted_version = Some(current_version.clone());
4606            if let Err(e) = self.config.save() {
4607                log::error!("Failed to save config after skipping integrations: {}", e);
4608            }
4609        }
4610
4611        // Handle "Never Ask" - disable prompting permanently
4612        if response.never_ask {
4613            log::info!("User declined integrations (never ask again)");
4614            self.integrations_ui.hide();
4615            // Set install prompts to Never
4616            self.config.shader_install_prompt = ShaderInstallPrompt::Never;
4617            self.config.shell_integration_state = crate::config::InstallPromptState::Never;
4618            if let Err(e) = self.config.save() {
4619                log::error!("Failed to save config after declining integrations: {}", e);
4620            }
4621        }
4622
4623        // Handle dialog closed (OK button after success)
4624        if response.closed {
4625            self.integrations_ui.hide();
4626        }
4627    }
4628
4629    /// Perform the shutdown sequence (save state and set shutdown flag)
4630    pub(crate) fn perform_shutdown(&mut self) {
4631        // Save last working directory for "previous session" mode
4632        if self.config.startup_directory_mode == crate::config::StartupDirectoryMode::Previous
4633            && let Some(tab) = self.tab_manager.active_tab()
4634            && let Ok(term) = tab.terminal.try_lock()
4635            && let Some(cwd) = term.shell_integration_cwd()
4636        {
4637            log::info!("Saving last working directory: {}", cwd);
4638            if let Err(e) = self.config.save_last_working_directory(&cwd) {
4639                log::warn!("Failed to save last working directory: {}", e);
4640            }
4641        }
4642
4643        // Set shutdown flag to stop redraw loop
4644        self.is_shutting_down = true;
4645        // Abort refresh tasks for all tabs
4646        for tab in self.tab_manager.tabs_mut() {
4647            if let Some(task) = tab.refresh_task.take() {
4648                task.abort();
4649            }
4650        }
4651        log::info!("Refresh tasks aborted, shutdown initiated");
4652    }
4653}
4654
4655// ---------------------------------------------------------------------------
4656impl Drop for WindowState {
4657    fn drop(&mut self) {
4658        let t0 = std::time::Instant::now();
4659        log::info!("Shutting down window (fast path)");
4660
4661        // Signal status bar polling threads to stop immediately.
4662        // They check the flag every 50ms, so by the time the auto-drop
4663        // calls join() later, the threads will already be exiting.
4664        self.status_bar_ui.signal_shutdown();
4665
4666        // Save command history on a background thread (serializes in-memory, writes async)
4667        self.command_history.save_background();
4668
4669        // Set shutdown flag
4670        self.is_shutting_down = true;
4671
4672        // Hide the window immediately for instant visual feedback
4673        if let Some(ref window) = self.window {
4674            window.set_visible(false);
4675            log::info!(
4676                "Window hidden for instant visual close (+{:.1}ms)",
4677                t0.elapsed().as_secs_f64() * 1000.0
4678            );
4679        }
4680
4681        // Clean up egui state FIRST before any other resources are dropped
4682        self.egui_state = None;
4683        self.egui_ctx = None;
4684
4685        // Drain all tabs from the manager (takes ownership without dropping)
4686        let mut tabs = self.tab_manager.drain_tabs();
4687        let tab_count = tabs.len();
4688        log::info!(
4689            "Fast shutdown: draining {} tabs (+{:.1}ms)",
4690            tab_count,
4691            t0.elapsed().as_secs_f64() * 1000.0
4692        );
4693
4694        // Collect terminal Arcs and session loggers from all tabs and panes
4695        // BEFORE setting shutdown_fast. Cloning the Arc keeps TerminalManager
4696        // alive even after Tab/Pane is dropped. Session loggers are collected
4697        // so they can be stopped on a background thread instead of blocking.
4698        let mut terminal_arcs = Vec::new();
4699        let mut session_loggers = Vec::new();
4700
4701        for tab in &mut tabs {
4702            // Stop refresh tasks (fast - just aborts tokio tasks)
4703            tab.stop_refresh_task();
4704
4705            // Collect session logger for background stop
4706            session_loggers.push(Arc::clone(&tab.session_logger));
4707
4708            // Clone terminal Arc before we mark shutdown_fast
4709            terminal_arcs.push(Arc::clone(&tab.terminal));
4710
4711            // Also handle panes if this tab has splits
4712            if let Some(ref mut pm) = tab.pane_manager {
4713                for pane in pm.all_panes_mut() {
4714                    pane.stop_refresh_task();
4715                    session_loggers.push(Arc::clone(&pane.session_logger));
4716                    terminal_arcs.push(Arc::clone(&pane.terminal));
4717                    pane.shutdown_fast = true;
4718                }
4719            }
4720
4721            // Mark tab for fast drop (skips sleep + kill in Tab::drop)
4722            tab.shutdown_fast = true;
4723        }
4724
4725        // Pre-kill all PTY processes (sends SIGKILL, fast non-blocking)
4726        for arc in &terminal_arcs {
4727            if let Ok(mut term) = arc.try_lock()
4728                && term.is_running()
4729            {
4730                let _ = term.kill();
4731            }
4732        }
4733        log::info!(
4734            "Pre-killed {} terminal sessions (+{:.1}ms)",
4735            terminal_arcs.len(),
4736            t0.elapsed().as_secs_f64() * 1000.0
4737        );
4738
4739        // Drop tabs on main thread (fast - Tab::drop just returns immediately)
4740        drop(tabs);
4741        log::info!(
4742            "Tabs dropped (+{:.1}ms)",
4743            t0.elapsed().as_secs_f64() * 1000.0
4744        );
4745
4746        // Fire-and-forget: stop session loggers on a background thread.
4747        // Each logger.stop() flushes buffered I/O which can block.
4748        if !session_loggers.is_empty() {
4749            let _ = std::thread::Builder::new()
4750                .name("logger-cleanup".into())
4751                .spawn(move || {
4752                    for logger_arc in session_loggers {
4753                        if let Some(ref mut logger) = *logger_arc.lock() {
4754                            let _ = logger.stop();
4755                        }
4756                    }
4757                });
4758        }
4759
4760        // Fire-and-forget: drop the cloned terminal Arcs on background threads.
4761        // When our clone is the last reference, TerminalManager::drop runs,
4762        // which triggers PtySession::drop (up to 2s reader thread wait).
4763        // By running these in parallel, all sessions clean up concurrently.
4764        // We intentionally do NOT join these threads — the process is exiting
4765        // and the OS will reclaim all resources.
4766        for (i, arc) in terminal_arcs.into_iter().enumerate() {
4767            let _ = std::thread::Builder::new()
4768                .name(format!("pty-cleanup-{}", i))
4769                .spawn(move || {
4770                    let t = std::time::Instant::now();
4771                    drop(arc);
4772                    log::info!(
4773                        "pty-cleanup-{} finished in {:.1}ms",
4774                        i,
4775                        t.elapsed().as_secs_f64() * 1000.0
4776                    );
4777                });
4778        }
4779
4780        log::info!(
4781            "Window shutdown complete ({} tabs, main thread blocked {:.1}ms)",
4782            tab_count,
4783            t0.elapsed().as_secs_f64() * 1000.0
4784        );
4785    }
4786}