boxmux_lib/
draw_loop.rs

1use crate::draw_utils::{draw_app, draw_muxbox};
2use crate::model::app::{save_muxbox_bounds_to_yaml, save_complete_state_to_yaml, save_active_layout_to_yaml, save_muxbox_content_to_yaml, save_muxbox_scroll_to_yaml};
3use crate::model::common::InputBounds;
4use crate::model::muxbox::Choice;
5use crate::thread_manager::Runnable;
6use crate::utils::{run_script_with_pty_and_redirect, should_use_pty_for_choice};
7use crate::{
8    apply_buffer, apply_buffer_if_changed, handle_keypress, run_script, AppContext, MuxBox,
9    ScreenBuffer,
10};
11use crate::{thread_manager::*, FieldUpdate};
12// use crossbeam_channel::Sender; // T311: Removed with ChoiceThreadManager
13use crossterm::{
14    terminal::{enable_raw_mode, EnterAlternateScreen},
15    ExecutableCommand,
16};
17use std::io::stdout;
18use std::io::Stdout;
19use std::sync::{mpsc, Mutex};
20
21use uuid::Uuid;
22
23// F0188: Drag state tracking for draggable scroll knobs
24#[derive(Debug, Clone)]
25struct DragState {
26    muxbox_id: String,
27    is_vertical: bool, // true for vertical scrollbar, false for horizontal
28    start_x: u16,
29    start_y: u16,
30    start_scroll_percentage: f64,
31}
32
33// F0189: MuxBox resize state tracking for draggable muxbox borders
34#[derive(Debug, Clone)]
35struct MuxBoxResizeState {
36    muxbox_id: String,
37    resize_edge: ResizeEdge,
38    start_x: u16,
39    start_y: u16,
40    original_bounds: InputBounds,
41}
42
43// F0191: MuxBox move state tracking for draggable muxbox titles/top borders
44#[derive(Debug, Clone)]
45struct MuxBoxMoveState {
46    muxbox_id: String,
47    start_x: u16,
48    start_y: u16,
49    original_bounds: InputBounds,
50}
51
52#[derive(Debug, Clone, PartialEq)]
53pub enum ResizeEdge {
54    BottomRight, // Only corner resize allowed
55}
56
57static DRAG_STATE: Mutex<Option<DragState>> = Mutex::new(None);
58static MUXBOX_RESIZE_STATE: Mutex<Option<MuxBoxResizeState>> = Mutex::new(None);
59static MUXBOX_MOVE_STATE: Mutex<Option<MuxBoxMoveState>> = Mutex::new(None);
60
61// F0189: Helper functions to detect muxbox border resize areas (corner-only)
62pub fn detect_resize_edge(muxbox: &MuxBox, click_x: u16, click_y: u16) -> Option<ResizeEdge> {
63    let bounds = muxbox.bounds();
64    let x = click_x as usize;
65    let y = click_y as usize;
66
67    // Check for corner resize (bottom-right only) with tolerance for easier clicking
68    // Allow clicking within 1 pixel of the exact corner to make it easier to grab
69    let corner_tolerance = 1;
70
71    // Standard detection zone - same for all panels including 100% width
72    if (x >= bounds.x2.saturating_sub(corner_tolerance) && x <= bounds.x2)
73        && (y >= bounds.y2.saturating_sub(corner_tolerance) && y <= bounds.y2)
74    {
75        return Some(ResizeEdge::BottomRight);
76    }
77
78    None
79}
80
81// F0191: Helper function to detect muxbox title/top border for movement
82pub fn detect_move_area(muxbox: &MuxBox, click_x: u16, click_y: u16) -> bool {
83    let bounds = muxbox.bounds();
84    let x = click_x as usize;
85    let y = click_y as usize;
86
87    // Check for title area or top border (y1 coordinate across muxbox width)
88    y == bounds.y1 && x >= bounds.x1 && x <= bounds.x2
89}
90
91pub fn calculate_new_bounds(
92    original_bounds: &InputBounds,
93    resize_edge: &ResizeEdge,
94    start_x: u16,
95    start_y: u16,
96    current_x: u16,
97    current_y: u16,
98    terminal_width: usize,
99    terminal_height: usize,
100) -> InputBounds {
101    let delta_x = (current_x as i32) - (start_x as i32);
102    let delta_y = (current_y as i32) - (start_y as i32);
103
104    let mut new_bounds = original_bounds.clone();
105
106    // F0197: Minimum resize constraints - prevent boxes smaller than 2x2 characters
107    let min_width_percent = (2.0 / terminal_width as f32) * 100.0;
108    let min_height_percent = (2.0 / terminal_height as f32) * 100.0;
109
110    match resize_edge {
111        ResizeEdge::BottomRight => {
112            // Update both x2 and y2 coordinates for corner resize
113            if let Ok(current_x2_percent) = new_bounds.x2.replace('%', "").parse::<f32>() {
114                if let Ok(current_x1_percent) = new_bounds.x1.replace('%', "").parse::<f32>() {
115                    let pixel_delta_x = delta_x as f32;
116                    let percent_delta_x = (pixel_delta_x / terminal_width as f32) * 100.0;
117                    let new_x2_percent =
118                        (current_x2_percent + percent_delta_x).max(10.0).min(100.0);
119
120                    // Enforce minimum width constraint
121                    let min_x2_for_width = current_x1_percent + min_width_percent;
122                    let constrained_x2 = new_x2_percent.max(min_x2_for_width);
123
124                    new_bounds.x2 = format!("{}%", constrained_x2.round() as i32);
125                }
126            }
127
128            // Also update y2 coordinate for corner resize
129            if let Ok(current_y2_percent) = new_bounds.y2.replace('%', "").parse::<f32>() {
130                if let Ok(current_y1_percent) = new_bounds.y1.replace('%', "").parse::<f32>() {
131                    let pixel_delta_y = delta_y as f32;
132                    let percent_delta_y = (pixel_delta_y / terminal_height as f32) * 100.0;
133                    let new_y2_percent =
134                        (current_y2_percent + percent_delta_y).max(10.0).min(100.0);
135
136                    // Enforce minimum height constraint
137                    let min_y2_for_height = current_y1_percent + min_height_percent;
138                    let constrained_y2 = new_y2_percent.max(min_y2_for_height);
139
140                    new_bounds.y2 = format!("{}%", constrained_y2.round() as i32);
141                }
142            }
143        }
144    }
145
146    new_bounds
147}
148
149// F0191: Calculate new muxbox position during drag move
150pub fn calculate_new_position(
151    original_bounds: &InputBounds,
152    start_x: u16,
153    start_y: u16,
154    current_x: u16,
155    current_y: u16,
156    terminal_width: usize,
157    terminal_height: usize,
158) -> InputBounds {
159    let delta_x = (current_x as i32) - (start_x as i32);
160    let delta_y = (current_y as i32) - (start_y as i32);
161
162    let mut new_bounds = original_bounds.clone();
163
164    // Convert pixel deltas to percentage deltas and update position
165    let pixel_delta_x = delta_x as f32;
166    let percent_delta_x = (pixel_delta_x / terminal_width as f32) * 100.0;
167
168    let pixel_delta_y = delta_y as f32;
169    let percent_delta_y = (pixel_delta_y / terminal_height as f32) * 100.0;
170
171    // Update x1 and x2 (maintain width)
172    if let (Ok(current_x1), Ok(current_x2)) = (
173        new_bounds.x1.replace('%', "").parse::<f32>(),
174        new_bounds.x2.replace('%', "").parse::<f32>(),
175    ) {
176        let new_x1 = (current_x1 + percent_delta_x).max(0.0).min(90.0);
177        let new_x2 = (current_x2 + percent_delta_x).max(10.0).min(100.0);
178
179        // Ensure we don't go beyond boundaries while maintaining muxbox width
180        if new_x2 <= 100.0 && new_x1 >= 0.0 {
181            new_bounds.x1 = format!("{}%", new_x1.round() as i32);
182            new_bounds.x2 = format!("{}%", new_x2.round() as i32);
183        }
184    }
185
186    // Update y1 and y2 (maintain height)
187    if let (Ok(current_y1), Ok(current_y2)) = (
188        new_bounds.y1.replace('%', "").parse::<f32>(),
189        new_bounds.y2.replace('%', "").parse::<f32>(),
190    ) {
191        let new_y1 = (current_y1 + percent_delta_y).max(0.0).min(90.0);
192        let new_y2 = (current_y2 + percent_delta_y).max(10.0).min(100.0);
193
194        // Ensure we don't go beyond boundaries while maintaining muxbox height
195        if new_y2 <= 100.0 && new_y1 >= 0.0 {
196            new_bounds.y1 = format!("{}%", new_y1.round() as i32);
197            new_bounds.y2 = format!("{}%", new_y2.round() as i32);
198        }
199    }
200
201    new_bounds
202}
203
204// F0188: Helper functions to determine if click is on scroll knob (not just track)
205fn is_on_vertical_knob(muxbox: &MuxBox, click_y: usize) -> bool {
206    let muxbox_bounds = muxbox.bounds();
207    let viewable_height = muxbox_bounds.height().saturating_sub(4);
208
209    // Get content dimensions to calculate knob position and size
210    let max_content_height = if let Some(content) = &muxbox.content {
211        let lines: Vec<&str> = content.split('\n').collect();
212        let mut total_height = lines.len();
213
214        // Add choices height if present
215        if let Some(choices) = &muxbox.choices {
216            total_height += choices.len();
217        }
218        total_height
219    } else if let Some(choices) = &muxbox.choices {
220        choices.len()
221    } else {
222        viewable_height // No scrolling needed
223    };
224
225    if max_content_height <= viewable_height {
226        return false; // No scrollbar needed
227    }
228
229    let track_height = viewable_height.saturating_sub(2);
230    if track_height == 0 {
231        return false;
232    }
233
234    // Calculate knob position and size (matching draw_utils.rs logic)
235    let content_ratio = viewable_height as f64 / max_content_height as f64;
236    let knob_size = std::cmp::max(1, (track_height as f64 * content_ratio).round() as usize);
237    let available_track = track_height.saturating_sub(knob_size);
238
239    let vertical_scroll = muxbox.vertical_scroll.unwrap_or(0.0);
240    let knob_position = if available_track > 0 {
241        ((vertical_scroll / 100.0) * available_track as f64).round() as usize
242    } else {
243        0
244    };
245
246    // Check if click is within knob bounds
247    let knob_start_y = muxbox_bounds.top() + 1 + knob_position;
248    let knob_end_y = knob_start_y + knob_size;
249
250    click_y >= knob_start_y && click_y < knob_end_y
251}
252
253fn is_on_horizontal_knob(muxbox: &MuxBox, click_x: usize) -> bool {
254    let muxbox_bounds = muxbox.bounds();
255    let viewable_width = muxbox_bounds.width().saturating_sub(4);
256
257    // Get content width to calculate knob position and size
258    let max_content_width = if let Some(content) = &muxbox.content {
259        let lines: Vec<&str> = content.split('\n').collect();
260        lines.iter().map(|line| line.len()).max().unwrap_or(0)
261    } else if let Some(choices) = &muxbox.choices {
262        choices
263            .iter()
264            .map(|choice| choice.content.as_ref().map(|c| c.len()).unwrap_or(0))
265            .max()
266            .unwrap_or(0)
267    } else {
268        viewable_width // No scrolling needed
269    };
270
271    if max_content_width <= viewable_width {
272        return false; // No scrollbar needed
273    }
274
275    let track_width = viewable_width.saturating_sub(2);
276    if track_width == 0 {
277        return false;
278    }
279
280    // Calculate knob position and size (matching draw_utils.rs logic)
281    let content_ratio = viewable_width as f64 / max_content_width as f64;
282    let knob_size = std::cmp::max(1, (track_width as f64 * content_ratio).round() as usize);
283    let available_track = track_width.saturating_sub(knob_size);
284
285    let horizontal_scroll = muxbox.horizontal_scroll.unwrap_or(0.0);
286    let knob_position = if available_track > 0 {
287        ((horizontal_scroll / 100.0) * available_track as f64).round() as usize
288    } else {
289        0
290    };
291
292    // Check if click is within knob bounds
293    let knob_start_x = muxbox_bounds.left() + 1 + knob_position;
294    let knob_end_x = knob_start_x + knob_size;
295
296    click_x >= knob_start_x && click_x < knob_end_x
297}
298
299lazy_static! {
300    static ref GLOBAL_SCREEN: Mutex<Option<Stdout>> = Mutex::new(None);
301    static ref GLOBAL_BUFFER: Mutex<Option<ScreenBuffer>> = Mutex::new(None);
302}
303
304create_runnable!(
305    DrawLoop,
306    |_inner: &mut RunnableImpl, app_context: AppContext, _messages: Vec<Message>| -> bool {
307        let mut global_screen = GLOBAL_SCREEN.lock().unwrap();
308        let mut global_buffer = GLOBAL_BUFFER.lock().unwrap();
309        let mut app_context_unwrapped = app_context.clone();
310        let (adjusted_bounds, app_graph) = app_context_unwrapped
311            .app
312            .get_adjusted_bounds_and_app_graph(Some(true));
313
314        let is_first_render = global_screen.is_none();
315        if is_first_render {
316            let mut stdout = stdout();
317            enable_raw_mode().unwrap();
318            stdout.execute(EnterAlternateScreen).unwrap();
319            *global_screen = Some(stdout);
320            *global_buffer = Some(ScreenBuffer::new());
321        }
322
323        if let (Some(ref mut screen), Some(ref mut buffer)) =
324            (&mut *global_screen, &mut *global_buffer)
325        {
326            let mut new_buffer = ScreenBuffer::new();
327            draw_app(
328                &app_context_unwrapped,
329                &app_graph,
330                &adjusted_bounds,
331                &mut new_buffer,
332            );
333            if is_first_render {
334                // Force full render on first run to ensure everything is drawn
335                apply_buffer(&mut new_buffer, screen);
336            } else {
337                apply_buffer_if_changed(buffer, &new_buffer, screen);
338            }
339            *buffer = new_buffer;
340        }
341
342        true
343    },
344    |inner: &mut RunnableImpl,
345     app_context: AppContext,
346     messages: Vec<Message>|
347     -> (bool, AppContext) {
348        let mut global_screen = GLOBAL_SCREEN.lock().unwrap();
349        let mut global_buffer = GLOBAL_BUFFER.lock().unwrap();
350        let mut should_continue = true;
351
352        if let (Some(ref mut screen), Some(ref mut buffer)) =
353            (&mut *global_screen, &mut *global_buffer)
354        {
355            let mut new_buffer;
356            let mut app_context_unwrapped = app_context.clone();
357            let (adjusted_bounds, app_graph) = app_context_unwrapped
358                .app
359                .get_adjusted_bounds_and_app_graph(Some(true));
360            // T311: choice_ids_now_waiting removed - no longer needed with unified threading
361
362            if !messages.is_empty() {
363                log::info!("DrawLoop processing {} messages", messages.len());
364                for msg in &messages {
365                    match msg {
366                        Message::ChoiceExecutionComplete(choice_id, muxbox_id, _) => {
367                            log::info!(
368                                "About to process ChoiceExecutionComplete: {} -> {}",
369                                choice_id,
370                                muxbox_id
371                            );
372                        }
373                        _ => {}
374                    }
375                }
376            }
377
378            for message in &messages {
379                match message {
380                    Message::MuxBoxEventRefresh(_) => {
381                        log::trace!("MuxBoxEventRefresh");
382                    }
383                    Message::Exit => should_continue = false,
384                    Message::Terminate => should_continue = false,
385                    Message::NextMuxBox() => {
386                        let active_layout = app_context_unwrapped
387                            .app
388                            .get_active_layout_mut()
389                            .expect("No active layout found!");
390
391                        // First, collect the IDs of currently selected muxboxes before changing the selection.
392                        let unselected_muxbox_ids: Vec<String> = active_layout
393                            .get_selected_muxboxes()
394                            .iter()
395                            .map(|muxbox| muxbox.id.clone())
396                            .collect();
397
398                        // Now perform the mutation that changes the muxbox selection.
399                        active_layout.select_next_muxbox();
400
401                        // After mutation, get the newly selected muxboxes' IDs.
402                        let selected_muxbox_ids: Vec<String> = active_layout
403                            .get_selected_muxboxes()
404                            .iter()
405                            .map(|muxbox| muxbox.id.clone())
406                            .collect();
407
408                        // Update the application context and issue redraw commands based on the collected IDs.
409                        inner.update_app_context(app_context_unwrapped.clone());
410                        for muxbox_id in unselected_muxbox_ids {
411                            inner.send_message(Message::RedrawMuxBox(muxbox_id));
412                        }
413                        for muxbox_id in selected_muxbox_ids {
414                            inner.send_message(Message::RedrawMuxBox(muxbox_id));
415                        }
416                    }
417                    Message::PreviousMuxBox() => {
418                        let active_layout = app_context_unwrapped
419                            .app
420                            .get_active_layout_mut()
421                            .expect("No active layout found!");
422
423                        // First, collect the IDs of currently selected muxboxes before changing the selection.
424                        let unselected_muxbox_ids: Vec<String> = active_layout
425                            .get_selected_muxboxes()
426                            .iter()
427                            .map(|muxbox| muxbox.id.clone())
428                            .collect();
429
430                        // Now perform the mutation that changes the muxbox selection.
431                        active_layout.select_previous_muxbox();
432
433                        // After mutation, get the newly selected muxboxes' IDs.
434                        let selected_muxbox_ids: Vec<String> = active_layout
435                            .get_selected_muxboxes()
436                            .iter()
437                            .map(|muxbox| muxbox.id.clone())
438                            .collect();
439
440                        // Update the application context and issue redraw commands based on the collected IDs.
441                        inner.update_app_context(app_context_unwrapped.clone());
442                        for muxbox_id in unselected_muxbox_ids {
443                            inner.send_message(Message::RedrawMuxBox(muxbox_id));
444                        }
445                        for muxbox_id in selected_muxbox_ids {
446                            inner.send_message(Message::RedrawMuxBox(muxbox_id));
447                        }
448                    }
449                    Message::ScrollMuxBoxDown() => {
450                        let selected_muxboxes = app_context_unwrapped
451                            .app
452                            .get_active_layout()
453                            .unwrap()
454                            .get_selected_muxboxes();
455                        if !selected_muxboxes.is_empty() {
456                            let selected_id = selected_muxboxes.first().unwrap().id.clone();
457                            let muxbox =
458                                app_context_unwrapped.app.get_muxbox_by_id_mut(&selected_id);
459                            if let Some(found_muxbox) = muxbox {
460                                if found_muxbox.choices.is_some() {
461                                    //select first or next choice
462                                    let choices = found_muxbox.choices.as_mut().unwrap();
463                                    let selected_choice = choices.iter().position(|c| c.selected);
464                                    let selected_choice_unwrapped =
465                                        selected_choice.unwrap_or_default();
466                                    let new_selected_choice =
467                                        if selected_choice_unwrapped + 1 < choices.len() {
468                                            selected_choice_unwrapped + 1
469                                        } else {
470                                            0
471                                        };
472                                    for (i, choice) in choices.iter_mut().enumerate() {
473                                        choice.selected = i == new_selected_choice;
474                                    }
475
476                                    // Auto-scroll to keep selected choice visible
477                                    auto_scroll_to_selected_choice(found_muxbox, new_selected_choice);
478                                } else {
479                                    found_muxbox.scroll_down(Some(1.0));
480                                }
481
482                                inner.update_app_context(app_context_unwrapped.clone());
483                                inner.send_message(Message::RedrawMuxBox(selected_id));
484                            }
485                        }
486                    }
487                    Message::ScrollMuxBoxUp() => {
488                        let selected_muxboxes = app_context_unwrapped
489                            .app
490                            .get_active_layout()
491                            .unwrap()
492                            .get_selected_muxboxes();
493                        if !selected_muxboxes.is_empty() {
494                            let selected_id = selected_muxboxes.first().unwrap().id.clone();
495                            let muxbox =
496                                app_context_unwrapped.app.get_muxbox_by_id_mut(&selected_id);
497                            if let Some(found_muxbox) = muxbox {
498                                if found_muxbox.choices.is_some() {
499                                    //select first or next choice
500                                    let choices = found_muxbox.choices.as_mut().unwrap();
501                                    let selected_choice = choices.iter().position(|c| c.selected);
502                                    let selected_choice_unwrapped =
503                                        selected_choice.unwrap_or_default();
504                                    let new_selected_choice = if selected_choice_unwrapped > 0 {
505                                        selected_choice_unwrapped - 1
506                                    } else {
507                                        choices.len() - 1
508                                    };
509                                    for (i, choice) in choices.iter_mut().enumerate() {
510                                        choice.selected = i == new_selected_choice;
511                                    }
512
513                                    // Auto-scroll to keep selected choice visible
514                                    auto_scroll_to_selected_choice(found_muxbox, new_selected_choice);
515                                } else {
516                                    found_muxbox.scroll_up(Some(1.0));
517                                }
518                                inner.update_app_context(app_context_unwrapped.clone());
519                                inner.send_message(Message::RedrawMuxBox(selected_id));
520                            }
521                        }
522                    }
523                    Message::ScrollMuxBoxLeft() => {
524                        let selected_muxboxes = app_context_unwrapped
525                            .app
526                            .get_active_layout()
527                            .unwrap()
528                            .get_selected_muxboxes();
529                        if !selected_muxboxes.is_empty() {
530                            let selected_id = selected_muxboxes.first().unwrap().id.clone();
531                            let muxbox =
532                                app_context_unwrapped.app.get_muxbox_by_id_mut(&selected_id);
533                            if let Some(found_muxbox) = muxbox {
534                                found_muxbox.scroll_left(Some(1.0));
535                                inner.update_app_context(app_context_unwrapped.clone());
536                                inner.send_message(Message::RedrawMuxBox(selected_id));
537                            }
538                        }
539                    }
540                    Message::ScrollMuxBoxRight() => {
541                        let selected_muxboxes = app_context_unwrapped
542                            .app
543                            .get_active_layout()
544                            .unwrap()
545                            .get_selected_muxboxes();
546                        if !selected_muxboxes.is_empty() {
547                            let selected_id = selected_muxboxes.first().unwrap().id.clone();
548                            let muxbox =
549                                app_context_unwrapped.app.get_muxbox_by_id_mut(&selected_id);
550                            if let Some(found_muxbox) = muxbox {
551                                found_muxbox.scroll_right(Some(1.0));
552                                inner.update_app_context(app_context_unwrapped.clone());
553                                inner.send_message(Message::RedrawMuxBox(selected_id));
554                            }
555                        }
556                    }
557                    Message::ScrollMuxBoxPageUp() => {
558                        let selected_muxboxes = app_context_unwrapped
559                            .app
560                            .get_active_layout()
561                            .unwrap()
562                            .get_selected_muxboxes();
563                        if !selected_muxboxes.is_empty() {
564                            let selected_id = selected_muxboxes.first().unwrap().id.clone();
565                            let muxbox =
566                                app_context_unwrapped.app.get_muxbox_by_id_mut(&selected_id);
567                            if let Some(found_muxbox) = muxbox {
568                                // Page up scrolls by larger amount (10 units for page-based scrolling)
569                                found_muxbox.scroll_up(Some(10.0));
570                                inner.update_app_context(app_context_unwrapped.clone());
571                                inner.send_message(Message::RedrawMuxBox(selected_id));
572                            }
573                        }
574                    }
575                    Message::ScrollMuxBoxPageDown() => {
576                        let selected_muxboxes = app_context_unwrapped
577                            .app
578                            .get_active_layout()
579                            .unwrap()
580                            .get_selected_muxboxes();
581                        if !selected_muxboxes.is_empty() {
582                            let selected_id = selected_muxboxes.first().unwrap().id.clone();
583                            let muxbox =
584                                app_context_unwrapped.app.get_muxbox_by_id_mut(&selected_id);
585                            if let Some(found_muxbox) = muxbox {
586                                // Page down scrolls by larger amount (10 units for page-based scrolling)
587                                found_muxbox.scroll_down(Some(10.0));
588                                inner.update_app_context(app_context_unwrapped.clone());
589                                inner.send_message(Message::RedrawMuxBox(selected_id));
590                            }
591                        }
592                    }
593                    Message::ScrollMuxBoxPageLeft() => {
594                        let selected_muxboxes = app_context_unwrapped
595                            .app
596                            .get_active_layout()
597                            .unwrap()
598                            .get_selected_muxboxes();
599                        if !selected_muxboxes.is_empty() {
600                            let selected_id = selected_muxboxes.first().unwrap().id.clone();
601                            let muxbox =
602                                app_context_unwrapped.app.get_muxbox_by_id_mut(&selected_id);
603                            if let Some(found_muxbox) = muxbox {
604                                // Page left scrolls by larger amount (10 units for page-based scrolling)
605                                found_muxbox.scroll_left(Some(10.0));
606                                inner.update_app_context(app_context_unwrapped.clone());
607                                inner.send_message(Message::RedrawMuxBox(selected_id));
608                            }
609                        }
610                    }
611                    Message::ScrollMuxBoxPageRight() => {
612                        let selected_muxboxes = app_context_unwrapped
613                            .app
614                            .get_active_layout()
615                            .unwrap()
616                            .get_selected_muxboxes();
617                        if !selected_muxboxes.is_empty() {
618                            let selected_id = selected_muxboxes.first().unwrap().id.clone();
619                            let muxbox =
620                                app_context_unwrapped.app.get_muxbox_by_id_mut(&selected_id);
621                            if let Some(found_muxbox) = muxbox {
622                                // Page right scrolls by larger amount (10 units for page-based scrolling)
623                                found_muxbox.scroll_right(Some(10.0));
624                                inner.update_app_context(app_context_unwrapped.clone());
625                                inner.send_message(Message::RedrawMuxBox(selected_id));
626                            }
627                        }
628                    }
629                    Message::ScrollMuxBoxToBeginning() => {
630                        // Home key: scroll to beginning horizontally (horizontal_scroll = 0)
631                        let selected_muxboxes = app_context_unwrapped
632                            .app
633                            .get_active_layout()
634                            .unwrap()
635                            .get_selected_muxboxes();
636                        if !selected_muxboxes.is_empty() {
637                            let selected_id = selected_muxboxes.first().unwrap().id.clone();
638                            let muxbox =
639                                app_context_unwrapped.app.get_muxbox_by_id_mut(&selected_id);
640                            if let Some(found_muxbox) = muxbox {
641                                found_muxbox.horizontal_scroll = Some(0.0);
642                                
643                                // F0200: Save scroll position to YAML
644                                inner.send_message(Message::SaveMuxBoxScroll(
645                                    found_muxbox.id.clone(),
646                                    0,
647                                    (found_muxbox.vertical_scroll.unwrap_or(0.0) * 100.0) as usize,
648                                ));
649                                inner.update_app_context(app_context_unwrapped.clone());
650                                inner.send_message(Message::RedrawMuxBox(selected_id));
651                            }
652                        }
653                    }
654                    Message::ScrollMuxBoxToEnd() => {
655                        // End key: scroll to end horizontally (horizontal_scroll = 100)
656                        let selected_muxboxes = app_context_unwrapped
657                            .app
658                            .get_active_layout()
659                            .unwrap()
660                            .get_selected_muxboxes();
661                        if !selected_muxboxes.is_empty() {
662                            let selected_id = selected_muxboxes.first().unwrap().id.clone();
663                            let muxbox =
664                                app_context_unwrapped.app.get_muxbox_by_id_mut(&selected_id);
665                            if let Some(found_muxbox) = muxbox {
666                                found_muxbox.horizontal_scroll = Some(100.0);
667                                
668                                // F0200: Save scroll position to YAML
669                                inner.send_message(Message::SaveMuxBoxScroll(
670                                    found_muxbox.id.clone(),
671                                    100,
672                                    (found_muxbox.vertical_scroll.unwrap_or(0.0) * 100.0) as usize,
673                                ));
674                                inner.update_app_context(app_context_unwrapped.clone());
675                                inner.send_message(Message::RedrawMuxBox(selected_id));
676                            }
677                        }
678                    }
679                    Message::ScrollMuxBoxToTop() => {
680                        // Ctrl+Home: scroll to top vertically (vertical_scroll = 0)
681                        let selected_muxboxes = app_context_unwrapped
682                            .app
683                            .get_active_layout()
684                            .unwrap()
685                            .get_selected_muxboxes();
686                        if !selected_muxboxes.is_empty() {
687                            let selected_id = selected_muxboxes.first().unwrap().id.clone();
688                            let muxbox =
689                                app_context_unwrapped.app.get_muxbox_by_id_mut(&selected_id);
690                            if let Some(found_muxbox) = muxbox {
691                                found_muxbox.vertical_scroll = Some(0.0);
692                                
693                                // F0200: Save scroll position to YAML
694                                inner.send_message(Message::SaveMuxBoxScroll(
695                                    found_muxbox.id.clone(),
696                                    (found_muxbox.horizontal_scroll.unwrap_or(0.0) * 100.0) as usize,
697                                    0,
698                                ));
699                                inner.update_app_context(app_context_unwrapped.clone());
700                                inner.send_message(Message::RedrawMuxBox(selected_id));
701                            }
702                        }
703                    }
704                    Message::ScrollMuxBoxToBottom() => {
705                        // Ctrl+End: scroll to bottom vertically (vertical_scroll = 100)
706                        let selected_muxboxes = app_context_unwrapped
707                            .app
708                            .get_active_layout()
709                            .unwrap()
710                            .get_selected_muxboxes();
711                        if !selected_muxboxes.is_empty() {
712                            let selected_id = selected_muxboxes.first().unwrap().id.clone();
713                            let muxbox =
714                                app_context_unwrapped.app.get_muxbox_by_id_mut(&selected_id);
715                            if let Some(found_muxbox) = muxbox {
716                                found_muxbox.vertical_scroll = Some(100.0);
717                                
718                                // F0200: Save scroll position to YAML
719                                inner.send_message(Message::SaveMuxBoxScroll(
720                                    found_muxbox.id.clone(),
721                                    (found_muxbox.horizontal_scroll.unwrap_or(0.0) * 100.0) as usize,
722                                    100,
723                                ));
724                                inner.update_app_context(app_context_unwrapped.clone());
725                                inner.send_message(Message::RedrawMuxBox(selected_id));
726                            }
727                        }
728                    }
729                    Message::CopyFocusedMuxBoxContent() => {
730                        let selected_muxboxes = app_context_unwrapped
731                            .app
732                            .get_active_layout()
733                            .unwrap()
734                            .get_selected_muxboxes();
735                        if !selected_muxboxes.is_empty() {
736                            let selected_id = selected_muxboxes.first().unwrap().id.clone();
737                            let muxbox = app_context_unwrapped.app.get_muxbox_by_id(&selected_id);
738                            if let Some(found_muxbox) = muxbox {
739                                // Get muxbox content to copy
740                                let content_to_copy =
741                                    get_muxbox_content_for_clipboard(found_muxbox);
742
743                                // Copy to clipboard
744                                if copy_to_clipboard(&content_to_copy).is_ok() {
745                                    // Trigger visual flash for the muxbox
746                                    trigger_muxbox_flash(&selected_id);
747                                    inner.send_message(Message::RedrawMuxBox(selected_id));
748                                }
749                            }
750                        }
751                    }
752                    Message::RedrawMuxBox(muxbox_id) => {
753                        if let Some(mut found_muxbox) = app_context_unwrapped
754                            .app
755                            .get_muxbox_by_id_mut(muxbox_id)
756                            .cloned()
757                        {
758                            new_buffer = buffer.clone();
759
760                            // Clone the parent layout to avoid mutable borrow conflicts
761                            if let Some(parent_layout) =
762                                found_muxbox.get_parent_layout_clone(&mut app_context_unwrapped)
763                            {
764                                draw_muxbox(
765                                    &app_context_unwrapped,
766                                    &app_graph,
767                                    &adjusted_bounds,
768                                    &parent_layout,
769                                    &mut found_muxbox,
770                                    &mut new_buffer,
771                                );
772                                apply_buffer_if_changed(buffer, &new_buffer, screen);
773                                *buffer = new_buffer;
774                            }
775                        }
776                    }
777                    Message::RedrawApp | Message::Resize => {
778                        screen
779                            .execute(crossterm::terminal::Clear(
780                                crossterm::terminal::ClearType::All,
781                            ))
782                            .unwrap();
783                        let mut new_buffer = ScreenBuffer::new();
784                        draw_app(
785                            &app_context_unwrapped,
786                            &app_graph,
787                            &adjusted_bounds,
788                            &mut new_buffer,
789                        );
790                        apply_buffer(&mut new_buffer, screen);
791                        *buffer = new_buffer;
792                    }
793                    Message::RedrawAppDiff => {
794                        // Redraw entire app with diff-based rendering (no screen clear)
795                        let mut new_buffer = ScreenBuffer::new();
796                        draw_app(
797                            &app_context_unwrapped,
798                            &app_graph,
799                            &adjusted_bounds,
800                            &mut new_buffer,
801                        );
802                        apply_buffer_if_changed(buffer, &new_buffer, screen);
803                        *buffer = new_buffer;
804                    }
805                    Message::MuxBoxOutputUpdate(muxbox_id, success, output) => {
806                        log::info!("RECEIVED MuxBoxOutputUpdate for muxbox: {}, success: {}, output_len: {}, preview: {}", 
807                                   muxbox_id, success, output.len(), output.chars().take(50).collect::<String>());
808                        let mut app_context_unwrapped_cloned = app_context_unwrapped.clone();
809                        // For PTY streaming output, we need to use a special update method
810                        // that doesn't add timestamp formatting. The presence of a newline
811                        // at the end of the output indicates it's PTY streaming data.
812                        let is_pty_streaming = output.ends_with('\n');
813
814                        if is_pty_streaming {
815                            // Use streaming update for PTY output
816                            let content_for_save = {
817                                let target_muxbox = app_context_unwrapped_cloned
818                                    .app
819                                    .get_muxbox_by_id_mut(muxbox_id)
820                                    .unwrap();
821                                target_muxbox.update_streaming_content(output, *success);
822                                target_muxbox.content.clone()
823                            };
824                            inner.update_app_context(app_context_unwrapped_cloned.clone());
825                            inner.send_message(Message::RedrawMuxBox(muxbox_id.to_string()));
826                            
827                            // F0200: Save PTY streaming content to YAML
828                            if let Some(updated_content) = content_for_save {
829                                inner.send_message(Message::SaveMuxBoxContent(
830                                    muxbox_id.clone(),
831                                    updated_content,
832                                ));
833                            }
834                        } else {
835                            // Use regular update for non-PTY output
836                            let muxbox = app_context_unwrapped
837                                .app
838                                .get_muxbox_by_id(muxbox_id)
839                                .unwrap();
840                            
841                            // Store content before update for YAML persistence
842                            let old_content = muxbox.content.clone();
843                            
844                            update_muxbox_content(
845                                inner,
846                                &mut app_context_unwrapped_cloned,
847                                muxbox_id,
848                                *success,
849                                muxbox.append_output.unwrap_or(false),
850                                output,
851                            );
852                            
853                            // F0200: Save updated content to YAML if it changed
854                            if let Some(updated_muxbox) = app_context_unwrapped_cloned.app.get_muxbox_by_id(muxbox_id) {
855                                if updated_muxbox.content != old_content {
856                                    if let Some(new_content) = &updated_muxbox.content {
857                                        inner.send_message(Message::SaveMuxBoxContent(
858                                            muxbox_id.clone(),
859                                            new_content.clone(),
860                                        ));
861                                    }
862                                }
863                            }
864                        }
865                    }
866                    // ExternalMessage handling is now done by RSJanusComms library
867                    // Messages are converted to appropriate internal messages by the socket handler
868                    Message::ExternalMessage(_) => {
869                        // This should no longer be used - socket handler converts messages directly
870                        log::warn!("Received deprecated ExternalMessage - should be converted by socket handler");
871                    }
872                    Message::ExecuteHotKeyChoice(choice_id) => {
873                        log::info!("=== EXECUTING HOT KEY CHOICE: {} ===", choice_id);
874
875                        let active_layout = app_context_unwrapped.app.get_active_layout().unwrap();
876
877                        // Find the choice by ID in any muxbox
878                        log::info!("Searching for choice {} in active layout", choice_id);
879                        if let Some(choice_muxbox) =
880                            active_layout.find_muxbox_with_choice(&choice_id)
881                        {
882                            log::info!("Found choice in muxbox: {}", choice_muxbox.id);
883
884                            if let Some(choices) = &choice_muxbox.choices {
885                                if let Some(choice) = choices.iter().find(|c| c.id == *choice_id) {
886                                    // T315: Unified choice execution - thread field no longer affects execution path
887                                    log::info!("Executing choice config - pty: {}, redirect: {:?}, script_lines: {}", 
888                                        choice.pty.unwrap_or(false),
889                                        choice.redirect_output,
890                                        choice.script.as_ref().map(|s| s.len()).unwrap_or(0)
891                                    );
892
893                                    if let Some(script) = &choice.script {
894                                        let libs = app_context_unwrapped.app.libs.clone();
895                                        let use_pty = should_use_pty_for_choice(choice);
896                                        let pty_manager =
897                                            app_context_unwrapped.pty_manager.as_ref();
898                                        let message_sender = Some((
899                                            inner.get_message_sender().as_ref().unwrap().clone(),
900                                            inner.get_uuid(),
901                                        ));
902
903                                        log::info!("Unified execution - use_pty: {}, has_manager: {}, redirect: {:?}", 
904                                            use_pty, pty_manager.is_some(), choice.redirect_output);
905
906                                        let result = run_script_with_pty_and_redirect(
907                                            libs,
908                                            script,
909                                            use_pty,
910                                            pty_manager.map(|arc| arc.as_ref()),
911                                            Some(choice.id.clone()),
912                                            message_sender,
913                                            choice.redirect_output.clone(),
914                                        );
915
916                                        // Send completion message via unified system
917                                        inner.send_message(Message::ChoiceExecutionComplete(
918                                            choice_id.clone(),
919                                            choice_muxbox.id.clone(),
920                                            result.map_err(|e| e.to_string()),
921                                        ));
922                                    }
923                                } else {
924                                    log::warn!("Choice {} found in muxbox {} but no matching choice in choices list", choice_id, choice_muxbox.id);
925                                }
926                            } else {
927                                log::warn!("MuxBox {} has no choices list", choice_muxbox.id);
928                            }
929                        } else {
930                            log::error!(
931                                "Choice {} not found in any muxbox of active layout",
932                                choice_id
933                            );
934                        }
935                    }
936                    Message::KeyPress(pressed_key) => {
937                        let mut app_context_for_keypress = app_context_unwrapped.clone();
938                        let active_layout = app_context_unwrapped.app.get_active_layout().unwrap();
939
940                        let selected_muxboxes: Vec<&MuxBox> = active_layout.get_selected_muxboxes();
941
942                        let selected_muxboxes_with_keypress_events: Vec<&MuxBox> =
943                            selected_muxboxes
944                                .clone()
945                                .into_iter()
946                                .filter(|p| p.on_keypress.is_some())
947                                .filter(|p| p.choices.is_none())
948                                .collect();
949
950                        let libs = app_context_unwrapped.app.libs.clone();
951
952                        if pressed_key == "Enter" {
953                            let selected_muxboxes_with_choices: Vec<&MuxBox> = selected_muxboxes
954                                .into_iter()
955                                .filter(|p| p.choices.is_some())
956                                .collect();
957                            for muxbox in selected_muxboxes_with_choices {
958                                // First, extract choice information before any mutable operations
959                                let (selected_choice_data, choice_needs_execution) = {
960                                    let muxbox_ref = app_context_for_keypress
961                                        .app
962                                        .get_muxbox_by_id(&muxbox.id)
963                                        .unwrap();
964                                    if let Some(ref choices) = muxbox_ref.choices {
965                                        if let Some(selected_choice) =
966                                            choices.iter().find(|c| c.selected)
967                                        {
968                                            let choice_data = (
969                                                selected_choice.id.clone(),
970                                                selected_choice.script.clone(),
971                                                selected_choice.pty.unwrap_or(false),
972                                                selected_choice.thread.unwrap_or(false),
973                                                selected_choice.redirect_output.clone(),
974                                                selected_choice.append_output.unwrap_or(false),
975                                                muxbox.id.clone(),
976                                            );
977                                            (Some(choice_data), selected_choice.script.is_some())
978                                        } else {
979                                            (None, false)
980                                        }
981                                    } else {
982                                        (None, false)
983                                    }
984                                };
985
986                                if let Some((
987                                    choice_id,
988                                    script_opt,
989                                    use_pty,
990                                    use_thread,
991                                    redirect_output,
992                                    append_output,
993                                    muxbox_id,
994                                )) = selected_choice_data
995                                {
996                                    if choice_needs_execution {
997                                        log::info!(
998                                            "=== ENTER KEY CHOICE EXECUTION: {} (muxbox: {}) ===",
999                                            choice_id,
1000                                            muxbox_id
1001                                        );
1002                                        log::info!("Enter choice config - pty: {}, thread: {}, redirect: {:?}", 
1003                                            use_pty, use_thread, redirect_output
1004                                        );
1005
1006                                        if let Some(script) = script_opt {
1007                                            let libs_clone = libs.clone();
1008
1009                                            // T312: Execute choice using unified threading system - proper architecture
1010                                            log::info!("Enter key requesting ThreadManager to execute choice {} (pty: {})", choice_id, use_pty);
1011
1012                                            // Set choice to waiting state before execution
1013                                            if let Some(muxbox_mut) = app_context_for_keypress
1014                                                .app
1015                                                .get_muxbox_by_id_mut(&muxbox_id)
1016                                            {
1017                                                if let Some(ref mut choices) = muxbox_mut.choices {
1018                                                    if let Some(choice) = choices
1019                                                        .iter_mut()
1020                                                        .find(|c| c.id == choice_id)
1021                                                    {
1022                                                        choice.waiting = true;
1023                                                    }
1024                                                }
1025                                            }
1026
1027                                            // Create the choice object for execution
1028                                            let choice_for_execution = Choice {
1029                                                id: choice_id.clone(),
1030                                                content: Some("".to_string()), // Not needed for execution
1031                                                selected: false, // Not needed for execution
1032                                                script: Some(script.clone()),
1033                                                pty: Some(use_pty),
1034                                                thread: Some(use_thread),
1035                                                redirect_output: redirect_output.clone(),
1036                                                append_output: Some(append_output),
1037                                                waiting: true,
1038                                            };
1039
1040                                            // Send ExecuteChoice message to ThreadManager (proper architecture)
1041                                            log::info!("Sending ExecuteChoice message for choice {} (pty: {}, thread: {})", 
1042                                            choice_id, use_pty, use_thread);
1043                                            inner.send_message(Message::ExecuteChoice(
1044                                                choice_for_execution,
1045                                                muxbox_id.clone(),
1046                                                libs_clone,
1047                                            ));
1048
1049                                            // Update the app context to persist the waiting state change
1050                                            inner.update_app_context(
1051                                                app_context_for_keypress.clone(),
1052                                            );
1053
1054                                            log::trace!(
1055                                                "ExecuteChoice message sent for choice {}",
1056                                                choice_id
1057                                            );
1058                                        }
1059                                    }
1060                                }
1061                            }
1062                        }
1063
1064                        for muxbox in selected_muxboxes_with_keypress_events {
1065                            let actions =
1066                                handle_keypress(pressed_key, &muxbox.on_keypress.clone().unwrap());
1067                            if actions.is_none() {
1068                                if let Some(actions_unwrapped) = actions {
1069                                    let libs = app_context_unwrapped.app.libs.clone();
1070
1071                                    match run_script(libs, &actions_unwrapped) {
1072                                        Ok(output) => {
1073                                            if muxbox.redirect_output.is_some() {
1074                                                update_muxbox_content(
1075                                                    inner,
1076                                                    &mut app_context_for_keypress,
1077                                                    muxbox.redirect_output.as_ref().unwrap(),
1078                                                    true,
1079                                                    muxbox.append_output.unwrap_or(false),
1080                                                    &output,
1081                                                )
1082                                            } else {
1083                                                update_muxbox_content(
1084                                                    inner,
1085                                                    &mut app_context_for_keypress,
1086                                                    &muxbox.id,
1087                                                    true,
1088                                                    muxbox.append_output.unwrap_or(false),
1089                                                    &output,
1090                                                )
1091                                            }
1092                                        }
1093                                        Err(e) => {
1094                                            if muxbox.redirect_output.is_some() {
1095                                                update_muxbox_content(
1096                                                    inner,
1097                                                    &mut app_context_for_keypress,
1098                                                    muxbox.redirect_output.as_ref().unwrap(),
1099                                                    false,
1100                                                    muxbox.append_output.unwrap_or(false),
1101                                                    e.to_string().as_str(),
1102                                                )
1103                                            } else {
1104                                                update_muxbox_content(
1105                                                    inner,
1106                                                    &mut app_context_for_keypress,
1107                                                    &muxbox.id,
1108                                                    false,
1109                                                    muxbox.append_output.unwrap_or(false),
1110                                                    e.to_string().as_str(),
1111                                                )
1112                                            }
1113                                        }
1114                                    }
1115                                }
1116                            }
1117                        }
1118                    }
1119                    Message::PTYInput(muxbox_id, input) => {
1120                        log::trace!("PTY input for muxbox {}: {}", muxbox_id, input);
1121
1122                        // Find the target muxbox to verify it exists and has PTY enabled
1123                        if let Some(muxbox) = app_context_unwrapped.app.get_muxbox_by_id(muxbox_id)
1124                        {
1125                            if muxbox.pty.unwrap_or(false) {
1126                                log::debug!(
1127                                    "Routing input to PTY muxbox {}: {:?}",
1128                                    muxbox_id,
1129                                    input.chars().collect::<Vec<_>>()
1130                                );
1131
1132                                // TODO: Write input to PTY process when PTY manager is thread-safe
1133                                // For now, log the successful routing detection
1134                                log::info!(
1135                                    "PTY input ready for routing to muxbox {}: {} chars",
1136                                    muxbox_id,
1137                                    input.len()
1138                                );
1139                            } else {
1140                                log::warn!(
1141                                    "MuxBox {} received PTY input but pty field is false",
1142                                    muxbox_id
1143                                );
1144                            }
1145                        } else {
1146                            log::error!(
1147                                "PTY input received for non-existent muxbox: {}",
1148                                muxbox_id
1149                            );
1150                        }
1151                    }
1152                    Message::MouseClick(x, y) => {
1153                        log::trace!("Mouse click at ({}, {})", x, y);
1154                        let mut app_context_for_click = app_context_unwrapped.clone();
1155                        let active_layout = app_context_unwrapped.app.get_active_layout().unwrap();
1156
1157                        // F0187: Check for scrollbar clicks first
1158                        let mut handled_scrollbar_click = false;
1159                        for muxbox in active_layout.get_all_muxboxes() {
1160                            if muxbox.has_scrollable_content() {
1161                                let muxbox_bounds = muxbox.bounds();
1162
1163                                // Check for vertical scrollbar click (right border)
1164                                if *x as usize == muxbox_bounds.right()
1165                                    && *y as usize > muxbox_bounds.top()
1166                                    && (*y as usize) < muxbox_bounds.bottom()
1167                                {
1168                                    let track_height =
1169                                        (muxbox_bounds.height() as isize - 2).max(1) as usize;
1170                                    let click_position = ((*y as usize) - muxbox_bounds.top() - 1)
1171                                        as f64
1172                                        / track_height as f64;
1173                                    let scroll_percentage =
1174                                        (click_position * 100.0).min(100.0).max(0.0);
1175
1176                                    log::trace!(
1177                                        "Vertical scrollbar click on muxbox {} at {}%",
1178                                        muxbox.id,
1179                                        scroll_percentage
1180                                    );
1181
1182                                    // Update muxbox vertical scroll
1183                                    let (muxbox_id, horizontal_scroll) = {
1184                                        let muxbox_to_update = app_context_for_click
1185                                            .app
1186                                            .get_muxbox_by_id_mut(&muxbox.id)
1187                                            .unwrap();
1188                                        muxbox_to_update.vertical_scroll = Some(scroll_percentage);
1189                                        (muxbox_to_update.id.clone(), muxbox_to_update.horizontal_scroll.unwrap_or(0.0))
1190                                    };
1191
1192                                    inner.update_app_context(app_context_for_click.clone());
1193                                    inner.send_message(Message::RedrawAppDiff);
1194                                    handled_scrollbar_click = true;
1195                                    
1196                                    // F0200: Save scroll position to YAML
1197                                    inner.send_message(Message::SaveMuxBoxScroll(
1198                                        muxbox_id,
1199                                        (horizontal_scroll * 100.0) as usize,
1200                                        (scroll_percentage * 100.0) as usize,
1201                                    ));
1202                                    break;
1203                                }
1204
1205                                // Check for horizontal scrollbar click (bottom border)
1206                                if *y as usize == muxbox_bounds.bottom()
1207                                    && *x as usize > muxbox_bounds.left()
1208                                    && (*x as usize) < muxbox_bounds.right()
1209                                {
1210                                    let track_width =
1211                                        (muxbox_bounds.width() as isize - 2).max(1) as usize;
1212                                    let click_position = ((*x as usize) - muxbox_bounds.left() - 1)
1213                                        as f64
1214                                        / track_width as f64;
1215                                    let scroll_percentage =
1216                                        (click_position * 100.0).min(100.0).max(0.0);
1217
1218                                    log::trace!(
1219                                        "Horizontal scrollbar click on muxbox {} at {}%",
1220                                        muxbox.id,
1221                                        scroll_percentage
1222                                    );
1223
1224                                    // Update muxbox horizontal scroll
1225                                    let (muxbox_id, vertical_scroll) = {
1226                                        let muxbox_to_update = app_context_for_click
1227                                            .app
1228                                            .get_muxbox_by_id_mut(&muxbox.id)
1229                                            .unwrap();
1230                                        muxbox_to_update.horizontal_scroll = Some(scroll_percentage);
1231                                        (muxbox_to_update.id.clone(), muxbox_to_update.vertical_scroll.unwrap_or(0.0))
1232                                    };
1233
1234                                    inner.update_app_context(app_context_for_click.clone());
1235                                    inner.send_message(Message::RedrawAppDiff);
1236                                    handled_scrollbar_click = true;
1237                                    
1238                                    // F0200: Save scroll position to YAML
1239                                    inner.send_message(Message::SaveMuxBoxScroll(
1240                                        muxbox_id,
1241                                        (scroll_percentage * 100.0) as usize,
1242                                        (vertical_scroll * 100.0) as usize,
1243                                    ));
1244                                    break;
1245                                }
1246                            }
1247                        }
1248
1249                        // If scrollbar click was handled, skip muxbox selection
1250                        if handled_scrollbar_click {
1251                            // Continue to next message
1252                        } else {
1253                            // F0091: Find which muxbox was clicked based on coordinates
1254                            if let Some(clicked_muxbox) =
1255                                active_layout.find_muxbox_at_coordinates(*x, *y)
1256                            {
1257                                log::trace!("Clicked on muxbox: {}", clicked_muxbox.id);
1258
1259                                // Check if muxbox has choices (menu items)
1260                                if let Some(choices) = &clicked_muxbox.choices {
1261                                    // Calculate which choice was clicked based on y and x offset within muxbox
1262                                    if let Some(clicked_choice_idx) = calculate_clicked_choice_index(
1263                                        clicked_muxbox,
1264                                        *x,
1265                                        *y,
1266                                        choices,
1267                                    ) {
1268                                        if let Some(clicked_choice) =
1269                                            choices.get(clicked_choice_idx)
1270                                        {
1271                                            log::trace!("Clicked on choice: {}", clicked_choice.id);
1272
1273                                            // First, select the parent muxbox if not already selected
1274                                            let layout = app_context_for_click
1275                                                .app
1276                                                .get_active_layout_mut()
1277                                                .unwrap();
1278                                            layout.deselect_all_muxboxes();
1279                                            layout.select_only_muxbox(&clicked_muxbox.id);
1280
1281                                            // Then select the clicked choice visually
1282                                            let muxbox_to_update = app_context_for_click
1283                                                .app
1284                                                .get_muxbox_by_id_mut(&clicked_muxbox.id)
1285                                                .unwrap();
1286                                            if let Some(ref mut muxbox_choices) =
1287                                                muxbox_to_update.choices
1288                                            {
1289                                                // Deselect all choices first
1290                                                for choice in muxbox_choices.iter_mut() {
1291                                                    choice.selected = false;
1292                                                }
1293                                                // Select only the clicked choice
1294                                                if let Some(selected_choice) =
1295                                                    muxbox_choices.get_mut(clicked_choice_idx)
1296                                                {
1297                                                    selected_choice.selected = true;
1298                                                }
1299                                            }
1300
1301                                            // Update the app context and immediately trigger redraw for responsiveness
1302                                            inner.update_app_context(app_context_for_click.clone());
1303                                            inner.send_message(Message::RedrawAppDiff);
1304
1305                                            // Then activate the clicked choice (same as pressing Enter)
1306                                            // Force threaded execution for clicked choices to maintain UI responsiveness
1307                                            if let Some(script) = &clicked_choice.script {
1308                                                let libs = app_context_unwrapped.app.libs.clone();
1309
1310                                                // Always use threaded execution for mouse clicks to keep UI responsive
1311                                                let _script_clone = script.clone();
1312                                                let _choice_id_clone = clicked_choice.id.clone();
1313                                                let muxbox_id_clone = clicked_muxbox.id.clone();
1314                                                let libs_clone = libs.clone();
1315
1316                                                // T312: Use unified ExecuteChoice message system
1317                                                inner.send_message(Message::ExecuteChoice(
1318                                                    clicked_choice.clone(),
1319                                                    muxbox_id_clone,
1320                                                    libs_clone,
1321                                                ));
1322
1323                                                // Spawn the choice execution in ThreadManager
1324                                                // TODO: Get ThreadManager reference to spawn the runnable
1325                                                log::trace!("Mouse click choice {} ready for ThreadManager execution", clicked_choice.id);
1326                                            }
1327                                        }
1328                                    } else {
1329                                        // Click was on muxbox with choices but not on any specific choice
1330                                        // Only select the muxbox, don't activate any choice
1331                                        if clicked_muxbox.tab_order.is_some()
1332                                            || clicked_muxbox.has_scrollable_content()
1333                                        {
1334                                            log::trace!(
1335                                                "Selecting muxbox (clicked on empty area): {}",
1336                                                clicked_muxbox.id
1337                                            );
1338
1339                                            // Deselect all muxboxes in the layout first
1340                                            let layout = app_context_for_click
1341                                                .app
1342                                                .get_active_layout_mut()
1343                                                .unwrap();
1344                                            layout.deselect_all_muxboxes();
1345                                            layout.select_only_muxbox(&clicked_muxbox.id);
1346
1347                                            inner.update_app_context(app_context_for_click);
1348                                            inner.send_message(Message::RedrawAppDiff);
1349                                        }
1350                                    }
1351                                } else {
1352                                    // MuxBox has no choices - just select it if it's selectable
1353                                    if clicked_muxbox.tab_order.is_some()
1354                                        || clicked_muxbox.has_scrollable_content()
1355                                    {
1356                                        log::trace!(
1357                                            "Selecting muxbox (no choices): {}",
1358                                            clicked_muxbox.id
1359                                        );
1360
1361                                        // Deselect all muxboxes in the layout first
1362                                        let layout = app_context_for_click
1363                                            .app
1364                                            .get_active_layout_mut()
1365                                            .unwrap();
1366                                        layout.deselect_all_muxboxes();
1367                                        layout.select_only_muxbox(&clicked_muxbox.id);
1368
1369                                        inner.update_app_context(app_context_for_click);
1370                                        inner.send_message(Message::RedrawAppDiff);
1371                                    }
1372                                }
1373                            }
1374                        }
1375                    }
1376                    Message::MouseDragStart(x, y) => {
1377                        // Check if muxboxes are locked before allowing resize/move
1378                        if app_context_unwrapped.config.locked {
1379                            // Skip all resize/move operations when locked
1380                            log::trace!("MuxBox resize/move blocked: muxboxes are locked");
1381                        } else {
1382                            // F0189: Check if drag started on a muxbox border first
1383                            let active_layout =
1384                                app_context_unwrapped.app.get_active_layout().unwrap();
1385                            let mut resize_state = MUXBOX_RESIZE_STATE.lock().unwrap();
1386                            *resize_state = None; // Clear any previous resize state
1387
1388                            // Check for muxbox border resize first
1389                            let mut handled_resize = false;
1390                            for muxbox in active_layout.get_all_muxboxes() {
1391                                if let Some(resize_edge) = detect_resize_edge(muxbox, *x, *y) {
1392                                    *resize_state = Some(MuxBoxResizeState {
1393                                        muxbox_id: muxbox.id.clone(),
1394                                        resize_edge,
1395                                        start_x: *x,
1396                                        start_y: *y,
1397                                        original_bounds: muxbox.position.clone(),
1398                                    });
1399                                    log::trace!(
1400                                        "Started resizing muxbox {} via {:?} edge",
1401                                        muxbox.id,
1402                                        resize_state.as_ref().unwrap().resize_edge
1403                                    );
1404                                    handled_resize = true;
1405                                    break;
1406                                }
1407                            }
1408
1409                            // F0191: If not a resize, check if drag started on muxbox title/top border for movement
1410                            let mut handled_move = false;
1411                            if !handled_resize {
1412                                let mut move_state = MUXBOX_MOVE_STATE.lock().unwrap();
1413                                *move_state = None; // Clear any previous move state
1414
1415                                for muxbox in active_layout.get_all_muxboxes() {
1416                                    if detect_move_area(muxbox, *x, *y) {
1417                                        *move_state = Some(MuxBoxMoveState {
1418                                            muxbox_id: muxbox.id.clone(),
1419                                            start_x: *x,
1420                                            start_y: *y,
1421                                            original_bounds: muxbox.position.clone(),
1422                                        });
1423                                        log::trace!(
1424                                            "Started moving muxbox {} via title/top border",
1425                                            muxbox.id
1426                                        );
1427                                        handled_move = true;
1428                                        break;
1429                                    }
1430                                }
1431                            }
1432                        }
1433
1434                        // F0188: Check for scroll knob drag (allowed even when locked)
1435                        let active_layout = app_context_unwrapped.app.get_active_layout().unwrap();
1436
1437                        // Check if any resize/move states are active (only possible when unlocked)
1438                        let has_active_resize = if !app_context_unwrapped.config.locked {
1439                            let resize_state_guard = MUXBOX_RESIZE_STATE.lock().unwrap();
1440                            resize_state_guard.is_some()
1441                        } else {
1442                            false
1443                        };
1444
1445                        let has_active_move = if !app_context_unwrapped.config.locked {
1446                            let move_state_guard = MUXBOX_MOVE_STATE.lock().unwrap();
1447                            move_state_guard.is_some()
1448                        } else {
1449                            false
1450                        };
1451
1452                        // F0188: If no resize or move is active, check if drag started on a scroll knob
1453                        if !has_active_resize && !has_active_move {
1454                            let mut drag_state = DRAG_STATE.lock().unwrap();
1455                            *drag_state = None; // Clear any previous drag state
1456
1457                            for muxbox in active_layout.get_all_muxboxes() {
1458                                if muxbox.has_scrollable_content() {
1459                                    let muxbox_bounds = muxbox.bounds();
1460
1461                                    // Check if drag started on vertical scroll knob
1462                                    if *x as usize == muxbox_bounds.right()
1463                                        && *y as usize > muxbox_bounds.top()
1464                                        && (*y as usize) < muxbox_bounds.bottom()
1465                                    {
1466                                        // Check if we clicked on the actual knob, not just the track
1467                                        if is_on_vertical_knob(muxbox, *y as usize) {
1468                                            let current_scroll =
1469                                                muxbox.vertical_scroll.unwrap_or(0.0);
1470                                            *drag_state = Some(DragState {
1471                                                muxbox_id: muxbox.id.clone(),
1472                                                is_vertical: true,
1473                                                start_x: *x,
1474                                                start_y: *y,
1475                                                start_scroll_percentage: current_scroll,
1476                                            });
1477                                            log::trace!("Started dragging vertical scroll knob on muxbox {}", muxbox.id);
1478                                            break;
1479                                        }
1480                                    }
1481
1482                                    // Check if drag started on horizontal scroll knob
1483                                    if *y as usize == muxbox_bounds.bottom()
1484                                        && *x as usize > muxbox_bounds.left()
1485                                        && (*x as usize) < muxbox_bounds.right()
1486                                    {
1487                                        // Check if we clicked on the actual knob, not just the track
1488                                        if is_on_horizontal_knob(muxbox, *x as usize) {
1489                                            let current_scroll =
1490                                                muxbox.horizontal_scroll.unwrap_or(0.0);
1491                                            *drag_state = Some(DragState {
1492                                                muxbox_id: muxbox.id.clone(),
1493                                                is_vertical: false,
1494                                                start_x: *x,
1495                                                start_y: *y,
1496                                                start_scroll_percentage: current_scroll,
1497                                            });
1498                                            log::trace!("Started dragging horizontal scroll knob on muxbox {}", muxbox.id);
1499                                            break;
1500                                        }
1501                                    }
1502                                }
1503                            }
1504                        }
1505                    }
1506                    Message::MouseDrag(x, y) => {
1507                        // Skip resize/move operations when muxboxes are locked
1508                        if !app_context_unwrapped.config.locked {
1509                            // F0189: Handle muxbox border resize during drag
1510                            let resize_state_guard = MUXBOX_RESIZE_STATE.lock().unwrap();
1511                            if let Some(ref resize_state) = *resize_state_guard {
1512                                let terminal_width = crate::screen_width();
1513                                let terminal_height = crate::screen_height();
1514
1515                                // FIXED: Handle 100% width panels where horizontal drag events may not work
1516                                let (effective_x, effective_y) =
1517                                    if resize_state.original_bounds.x2 == "100%" {
1518                                        // For 100% width panels at rightmost edge, if no horizontal movement is detected,
1519                                        // use the vertical movement as a proxy for horizontal movement to enable resizing
1520                                        let horizontal_delta =
1521                                            (*x as i32) - (resize_state.start_x as i32);
1522                                        let vertical_delta =
1523                                            (*y as i32) - (resize_state.start_y as i32);
1524
1525                                        if horizontal_delta == 0 && vertical_delta != 0 {
1526                                            // No horizontal movement detected but vertical movement exists
1527                                            // Use diagonal movement: apply vertical delta to horizontal as well
1528                                            let adjusted_x =
1529                                                resize_state.start_x as i32 + vertical_delta;
1530                                            (adjusted_x.max(0) as u16, *y)
1531                                        } else {
1532                                            (*x, *y)
1533                                        }
1534                                    } else {
1535                                        (*x, *y)
1536                                    };
1537
1538                                let new_bounds = calculate_new_bounds(
1539                                    &resize_state.original_bounds,
1540                                    &resize_state.resize_edge,
1541                                    resize_state.start_x,
1542                                    resize_state.start_y,
1543                                    effective_x,
1544                                    effective_y,
1545                                    terminal_width,
1546                                    terminal_height,
1547                                );
1548
1549                                // Update the muxbox bounds in real-time
1550                                if let Some(muxbox) = app_context_unwrapped
1551                                    .app
1552                                    .get_muxbox_by_id_mut(&resize_state.muxbox_id)
1553                                {
1554                                    muxbox.position = new_bounds;
1555                                    inner.update_app_context(app_context_unwrapped.clone());
1556                                    inner.send_message(Message::RedrawAppDiff);
1557                                }
1558                            }
1559
1560                            // F0191: Handle muxbox movement during drag
1561                            let move_state_guard = MUXBOX_MOVE_STATE.lock().unwrap();
1562                            if let Some(ref move_state) = *move_state_guard {
1563                                let terminal_width = crate::screen_width();
1564                                let terminal_height = crate::screen_height();
1565
1566                                let new_position = calculate_new_position(
1567                                    &move_state.original_bounds,
1568                                    move_state.start_x,
1569                                    move_state.start_y,
1570                                    *x,
1571                                    *y,
1572                                    terminal_width,
1573                                    terminal_height,
1574                                );
1575
1576                                // Update the muxbox position in real-time
1577                                if let Some(muxbox) = app_context_unwrapped
1578                                    .app
1579                                    .get_muxbox_by_id_mut(&move_state.muxbox_id)
1580                                {
1581                                    muxbox.position = new_position;
1582                                    inner.update_app_context(app_context_unwrapped.clone());
1583                                    inner.send_message(Message::RedrawAppDiff);
1584                                }
1585                            }
1586                        }
1587
1588                        // F0188: Handle scroll knob drag (always allowed, even when locked)
1589                        let drag_state_guard = DRAG_STATE.lock().unwrap();
1590                        if let Some(ref drag_state) = *drag_state_guard {
1591                            let muxbox_to_update = app_context_unwrapped
1592                                .app
1593                                .get_muxbox_by_id_mut(&drag_state.muxbox_id);
1594
1595                            if let Some(muxbox) = muxbox_to_update {
1596                                let muxbox_bounds = muxbox.bounds();
1597
1598                                if drag_state.is_vertical {
1599                                    // Calculate new vertical scroll percentage based on drag distance
1600                                    let track_height =
1601                                        (muxbox_bounds.height() as isize - 2).max(1) as usize;
1602                                    let drag_delta = (*y as isize) - (drag_state.start_y as isize);
1603                                    let percentage_delta =
1604                                        (drag_delta as f64 / track_height as f64) * 100.0;
1605                                    let new_percentage = (drag_state.start_scroll_percentage
1606                                        + percentage_delta)
1607                                        .min(100.0)
1608                                        .max(0.0);
1609
1610                                    muxbox.vertical_scroll = Some(new_percentage);
1611                                } else {
1612                                    // Calculate new horizontal scroll percentage based on drag distance
1613                                    let track_width =
1614                                        (muxbox_bounds.width() as isize - 2).max(1) as usize;
1615                                    let drag_delta = (*x as isize) - (drag_state.start_x as isize);
1616                                    let percentage_delta =
1617                                        (drag_delta as f64 / track_width as f64) * 100.0;
1618                                    let new_percentage = (drag_state.start_scroll_percentage
1619                                        + percentage_delta)
1620                                        .min(100.0)
1621                                        .max(0.0);
1622
1623                                    muxbox.horizontal_scroll = Some(new_percentage);
1624                                }
1625
1626                                inner.update_app_context(app_context_unwrapped.clone());
1627                                inner.send_message(Message::RedrawAppDiff);
1628                            }
1629                        }
1630                    }
1631                    Message::MouseDragEnd(x, y) => {
1632                        // Only handle resize/move end when muxboxes are unlocked
1633                        if !app_context_unwrapped.config.locked {
1634                            // F0189: End muxbox resize operation
1635                            let mut resize_state = MUXBOX_RESIZE_STATE.lock().unwrap();
1636                            if let Some(ref resize_state_data) = *resize_state {
1637                                log::trace!(
1638                                    "Ended muxbox resize at ({}, {}) for muxbox {}",
1639                                    x,
1640                                    y,
1641                                    resize_state_data.muxbox_id
1642                                );
1643
1644                                // Trigger YAML persistence
1645                                inner.send_message(Message::MuxBoxResizeComplete(
1646                                    resize_state_data.muxbox_id.clone(),
1647                                ));
1648                                *resize_state = None; // Clear resize state
1649                            } else {
1650                                // F0191: End muxbox move operation
1651                                let mut move_state = MUXBOX_MOVE_STATE.lock().unwrap();
1652                                if let Some(ref move_state_data) = *move_state {
1653                                    log::trace!(
1654                                        "Ended muxbox move at ({}, {}) for muxbox {}",
1655                                        x,
1656                                        y,
1657                                        move_state_data.muxbox_id
1658                                    );
1659
1660                                    // Trigger YAML persistence for new position
1661                                    inner.send_message(Message::MuxBoxMoveComplete(
1662                                        move_state_data.muxbox_id.clone(),
1663                                    ));
1664                                    *move_state = None; // Clear move state
1665                                }
1666                            }
1667                        }
1668
1669                        // F0188: End scroll knob drag operation (always allowed, even when locked)
1670                        let mut drag_state = DRAG_STATE.lock().unwrap();
1671                        if drag_state.is_some() {
1672                            log::trace!("Ended scroll knob drag at ({}, {})", x, y);
1673                            *drag_state = None; // Clear drag state
1674                        }
1675                    }
1676                    Message::ChoiceExecutionComplete(choice_id, muxbox_id, result) => {
1677                        log::info!(
1678                            "=== DRAWLOOP RECEIVED CHOICE EXECUTION COMPLETE: {} on muxbox {} ===",
1679                            choice_id,
1680                            muxbox_id
1681                        );
1682                        match result {
1683                            Ok(ref output) => log::info!(
1684                                "DrawLoop processing choice success: {} chars of output",
1685                                output.len()
1686                            ),
1687                            Err(ref error) => {
1688                                log::error!("DrawLoop processing choice error: {}", error)
1689                            }
1690                        }
1691
1692                        // First update the choice waiting state
1693                        if let Some(muxbox) =
1694                            app_context_unwrapped.app.get_muxbox_by_id_mut(muxbox_id)
1695                        {
1696                            if let Some(ref mut choices) = muxbox.choices {
1697                                if let Some(choice) =
1698                                    choices.iter_mut().find(|c| c.id == *choice_id)
1699                                {
1700                                    choice.waiting = false;
1701                                }
1702                            }
1703                        }
1704
1705                        // Then handle the output in a separate scope to avoid borrow conflicts
1706                        let target_muxbox_id = {
1707                            if let Some(muxbox) =
1708                                app_context_unwrapped.app.get_muxbox_by_id(muxbox_id)
1709                            {
1710                                if let Some(ref choices) = muxbox.choices {
1711                                    if let Some(choice) =
1712                                        choices.iter().find(|c| c.id == *choice_id)
1713                                    {
1714                                        let redirect_target = choice
1715                                            .redirect_output
1716                                            .as_ref()
1717                                            .unwrap_or(muxbox_id)
1718                                            .clone();
1719                                        log::info!(
1720                                            "Choice {} redirect_output: {:?} -> target muxbox: {}",
1721                                            choice_id,
1722                                            choice.redirect_output,
1723                                            redirect_target
1724                                        );
1725                                        redirect_target
1726                                    } else {
1727                                        log::warn!(
1728                                            "Choice {} not found in muxbox {}",
1729                                            choice_id,
1730                                            muxbox_id
1731                                        );
1732                                        muxbox_id.clone()
1733                                    }
1734                                } else {
1735                                    log::warn!("MuxBox {} has no choices", muxbox_id);
1736                                    muxbox_id.clone()
1737                                }
1738                            } else {
1739                                log::error!("MuxBox {} not found", muxbox_id);
1740                                muxbox_id.clone()
1741                            }
1742                        };
1743
1744                        let append = {
1745                            if let Some(muxbox) =
1746                                app_context_unwrapped.app.get_muxbox_by_id(muxbox_id)
1747                            {
1748                                if let Some(ref choices) = muxbox.choices {
1749                                    if let Some(choice) =
1750                                        choices.iter().find(|c| c.id == *choice_id)
1751                                    {
1752                                        choice.append_output.unwrap_or(false)
1753                                    } else {
1754                                        false
1755                                    }
1756                                } else {
1757                                    false
1758                                }
1759                            } else {
1760                                false
1761                            }
1762                        };
1763
1764                        match result {
1765                            Ok(output) => {
1766                                log::info!(
1767                                    "Choice {} output length: {} chars, redirecting to muxbox: {}",
1768                                    choice_id,
1769                                    output.len(),
1770                                    target_muxbox_id
1771                                );
1772                                update_muxbox_content(
1773                                    inner,
1774                                    &mut app_context_unwrapped,
1775                                    &target_muxbox_id,
1776                                    true,
1777                                    append,
1778                                    output,
1779                                );
1780                            }
1781                            Err(error) => {
1782                                log::error!("Error running choice script: {}", error);
1783                                update_muxbox_content(
1784                                    inner,
1785                                    &mut app_context_unwrapped,
1786                                    &target_muxbox_id,
1787                                    false,
1788                                    append,
1789                                    error,
1790                                );
1791                            }
1792                        }
1793                    }
1794                    Message::MuxBoxResizeComplete(muxbox_id) => {
1795                        // F0190: Save muxbox bounds changes to YAML file
1796                        log::info!(
1797                            "Saving muxbox resize changes to YAML for muxbox: {}",
1798                            muxbox_id
1799                        );
1800
1801                        // Get the updated muxbox bounds
1802                        if let Some(muxbox) = app_context_unwrapped.app.get_muxbox_by_id(muxbox_id)
1803                        {
1804                            let new_bounds = &muxbox.position;
1805                            log::debug!(
1806                                "New bounds for muxbox {}: x1={}, y1={}, x2={}, y2={}",
1807                                muxbox_id,
1808                                new_bounds.x1,
1809                                new_bounds.y1,
1810                                new_bounds.x2,
1811                                new_bounds.y2
1812                            );
1813
1814                            // Find the original YAML file path
1815                            if let Some(yaml_path) = &app_context_unwrapped.yaml_file_path {
1816                                match save_muxbox_bounds_to_yaml(yaml_path, muxbox_id, new_bounds) {
1817                                    Ok(()) => {
1818                                        log::info!(
1819                                            "Successfully saved muxbox {} bounds to YAML file",
1820                                            muxbox_id
1821                                        );
1822                                    }
1823                                    Err(e) => {
1824                                        log::error!(
1825                                            "Failed to save muxbox {} bounds to YAML: {}",
1826                                            muxbox_id,
1827                                            e
1828                                        );
1829                                    }
1830                                }
1831                            } else {
1832                                log::error!("CRITICAL: No YAML file path available for saving muxbox bounds - resize changes will not persist!");
1833                            }
1834                        } else {
1835                            log::error!("MuxBox {} not found for saving bounds", muxbox_id);
1836                        }
1837                    }
1838                    Message::MuxBoxMoveComplete(muxbox_id) => {
1839                        // F0191: Save muxbox position changes to YAML file
1840                        log::info!(
1841                            "Saving muxbox move changes to YAML for muxbox: {}",
1842                            muxbox_id
1843                        );
1844
1845                        // Get the updated muxbox position
1846                        if let Some(muxbox) = app_context_unwrapped.app.get_muxbox_by_id(muxbox_id)
1847                        {
1848                            let new_position = &muxbox.position;
1849                            log::debug!(
1850                                "New position for muxbox {}: x1={}, y1={}, x2={}, y2={}",
1851                                muxbox_id,
1852                                new_position.x1,
1853                                new_position.y1,
1854                                new_position.x2,
1855                                new_position.y2
1856                            );
1857
1858                            // Find the original YAML file path
1859                            if let Some(yaml_path) = &app_context_unwrapped.yaml_file_path {
1860                                match save_muxbox_bounds_to_yaml(yaml_path, muxbox_id, new_position)
1861                                {
1862                                    Ok(()) => {
1863                                        log::info!(
1864                                            "Successfully saved muxbox {} position to YAML file",
1865                                            muxbox_id
1866                                        );
1867                                    }
1868                                    Err(e) => {
1869                                        log::error!(
1870                                            "Failed to save muxbox {} position to YAML: {}",
1871                                            muxbox_id,
1872                                            e
1873                                        );
1874                                    }
1875                                }
1876                            } else {
1877                                log::error!("CRITICAL: No YAML file path available for saving muxbox position - move changes will not persist!");
1878                            }
1879                        } else {
1880                            log::error!("MuxBox {} not found for saving position", muxbox_id);
1881                        }
1882                    }
1883                    Message::SaveYamlState => {
1884                        // F0200: Save complete application state to YAML
1885                        log::info!("Saving complete application state to YAML");
1886
1887                        if let Some(yaml_path) = &app_context_unwrapped.yaml_file_path {
1888                            match save_complete_state_to_yaml(yaml_path, &app_context_unwrapped) {
1889                                Ok(()) => {
1890                                    log::info!("Successfully saved complete state to YAML file");
1891                                }
1892                                Err(e) => {
1893                                    log::error!("Failed to save complete state to YAML: {}", e);
1894                                }
1895                            }
1896                        } else {
1897                            log::error!("CRITICAL: No YAML file path available for saving complete state!");
1898                        }
1899                    }
1900                    Message::SaveActiveLayout(layout_id) => {
1901                        // F0200: Save active layout to YAML
1902                        log::info!("Saving active layout '{}' to YAML", layout_id);
1903
1904                        if let Some(yaml_path) = &app_context_unwrapped.yaml_file_path {
1905                            match save_active_layout_to_yaml(yaml_path, layout_id) {
1906                                Ok(()) => {
1907                                    log::info!("Successfully saved active layout to YAML file");
1908                                }
1909                                Err(e) => {
1910                                    log::error!("Failed to save active layout to YAML: {}", e);
1911                                }
1912                            }
1913                        } else {
1914                            log::error!("CRITICAL: No YAML file path available for saving active layout!");
1915                        }
1916                    }
1917                    Message::SaveMuxBoxContent(muxbox_id, content) => {
1918                        // F0200: Save muxbox content changes to YAML
1919                        log::debug!("Saving content changes to YAML for muxbox: {}", muxbox_id);
1920
1921                        if let Some(yaml_path) = &app_context_unwrapped.yaml_file_path {
1922                            match save_muxbox_content_to_yaml(yaml_path, muxbox_id, content) {
1923                                Ok(()) => {
1924                                    log::debug!("Successfully saved muxbox {} content to YAML", muxbox_id);
1925                                }
1926                                Err(e) => {
1927                                    log::error!("Failed to save muxbox {} content to YAML: {}", muxbox_id, e);
1928                                }
1929                            }
1930                        } else {
1931                            log::warn!("No YAML file path available for saving muxbox content");
1932                        }
1933                    }
1934                    Message::SaveMuxBoxScroll(muxbox_id, scroll_x, scroll_y) => {
1935                        // F0200: Save muxbox scroll position to YAML
1936                        log::debug!("Saving scroll position to YAML for muxbox: {} ({}, {})", muxbox_id, scroll_x, scroll_y);
1937
1938                        if let Some(yaml_path) = &app_context_unwrapped.yaml_file_path {
1939                            match save_muxbox_scroll_to_yaml(yaml_path, muxbox_id, *scroll_x, *scroll_y) {
1940                                Ok(()) => {
1941                                    log::debug!("Successfully saved muxbox {} scroll position to YAML", muxbox_id);
1942                                }
1943                                Err(e) => {
1944                                    log::error!("Failed to save muxbox {} scroll position to YAML: {}", muxbox_id, e);
1945                                }
1946                            }
1947                        } else {
1948                            log::warn!("No YAML file path available for saving muxbox scroll position");
1949                        }
1950                    }
1951                    Message::SwitchActiveLayout(layout_id) => {
1952                        // F0200: Switch active layout with YAML persistence
1953                        log::info!("Switching to active layout: {}", layout_id);
1954                        
1955                        // Update the active layout in app context
1956                        let mut app_context_cloned = app_context_unwrapped.clone();
1957                        match app_context_cloned.app.set_active_layout_with_yaml_save(
1958                            layout_id,
1959                            app_context_cloned.yaml_file_path.as_deref()
1960                        ) {
1961                            Ok(()) => {
1962                                inner.update_app_context(app_context_cloned);
1963                                inner.send_message(Message::RedrawApp);
1964                                log::info!("Successfully switched to layout '{}' with YAML persistence", layout_id);
1965                            }
1966                            Err(e) => {
1967                                log::error!("Failed to switch layout with YAML persistence: {}", e);
1968                                // Still update app context without YAML persistence
1969                                app_context_cloned.app.set_active_layout(layout_id);
1970                                inner.update_app_context(app_context_cloned);
1971                                inner.send_message(Message::RedrawApp);
1972                            }
1973                        }
1974                    }
1975                    _ => {}
1976                }
1977            }
1978
1979            // T311: Choice execution now handled via ChoiceExecutionComplete messages
1980            // Old POOL-based choice results processing removed
1981
1982            // Ensure the loop continues by sleeping briefly
1983            std::thread::sleep(std::time::Duration::from_millis(
1984                app_context.config.frame_delay,
1985            ));
1986            return (should_continue, app_context_unwrapped);
1987        }
1988
1989        (should_continue, app_context)
1990    }
1991);
1992
1993pub fn update_muxbox_content(
1994    inner: &mut RunnableImpl,
1995    app_context_unwrapped: &mut AppContext,
1996    muxbox_id: &str,
1997    success: bool,
1998    append_output: bool,
1999    output: &str,
2000) {
2001    log::info!(
2002        "=== UPDATE MUXBOX CONTENT: {} (success: {}, append: {}, output_len: {}) ===",
2003        muxbox_id,
2004        success,
2005        append_output,
2006        output.len()
2007    );
2008
2009    let mut app_context_unwrapped_cloned = app_context_unwrapped.clone();
2010    let muxbox = app_context_unwrapped.app.get_muxbox_by_id_mut(muxbox_id);
2011
2012    if let Some(found_muxbox) = muxbox {
2013        log::info!(
2014            "Found target muxbox: {} (redirect_output: {:?})",
2015            muxbox_id,
2016            found_muxbox.redirect_output
2017        );
2018
2019        if found_muxbox.redirect_output.is_some()
2020            && found_muxbox.redirect_output.as_ref().unwrap() != muxbox_id
2021        {
2022            log::info!(
2023                "MuxBox {} has its own redirect to: {}, following redirect chain",
2024                muxbox_id,
2025                found_muxbox.redirect_output.as_ref().unwrap()
2026            );
2027            update_muxbox_content(
2028                inner,
2029                &mut app_context_unwrapped_cloned,
2030                found_muxbox.redirect_output.as_ref().unwrap(),
2031                success,
2032                append_output,
2033                output,
2034            );
2035        } else {
2036            log::info!(
2037                "Updating muxbox {} content directly (no redirection)",
2038                muxbox_id
2039            );
2040            log::info!(
2041                "MuxBox {} current content length: {} chars",
2042                muxbox_id,
2043                found_muxbox.content.as_ref().map_or(0, |c| c.len())
2044            );
2045
2046            // Check if this is PTY streaming output by the newline indicator
2047            let is_pty_streaming = output.ends_with('\n');
2048
2049            if is_pty_streaming {
2050                // Use streaming update for PTY output (no timestamp formatting)
2051                log::info!("Using streaming update for muxbox {}", muxbox_id);
2052                found_muxbox.update_streaming_content(output, success);
2053            } else {
2054                // Use regular update for non-PTY output
2055                log::info!(
2056                    "Using regular update for muxbox {} (append: {})",
2057                    muxbox_id,
2058                    append_output
2059                );
2060                found_muxbox.update_content(output, append_output, success);
2061            }
2062
2063            log::info!(
2064                "MuxBox {} updated content length: {} chars",
2065                muxbox_id,
2066                found_muxbox.content.as_ref().map_or(0, |c| c.len())
2067            );
2068            inner.update_app_context(app_context_unwrapped.clone());
2069            inner.send_message(Message::RedrawMuxBox(muxbox_id.to_string()));
2070            log::info!("Sent RedrawMuxBox message for muxbox: {}", muxbox_id);
2071        }
2072    } else {
2073        log::error!("Could not find muxbox {} for content update.", muxbox_id);
2074        // List available muxboxes for debugging
2075        let available_muxboxes: Vec<String> = app_context_unwrapped
2076            .app
2077            .get_active_layout()
2078            .unwrap()
2079            .get_all_muxboxes()
2080            .iter()
2081            .map(|p| p.id.clone())
2082            .collect();
2083        log::error!("Available muxboxes: {:?}", available_muxboxes);
2084    }
2085}
2086
2087/// Extract muxbox content for clipboard copy
2088pub fn get_muxbox_content_for_clipboard(muxbox: &MuxBox) -> String {
2089    // Priority order: output > content > default message
2090    if !muxbox.output.is_empty() {
2091        muxbox.output.clone()
2092    } else if let Some(content) = &muxbox.content {
2093        content.clone()
2094    } else {
2095        format!("MuxBox '{}': No content", muxbox.id)
2096    }
2097}
2098
2099/// Copy text to system clipboard
2100pub fn copy_to_clipboard(content: &str) -> Result<(), Box<dyn std::error::Error>> {
2101    use std::process::Command;
2102
2103    // Platform-specific clipboard commands
2104    #[cfg(target_os = "macos")]
2105    {
2106        let mut child = Command::new("pbcopy")
2107            .stdin(std::process::Stdio::piped())
2108            .spawn()?;
2109
2110        if let Some(stdin) = child.stdin.take() {
2111            use std::io::Write;
2112            let mut stdin = stdin;
2113            stdin.write_all(content.as_bytes())?;
2114        }
2115
2116        child.wait()?;
2117    }
2118
2119    #[cfg(target_os = "linux")]
2120    {
2121        // Try xclip first, then xsel as fallback
2122        let result = Command::new("xclip")
2123            .arg("-selection")
2124            .arg("clipboard")
2125            .stdin(std::process::Stdio::piped())
2126            .spawn();
2127
2128        match result {
2129            Ok(mut child) => {
2130                if let Some(stdin) = child.stdin.take() {
2131                    use std::io::Write;
2132                    let mut stdin = stdin;
2133                    stdin.write_all(content.as_bytes())?;
2134                }
2135                child.wait()?;
2136            }
2137            Err(_) => {
2138                // Fallback to xsel
2139                let mut child = Command::new("xsel")
2140                    .arg("--clipboard")
2141                    .arg("--input")
2142                    .stdin(std::process::Stdio::piped())
2143                    .spawn()?;
2144
2145                if let Some(stdin) = child.stdin.take() {
2146                    use std::io::Write;
2147                    let mut stdin = stdin;
2148                    stdin.write_all(content.as_bytes())?;
2149                }
2150                child.wait()?;
2151            }
2152        }
2153    }
2154
2155    #[cfg(target_os = "windows")]
2156    {
2157        let mut child = Command::new("clip")
2158            .stdin(std::process::Stdio::piped())
2159            .spawn()?;
2160
2161        if let Some(stdin) = child.stdin.take() {
2162            use std::io::Write;
2163            let mut stdin = stdin;
2164            stdin.write_all(content.as_bytes())?;
2165        }
2166
2167        child.wait()?;
2168    }
2169
2170    Ok(())
2171}
2172
2173/// Calculate which choice was clicked based on muxbox bounds and click coordinates
2174/// T0258: Enhanced to check both X and Y coordinates against actual choice text bounds
2175/// Only clicks on actual choice text (not empty space after text) trigger choice activation
2176#[cfg(test)]
2177pub fn calculate_clicked_choice_index(
2178    muxbox: &MuxBox,
2179    click_x: u16,
2180    click_y: u16,
2181    choices: &[crate::model::muxbox::Choice],
2182) -> Option<usize> {
2183    calculate_clicked_choice_index_impl(muxbox, click_x, click_y, choices)
2184}
2185
2186#[cfg(not(test))]
2187fn calculate_clicked_choice_index(
2188    muxbox: &MuxBox,
2189    click_x: u16,
2190    click_y: u16,
2191    choices: &[crate::model::muxbox::Choice],
2192) -> Option<usize> {
2193    calculate_clicked_choice_index_impl(muxbox, click_x, click_y, choices)
2194}
2195
2196fn calculate_clicked_choice_index_impl(
2197    muxbox: &MuxBox,
2198    click_x: u16,
2199    click_y: u16,
2200    choices: &[crate::model::muxbox::Choice],
2201) -> Option<usize> {
2202    let bounds = muxbox.bounds();
2203    let muxbox_top = bounds.y1 as u16;
2204
2205    if click_y < muxbox_top || choices.is_empty() {
2206        return None;
2207    }
2208
2209    // Choices start at bounds.top() + 1 (one line below border) as per draw_utils.rs:610
2210    let choices_start_y = muxbox_top + 1;
2211
2212    if click_y < choices_start_y {
2213        return None; // Click was on border or title area
2214    }
2215
2216    // Check if this muxbox uses text wrapping by checking overflow_behavior directly
2217    // Note: We assume "wrap" behavior since this function will be called from contexts
2218    // where the overflow behavior is already determined to be "wrap"
2219    if let Some(overflow_behavior) = &muxbox.overflow_behavior {
2220        if overflow_behavior == "wrap" {
2221            return calculate_wrapped_choice_click(muxbox, click_x, click_y, choices);
2222        }
2223    }
2224
2225    // Original logic for non-wrapped choices
2226    let choice_index = (click_y - choices_start_y) as usize;
2227
2228    // Ensure click is within choice bounds (don't exceed available choices or muxbox height)
2229    let muxbox_bottom = bounds.y2 as u16;
2230    if choice_index >= choices.len() || click_y >= muxbox_bottom {
2231        return None;
2232    }
2233
2234    // T0258: Check if click is within the actual text bounds of the choice
2235    if let Some(choice) = choices.get(choice_index) {
2236        if let Some(content) = &choice.content {
2237            // Choices are rendered at bounds.left() + 2 (per draw_utils.rs:636)
2238            let choice_text_start_x = bounds.left() + 2;
2239
2240            // Format the content as it appears (including "..." for waiting choices)
2241            let formatted_content = if choice.waiting {
2242                format!("{}...", content)
2243            } else {
2244                content.clone()
2245            };
2246
2247            let choice_text_end_x = choice_text_start_x + formatted_content.len();
2248
2249            // Check if click X is within the actual text bounds
2250            if (click_x as usize) >= choice_text_start_x && (click_x as usize) < choice_text_end_x {
2251                Some(choice_index)
2252            } else {
2253                None // Click was after the text on the same line - should only select muxbox
2254            }
2255        } else {
2256            None // Choice has no content to click on
2257        }
2258    } else {
2259        None
2260    }
2261}
2262
2263/// Handle click detection for wrapped choices
2264fn calculate_wrapped_choice_click(
2265    muxbox: &MuxBox,
2266    click_x: u16,
2267    click_y: u16,
2268    choices: &[crate::model::muxbox::Choice],
2269) -> Option<usize> {
2270    let bounds = muxbox.bounds();
2271    let viewable_width = bounds.width().saturating_sub(4);
2272    let choices_start_y = bounds.y1 as u16 + 1;
2273    let choice_text_start_x = bounds.left() + 2;
2274
2275    // Create wrapped choice lines (same logic as in draw_utils.rs)
2276    let mut wrapped_choices = Vec::new();
2277    
2278    for (choice_idx, choice) in choices.iter().enumerate() {
2279        if let Some(content) = &choice.content {
2280            let formatted_content = if choice.waiting {
2281                format!("{}...", content)
2282            } else {
2283                content.clone()
2284            };
2285            
2286            let wrapped_lines = wrap_text_to_width_simple(&formatted_content, viewable_width);
2287            
2288            for wrapped_line in wrapped_lines {
2289                wrapped_choices.push((choice_idx, wrapped_line));
2290            }
2291        }
2292    }
2293
2294    if wrapped_choices.is_empty() {
2295        return None;
2296    }
2297
2298    // Calculate which wrapped line was clicked
2299    let clicked_line_index = (click_y - choices_start_y) as usize;
2300    
2301    if clicked_line_index >= wrapped_choices.len() {
2302        return None;
2303    }
2304
2305    let (original_choice_index, line_content) = &wrapped_choices[clicked_line_index];
2306    let choice_text_end_x = choice_text_start_x + line_content.len();
2307
2308    // Check if click X is within the actual text bounds of this wrapped line
2309    if (click_x as usize) >= choice_text_start_x && (click_x as usize) < choice_text_end_x {
2310        Some(*original_choice_index)
2311    } else {
2312        None // Click was after the text on the same line - should only select muxbox
2313    }
2314}
2315
2316/// Simple text wrapping for click detection (matches draw_utils.rs logic)
2317fn wrap_text_to_width_simple(text: &str, width: usize) -> Vec<String> {
2318    if width == 0 {
2319        return vec![text.to_string()];
2320    }
2321
2322    let mut wrapped_lines = Vec::new();
2323    
2324    for line in text.lines() {
2325        if line.len() <= width {
2326            wrapped_lines.push(line.to_string());
2327            continue;
2328        }
2329
2330        // Split long line into multiple wrapped lines
2331        let mut current_line = String::new();
2332        let mut current_width = 0;
2333
2334        for word in line.split_whitespace() {
2335            let word_len = word.len();
2336            
2337            // If word itself is longer than width, break it
2338            if word_len > width {
2339                // Finish current line if it has content
2340                if !current_line.is_empty() {
2341                    wrapped_lines.push(current_line.clone());
2342                    current_line.clear();
2343                    current_width = 0;
2344                }
2345                
2346                // Break the long word across multiple lines
2347                let mut remaining_word = word;
2348                while remaining_word.len() > width {
2349                    let (chunk, rest) = remaining_word.split_at(width);
2350                    wrapped_lines.push(chunk.to_string());
2351                    remaining_word = rest;
2352                }
2353                
2354                if !remaining_word.is_empty() {
2355                    current_line = remaining_word.to_string();
2356                    current_width = remaining_word.len();
2357                }
2358                continue;
2359            }
2360
2361            // Check if adding this word would exceed width
2362            let space_needed = if current_line.is_empty() { 0 } else { 1 }; // Space before word
2363            if current_width + space_needed + word_len > width {
2364                // Start new line with this word
2365                if !current_line.is_empty() {
2366                    wrapped_lines.push(current_line.clone());
2367                }
2368                current_line = word.to_string();
2369                current_width = word_len;
2370            } else {
2371                // Add word to current line
2372                if !current_line.is_empty() {
2373                    current_line.push(' ');
2374                    current_width += 1;
2375                }
2376                current_line.push_str(word);
2377                current_width += word_len;
2378            }
2379        }
2380
2381        // Add final line if it has content
2382        if !current_line.is_empty() {
2383            wrapped_lines.push(current_line);
2384        }
2385    }
2386
2387    if wrapped_lines.is_empty() {
2388        wrapped_lines.push(String::new());
2389    }
2390
2391    wrapped_lines
2392}
2393
2394/// Auto-scroll to ensure selected choice is visible
2395fn auto_scroll_to_selected_choice(muxbox: &mut crate::model::muxbox::MuxBox, selected_choice_index: usize) {
2396    use crate::draw_utils::wrap_text_to_width;
2397    
2398    let bounds = muxbox.bounds();
2399    let viewable_height = bounds.height().saturating_sub(2); // Account for borders
2400    
2401    // Handle different overflow behaviors
2402    if let Some(overflow_behavior) = &muxbox.overflow_behavior {
2403        match overflow_behavior.as_str() {
2404            "wrap" => {
2405                // Calculate wrapped lines for auto-scroll in wrapped choice mode
2406                if let Some(choices) = &muxbox.choices {
2407                    let viewable_width = bounds.width().saturating_sub(4);
2408                    let mut total_lines = 0;
2409                    let mut selected_line_start = 0;
2410                    let mut selected_line_end = 0;
2411                    
2412                    for (i, choice) in choices.iter().enumerate() {
2413                        if let Some(content) = &choice.content {
2414                            let formatted_content = if choice.waiting {
2415                                format!("{}...", content)
2416                            } else {
2417                                content.clone()
2418                            };
2419                            
2420                            let wrapped_lines = wrap_text_to_width(&formatted_content, viewable_width);
2421                            let line_count = wrapped_lines.len();
2422                            
2423                            if i == selected_choice_index {
2424                                selected_line_start = total_lines;
2425                                selected_line_end = total_lines + line_count - 1;
2426                            }
2427                            
2428                            total_lines += line_count;
2429                        }
2430                    }
2431                    
2432                    // Adjust scroll to keep selected wrapped lines visible
2433                    if total_lines > viewable_height {
2434                        let current_scroll_percent = muxbox.vertical_scroll.unwrap_or(0.0);
2435                        let current_scroll_offset = ((current_scroll_percent / 100.0) * (total_lines - viewable_height) as f64).floor() as usize;
2436                        let visible_start = current_scroll_offset;
2437                        let visible_end = visible_start + viewable_height - 1;
2438                        
2439                        let mut new_scroll_percent = current_scroll_percent;
2440                        
2441                        // Scroll down if selected choice is below visible area
2442                        if selected_line_end > visible_end {
2443                            let new_offset = selected_line_end.saturating_sub(viewable_height - 1);
2444                            new_scroll_percent = (new_offset as f64 / (total_lines - viewable_height) as f64) * 100.0;
2445                        }
2446                        // Scroll up if selected choice is above visible area
2447                        else if selected_line_start < visible_start {
2448                            let new_offset = selected_line_start;
2449                            new_scroll_percent = (new_offset as f64 / (total_lines - viewable_height) as f64) * 100.0;
2450                        }
2451                        
2452                        muxbox.vertical_scroll = Some(new_scroll_percent.clamp(0.0, 100.0));
2453                    }
2454                }
2455            },
2456            "scroll" => {
2457                // For scroll mode, use choice index directly
2458                if let Some(choices) = &muxbox.choices {
2459                    let total_choices = choices.len();
2460                    if total_choices > viewable_height {
2461                        let current_scroll_percent = muxbox.vertical_scroll.unwrap_or(0.0);
2462                        let current_scroll_offset = ((current_scroll_percent / 100.0) * (total_choices - viewable_height) as f64).floor() as usize;
2463                        let visible_start = current_scroll_offset;
2464                        let visible_end = visible_start + viewable_height - 1;
2465                        
2466                        let mut new_scroll_percent = current_scroll_percent;
2467                        
2468                        // Scroll down if selected choice is below visible area
2469                        if selected_choice_index > visible_end {
2470                            let new_offset = selected_choice_index.saturating_sub(viewable_height - 1);
2471                            new_scroll_percent = (new_offset as f64 / (total_choices - viewable_height) as f64) * 100.0;
2472                        }
2473                        // Scroll up if selected choice is above visible area
2474                        else if selected_choice_index < visible_start {
2475                            let new_offset = selected_choice_index;
2476                            new_scroll_percent = (new_offset as f64 / (total_choices - viewable_height) as f64) * 100.0;
2477                        }
2478                        
2479                        muxbox.vertical_scroll = Some(new_scroll_percent.clamp(0.0, 100.0));
2480                    }
2481                }
2482            },
2483            _ => {
2484                // For other overflow behaviors (fill, cross_out, etc.), use simple choice index
2485                if let Some(choices) = &muxbox.choices {
2486                    let total_choices = choices.len();
2487                    if total_choices > viewable_height {
2488                        let current_scroll_percent = muxbox.vertical_scroll.unwrap_or(0.0);
2489                        let current_scroll_offset = ((current_scroll_percent / 100.0) * (total_choices - viewable_height) as f64).floor() as usize;
2490                        let visible_start = current_scroll_offset;
2491                        let visible_end = visible_start + viewable_height - 1;
2492                        
2493                        let mut new_scroll_percent = current_scroll_percent;
2494                        
2495                        if selected_choice_index > visible_end {
2496                            let new_offset = selected_choice_index.saturating_sub(viewable_height - 1);
2497                            new_scroll_percent = (new_offset as f64 / (total_choices - viewable_height) as f64) * 100.0;
2498                        } else if selected_choice_index < visible_start {
2499                            let new_offset = selected_choice_index;
2500                            new_scroll_percent = (new_offset as f64 / (total_choices - viewable_height) as f64) * 100.0;
2501                        }
2502                        
2503                        muxbox.vertical_scroll = Some(new_scroll_percent.clamp(0.0, 100.0));
2504                    }
2505                }
2506            }
2507        }
2508    }
2509}
2510
2511/// Trigger visual flash for muxbox (stub implementation)
2512fn trigger_muxbox_flash(_muxbox_id: &str) {
2513    // TODO: Implement visual flash with color inversion
2514    // This would require storing flash state and modifying muxbox rendering
2515    // For now, the redraw provides visual feedback
2516}