diff --git a/src/client/shell/composition.rs b/src/client/shell/composition.rs index 8005dd87..c057682d 100644 --- a/src/client/shell/composition.rs +++ b/src/client/shell/composition.rs @@ -54,7 +54,8 @@ impl ClientShellState { .iter() .map(|spinner| { let mut cell = spinner.cell.clone(); - cell.symbol = (*symbol).to_owned(); + cell.symbol.clear(); + cell.symbol.push_str(symbol); crate::protocol::PaneSurfacePatchRow { x: spinner.x, y: spinner.y, diff --git a/src/client/shell/render.rs b/src/client/shell/render.rs index 17e6f57b..95149c9c 100644 --- a/src/client/shell/render.rs +++ b/src/client/shell/render.rs @@ -286,6 +286,9 @@ pub(super) fn render_shell_sidebar( } else { render_sidebar(buffer, area, snapshot, config, state, hits); } + let sidebar_toggle = hits.sidebar_toggle; + hits.animated_status_cells + .retain(|cell| !super::contains(sidebar_toggle, (cell.x, cell.y))); } pub(super) fn record_animated_status_cells( diff --git a/src/client/shell/state.rs b/src/client/shell/state.rs index 046785a7..e0a90d51 100644 --- a/src/client/shell/state.rs +++ b/src/client/shell/state.rs @@ -1922,6 +1922,7 @@ impl ClientShellState { fn spinner_eligible(&self) -> bool { self.config.status_indicators == crate::config::StatusIndicatorStyle::Animated && self.mode != ClientShellMode::Navigate + && self.endpoint_status(&self.active_endpoint_id) == Some(ClientEndpointStatus::Online) && self .last_composed_size .is_some_and(|(cols, rows)| self.layout(cols, rows).sidebar.width > 0) diff --git a/src/client/shell/tests/agents_worktrees_notifications.rs b/src/client/shell/tests/agents_worktrees_notifications.rs index 645b8f82..0b1e6059 100644 --- a/src/client/shell/tests/agents_worktrees_notifications.rs +++ b/src/client/shell/tests/agents_worktrees_notifications.rs @@ -719,6 +719,7 @@ fn animated_status_advances_only_on_its_desktop_deadline() { state.chrome_drag = None; state.endpoints[0].status = ClientEndpointStatus::Reconnecting; assert!(state.compose_spinner_patch(106, 30).is_none()); + assert!(!state.tick_spinner(deadline + std::time::Duration::from_millis(80))); state.endpoints[0].status = ClientEndpointStatus::Online; state.config.status_indicators = crate::config::StatusIndicatorStyle::Dots; diff --git a/src/client/shell/tests/endpoints.rs b/src/client/shell/tests/endpoints.rs index 001d3049..a1c1abbf 100644 --- a/src/client/shell/tests/endpoints.rs +++ b/src/client/shell/tests/endpoints.rs @@ -139,6 +139,44 @@ fn collapsed_wide_machine_initial_records_the_spinner_cell() { .any(|spinner| spinner.x == rect.x + 2 && spinner.y == rect.y)); } +#[test] +fn sidebar_toggle_is_not_retained_as_a_spinner_cell() { + let (mut state, _) = state_with_scrollable_agents(); + state.config.status_indicators = crate::config::StatusIndicatorStyle::Animated; + state.sidebar_collapsed = true; + for endpoint in &mut state.endpoints { + if let Some(snapshot) = endpoint.snapshot.as_mut() { + for agent in &mut snapshot.agents { + agent.agent_status = AgentStatus::Working; + } + } + } + + let first = state.compose(100, 8).expect("collapsed endpoint sidebar"); + let toggle = state.hits.sidebar_toggle; + assert!(state + .hits + .endpoint_agents + .iter() + .any(|(rect, _, _)| contains(*rect, (toggle.x, toggle.y)))); + assert!(!state + .hits + .animated_status_cells + .iter() + .any(|cell| contains(toggle, (cell.x, cell.y)))); + + let now = std::time::Instant::now(); + state.timer_delay(now); + let deadline = state.next_spinner_frame.expect("spinner deadline"); + assert!(state.tick_spinner(deadline)); + let patch = state + .compose_spinner_patch(100, 8) + .expect("visible spinner patch"); + let patched = apply_composed_surface_patch(&first, patch).expect("applicable spinner patch"); + let recomposed = state.compose(100, 8).expect("advanced full frame"); + assert_eq!(patched, recomposed); +} + #[test] fn switching_machines_preserves_aggregate_agent_scroll_and_visible_rows() { let (mut state, remote) = state_with_scrollable_agents();