fix(tui): keep sidebar resize live during active turns

The loading mouse filter (should_drop_loading_mouse_motion) dropped all
Drag events while app.is_loading unless a transcript selection or
scrollbar drag was active. A sidebar resize started on the handle
(Down passes the filter) then never received its Drag events, leaving
the resize wedged mid-gesture during live runs (#3063, symptom of the
#3096 subagent-runtime pressure on the TUI).

- Allow Drag events through the loading filter while
  app.sidebar_resizing is set.
- Clear last_sidebar_area / last_sidebar_handle_area and any in-flight
  resize when the sidebar is hidden or doesn't fit, so stale handle
  hit-areas can't capture clicks.
- Tests: resize down/drag/up while loading, mouse-up outside the
  handle still ends the resize.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
CodeWhale Agent
2026-06-12 15:26:53 -07:00
parent 3169c459ed
commit 2da01367e6
3 changed files with 85 additions and 0 deletions
+3
View File
@@ -38,8 +38,11 @@ pub(crate) fn should_drop_loading_mouse_motion(app: &App, mouse: MouseEvent) ->
!(over_sidebar || app.sidebar_hover_tooltip.is_some())
}
MouseEventKind::Drag(_) => {
// Sidebar drag-to-resize must stay live during active turns —
// dropping these events wedges the resize state mid-drag (#3063).
!app.viewport.transcript_selection.dragging
&& !app.viewport.transcript_scrollbar_dragging
&& !app.sidebar_resizing
}
_ => false,
}
+9
View File
@@ -7654,6 +7654,15 @@ fn render(f: &mut Frame, app: &mut App) {
// hit-testing can route scroll events correctly.
app.viewport.last_sidebar_area = sidebar_area;
// When the sidebar is hidden or doesn't fit, drop its stale mouse
// hit areas and any in-flight resize so clicks on those columns
// don't keep routing to an invisible handle (#3063).
if sidebar_area.is_none() {
app.last_sidebar_area = None;
app.last_sidebar_handle_area = None;
app.sidebar_resizing = false;
}
let chat_widget = ChatWidget::new(app, chat_area);
let buf = f.buffer_mut();
chat_widget.render(chat_area, buf);
+73
View File
@@ -727,6 +727,53 @@ fn loading_mouse_filter_keeps_active_drags() {
app.viewport.transcript_selection.dragging = false;
app.viewport.transcript_scrollbar_dragging = true;
assert!(!should_drop_loading_mouse_motion(&app, drag));
// Sidebar drag-to-resize must also survive the loading filter (#3063).
app.viewport.transcript_scrollbar_dragging = false;
app.sidebar_resizing = true;
assert!(!should_drop_loading_mouse_motion(&app, drag));
}
#[test]
fn loading_mouse_filter_allows_sidebar_resize_down_drag_up() {
let mut app = create_test_app();
app.is_loading = true;
setup_resize_handle(&mut app, 80, 33, 120);
let down = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 80,
row: 5,
modifiers: KeyModifiers::NONE,
};
assert!(!should_drop_loading_mouse_motion(&app, down));
handle_mouse_event(&mut app, down);
assert!(app.sidebar_resizing, "down on handle starts resize");
let drag = MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
column: 76,
row: 5,
modifiers: KeyModifiers::NONE,
};
assert!(
!should_drop_loading_mouse_motion(&app, drag),
"resize drag must not be dropped while loading"
);
handle_mouse_event(&mut app, drag);
let expected = ((37u32 * 100) / 120) as u16;
assert_eq!(app.sidebar_width_percent, expected);
let up = MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 76,
row: 5,
modifiers: KeyModifiers::NONE,
};
assert!(!should_drop_loading_mouse_motion(&app, up));
handle_mouse_event(&mut app, up);
assert!(!app.sidebar_resizing);
assert!(app.sidebar_width_dirty);
}
#[test]
@@ -3626,6 +3673,32 @@ fn sidebar_resize_up_ends_resizing_and_marks_dirty() {
);
}
#[test]
fn sidebar_resize_up_outside_handle_still_ends_resizing() {
let mut app = create_test_app();
setup_resize_handle(&mut app, 80, 33, 120);
app.sidebar_resizing = true;
app.sidebar_resize_anchor_x = 80;
app.sidebar_resize_anchor_width = 33;
// Release far away from the handle and the sidebar entirely.
handle_mouse_event(
&mut app,
MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 5,
row: 20,
modifiers: KeyModifiers::NONE,
},
);
assert!(
!app.sidebar_resizing,
"mouse up must clear resize state even outside the handle"
);
assert!(app.sidebar_width_dirty);
}
fn make_subagent(
id: &str,
status: crate::tools::subagent::SubAgentStatus,