diff --git a/src/client/shell/composition.rs b/src/client/shell/composition.rs index 523cec87..92cf3c46 100644 --- a/src/client/shell/composition.rs +++ b/src/client/shell/composition.rs @@ -40,35 +40,55 @@ impl ClientShellState { .navigate_workspace_id .as_ref() .is_some_and(|target| self.navigation_target_valid(target)); - super::endpoint_sidebar::render_expanded( - &mut buffer, - sidebar, - self.snapshot.as_deref(), - &self.config, - &mut render::ShellRenderState { - endpoints: &self.endpoints, - active_endpoint_id: &self.active_endpoint_id, - collapsed_endpoints: &self.collapsed_endpoints, - collapsed_groups: &self.collapsed_groups, - remote_collapsed_groups: &self.remote_collapsed_groups, - workspace_scroll: &mut self.workspace_scroll, - agent_scroll: &mut self.agent_scroll, - tab_scroll: &mut self.tab_scroll, - reveal_focused_workspace: &mut self.reveal_focused_workspace, - reveal_focused_tab: &mut self.reveal_focused_tab, - sidebar_collapsed: false, - sidebar_section_split: self.sidebar_section_split, - tab_drag_insert_index: None, - selected_workspace_id: self - .navigate_workspace_id - .as_ref() - .filter(|_| valid_navigation_target), - reveal_navigation_workspace: &mut self.reveal_navigation_workspace, - dragged_workspace_id: None, - workspace_drop_indicator_row: None, - }, - &mut self.hits, - ); + // A resize invalidates pane geometry, not the healthy Local workspace chrome. + let local_snapshot = self.snapshot.as_deref().filter(|_| { + self.endpoints.len() == 1 + && !self.sidebar_collapsed + && layout.sidebar.width > 0 + && self.endpoint_status(&self.active_endpoint_id) + == Some(ClientEndpointStatus::Online) + }); + let mut render_state = render::ShellRenderState { + endpoints: &self.endpoints, + active_endpoint_id: &self.active_endpoint_id, + collapsed_endpoints: &self.collapsed_endpoints, + collapsed_groups: &self.collapsed_groups, + remote_collapsed_groups: &self.remote_collapsed_groups, + workspace_scroll: &mut self.workspace_scroll, + agent_scroll: &mut self.agent_scroll, + tab_scroll: &mut self.tab_scroll, + reveal_focused_workspace: &mut self.reveal_focused_workspace, + reveal_focused_tab: &mut self.reveal_focused_tab, + sidebar_collapsed: false, + sidebar_section_split: self.sidebar_section_split, + tab_drag_insert_index: None, + selected_workspace_id: self + .navigate_workspace_id + .as_ref() + .filter(|_| valid_navigation_target), + reveal_navigation_workspace: &mut self.reveal_navigation_workspace, + dragged_workspace_id: None, + workspace_drop_indicator_row: None, + }; + if let Some(snapshot) = local_snapshot { + render::render_sidebar( + &mut buffer, + sidebar, + snapshot, + &self.config, + &mut render_state, + &mut self.hits, + ); + } else { + super::endpoint_sidebar::render_expanded( + &mut buffer, + sidebar, + self.snapshot.as_deref(), + &self.config, + &mut render_state, + &mut self.hits, + ); + } if !self.config.mouse_capture { self.hits = ShellHitMap::default(); } @@ -87,14 +107,16 @@ impl ClientShellState { } else { Rect::new(0, 0, cols, 1) }; - render::put_text( - &mut buffer, - message_area.x, - message_area.y, - message_area.width, - &message, - Style::default().fg(self.config.palette.overlay0), - ); + if local_snapshot.is_none() || self.endpoint_error.is_some() { + render::put_text( + &mut buffer, + message_area.x, + message_area.y, + message_area.width, + &message, + Style::default().fg(self.config.palette.overlay0), + ); + } render::render_mode_bar( &mut buffer, Rect::new(0, 0, cols, rows), diff --git a/src/client/shell/tests/chrome_context.rs b/src/client/shell/tests/chrome_context.rs index ff611a9b..5747bb97 100644 --- a/src/client/shell/tests/chrome_context.rs +++ b/src/client/shell/tests/chrome_context.rs @@ -99,6 +99,7 @@ fn client_owned_sidebar_dividers_resize_live() { state.set_snapshot(Box::new(snapshot())); state.set_pane_surface(surface()); state.compose(106, 30).expect("expanded sidebar"); + assert!(state.hits.machines.is_empty()); let workspace_body = state.hits.workspace_body; let needless_scroll = state.handle_raw_events(vec![RawInputEvent::Mouse(crossterm::event::MouseEvent { @@ -128,15 +129,55 @@ fn client_owned_sidebar_dividers_resize_live() { assert!(state.sidebar_width_manual); assert!(resize.repaint); assert!(resize.resize); + let waiting_frame = state.compose(106, 30).expect("waiting for resized surface"); + let waiting_text: String = waiting_frame + .cells + .iter() + .map(|cell| cell.symbol.as_str()) + .collect(); + assert!( + waiting_text.contains(" spaces"), + "local sidebar must keep spaces while resizing: {waiting_text}" + ); + assert!(!waiting_text.contains(" machines")); + assert!(!waiting_text.contains("Select a connected machine")); + assert!(!waiting_text.contains("LIVE")); + assert!(waiting_frame.cursor.is_none()); + assert!(state.pane_surface.is_none()); + assert!(state.hits.panes.is_empty()); + assert!(state.hits.pane_splits.is_empty()); + assert!(state.hits.machines.is_empty()); + assert_eq!(state.hits.sidebar_divider.x, 31); + assert_eq!(state.hits.workspaces[0].workspace_id, "ws_1"); + + let next_resize = + state.handle_raw_events(vec![RawInputEvent::Mouse(crossterm::event::MouseEvent { + kind: MouseEventKind::Drag(MouseButton::Left), + column: 32, + row: width_divider.y + 2, + modifiers: KeyModifiers::empty(), + })]); + assert!(next_resize.resize); + state.compose(106, 30).expect("continued resize"); + assert_eq!(state.hits.sidebar_divider.x, 32); state.handle_raw_events(vec![RawInputEvent::Mouse(crossterm::event::MouseEvent { kind: MouseEventKind::Up(MouseButton::Left), - column: 31, + column: 32, row: width_divider.y + 2, modifiers: KeyModifiers::empty(), })]); + assert!(state.chrome_drag.is_none()); state.set_pane_surface(surface()); - state.compose(106, 30).expect("resized sidebar"); + let recovered_frame = state.compose(106, 30).expect("resized sidebar"); + let recovered_text: String = recovered_frame + .cells + .iter() + .map(|cell| cell.symbol.as_str()) + .collect(); + assert!(recovered_text.contains(" spaces")); + assert!(recovered_text.contains("LIVE")); + assert!(!state.hits.panes.is_empty()); let section_divider = state.hits.sidebar_section_divider; state.handle_raw_events(vec![RawInputEvent::Mouse(crossterm::event::MouseEvent { kind: MouseEventKind::Down(MouseButton::Left),