From b7b327510da7aba71001d994e86c27f69db980f3 Mon Sep 17 00:00:00 2001 From: Maggie Shan Date: Wed, 29 Apr 2026 09:53:27 -0400 Subject: [PATCH] Fix ui jitter during remote server initialization (#9342) ## Description * Fixes [APP-4307](https://linear.app/warpdotdev/issue/APP-4307/fix-blocklist-ui-flicker-when-initializing-remote-server) * The issue: during the initialization flow there would be significant ui jitter where the entire blocklist is moving up and down - this is because there is a gap between when the command/init shell finishes and when we render the loading footer (which looks like the output grid cursor appearing, then disappearing, then the loading footer appearing) * The fix is to render the footer as we're checking for if the binary is installed so that there is no gap for when the cursor disappears and the loading footer appears * The original reason this was done was to ensure that the loading footer didn't appear before or with the choice block - I've updated it to have an explicit check instead * This also updates where we're handling `RemoteServerSetupStateChanged` such that `session` is able to handle and update itself, and the terminal view is only in charge of rerendering the footer ## Testing https://www.loom.com/share/d4f157b4afd94f648f61ab1afbcee249 ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode --- app/src/terminal/event.rs | 16 ---------------- app/src/terminal/model/session.rs | 7 +++++-- app/src/terminal/model_events.rs | 7 ------- app/src/terminal/view.rs | 31 +++++++++++++++++++------------ crates/remote_server/src/setup.rs | 4 ---- 5 files changed, 24 insertions(+), 41 deletions(-) diff --git a/app/src/terminal/event.rs b/app/src/terminal/event.rs index 2fe3361c..40d58433 100644 --- a/app/src/terminal/event.rs +++ b/app/src/terminal/event.rs @@ -112,13 +112,6 @@ pub enum Event { is_tagged_in: bool, }, Handler(HandlerEvent), - /// Emitted by the async remote server setup task to report intermediate - /// state changes (e.g. Checking → Installing → Initializing) so the - /// prompt can show stage-specific messages. - RemoteServerSetupStateChanged { - session_id: SessionId, - state: RemoteServerSetupState, - }, /// Emitted when the remote server binary has been successfully checked or /// installed and is ready. The session is initialized independently on /// `Bootstrapped`; when the remote server later connects, the client is @@ -468,15 +461,6 @@ impl Debug for Event { write!(f, "AgentTaggedInChanged(is_tagged_in: {is_tagged_in})") } Event::Handler(handler_event) => write!(f, "Handler({handler_event:?}))"), - Event::RemoteServerSetupStateChanged { - session_id, - ref state, - } => { - write!( - f, - "RemoteServerSetupStateChanged(session: {session_id:?}, state: {state:?})" - ) - } Event::RemoteServerReady { session_id } => { write!(f, "RemoteServerReady(session: {session_id:?})") } diff --git a/app/src/terminal/model/session.rs b/app/src/terminal/model/session.rs index 723779c9..83ad953f 100644 --- a/app/src/terminal/model/session.rs +++ b/app/src/terminal/model/session.rs @@ -145,7 +145,7 @@ impl Sessions { #[cfg(feature = "local_tty")] if FeatureFlag::SshRemoteServer.is_enabled() { let mgr = RemoteServerManager::handle(ctx); - ctx.subscribe_to_model(&mgr, |sessions, event, _ctx| match event { + ctx.subscribe_to_model(&mgr, |sessions, event, ctx| match event { RemoteServerManagerEvent::SessionConnected { session_id: sid, host_id, @@ -161,6 +161,10 @@ impl Sessions { session.set_remote_host_id(None); } } + RemoteServerManagerEvent::SetupStateChanged { session_id, state } => { + sessions.set_remote_server_setup_state(*session_id, state.clone()); + ctx.notify(); + } RemoteServerManagerEvent::SessionConnecting { .. } | RemoteServerManagerEvent::SessionDeregistered { .. } | RemoteServerManagerEvent::SessionConnectionFailed { .. } @@ -170,7 +174,6 @@ impl Sessions { | RemoteServerManagerEvent::RepoMetadataSnapshot { .. } | RemoteServerManagerEvent::RepoMetadataUpdated { .. } | RemoteServerManagerEvent::RepoMetadataDirectoryLoaded { .. } - | RemoteServerManagerEvent::SetupStateChanged { .. } | RemoteServerManagerEvent::BinaryCheckComplete { .. } | RemoteServerManagerEvent::BinaryInstallComplete { .. } | RemoteServerManagerEvent::ClientRequestFailed { .. } diff --git a/app/src/terminal/model_events.rs b/app/src/terminal/model_events.rs index 145c7bfe..e17ead6b 100644 --- a/app/src/terminal/model_events.rs +++ b/app/src/terminal/model_events.rs @@ -117,13 +117,6 @@ impl ModelEventDispatcher { is_subshell, }) } - Event::RemoteServerSetupStateChanged { session_id, state } => { - self.sessions.update(ctx, |sessions, ctx| { - sessions.set_remote_server_setup_state(session_id, state.clone()); - ctx.notify(); - }); - return; - } Event::RemoteServerReady { session_id } => { log::info!("Remote server ready for session {session_id:?}"); return; diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index 559c407c..b8b619fd 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -131,6 +131,9 @@ use crate::code_review::git_status_update::{ }; use crate::code_review::telemetry_event::CodeReviewPaneEntrypoint; use crate::projects::ProjectManagementModel; +use crate::remote_server::manager::{ + RemoteServerInitPhase, RemoteServerManager, RemoteServerManagerEvent, +}; use crate::settings::ai::FocusedTerminalInfo; use crate::settings_view::mcp_servers_page::MCPServersSettingsPage; use crate::terminal::cli_agent_sessions::event::{ @@ -4168,17 +4171,14 @@ impl TerminalView { // Forward RemoteServerManager setup events into the terminal event stream // so the ModelEventDispatcher can gate session initialization on them. - if crate::features::FeatureFlag::SshRemoteServer.is_enabled() { - use crate::remote_server::manager::{RemoteServerManager, RemoteServerManagerEvent}; + if FeatureFlag::SshRemoteServer.is_enabled() { let mgr_handle = RemoteServerManager::handle(ctx); ctx.subscribe_to_model(&mgr_handle, |me, _, event, ctx| match event { - RemoteServerManagerEvent::SetupStateChanged { session_id, state } => { - me.model.lock().event_proxy.send_terminal_event( - crate::terminal::event::Event::RemoteServerSetupStateChanged { - session_id: *session_id, - state: state.clone(), - }, - ); + RemoteServerManagerEvent::SetupStateChanged { .. } => { + // Sessions handles the state update directly via its own + // subscription to the manager. Notify the view so the + // loading footer re-renders with the updated message. + ctx.notify(); } RemoteServerManagerEvent::SessionConnected { session_id, .. } => { me.model.lock().event_proxy.send_terminal_event( @@ -4198,7 +4198,7 @@ impl TerminalView { .unwrap_or((None, None)); send_telemetry_from_ctx!( TelemetryEvent::RemoteServerInitialization { - phase: remote_server::manager::RemoteServerInitPhase::Initialize, + phase: RemoteServerInitPhase::Initialize, error: None, remote_os, remote_arch, @@ -11359,18 +11359,24 @@ impl TerminalView { }) } - /// Returns `true` when the pending session has a connecting remote-server setup state. + /// Returns `true` when the loading footer should be shown in place of the + /// input editor during the SSH remote-server setup flow. fn show_remote_server_loading_footer(&self, model: &TerminalModel, app: &AppContext) -> bool { if !FeatureFlag::SshRemoteServer.is_enabled() { return false; } + // Don't show the loading footer while the choice block is visible; + // the choice block replaces it. + if self.active_ssh_remote_server_choice_block().is_some() { + return false; + } let Some(pending_sid) = model.pending_session_id() else { return false; }; self.sessions .as_ref(app) .remote_server_setup_state(pending_sid) - .is_some_and(|state| state.is_connecting()) + .is_some_and(|state| state.is_in_progress()) } /// Renders a shimmering loading footer in place of the input editor @@ -11388,6 +11394,7 @@ impl TerminalView { .as_ref(app) .remote_server_setup_state(sid) .map(|state| match state { + RemoteServerSetupState::Checking => "Checking...".to_string(), RemoteServerSetupState::Installing { progress_percent: Some(p), } => format!("Installing... ({p}%)"), diff --git a/crates/remote_server/src/setup.rs b/crates/remote_server/src/setup.rs index 0c3f036b..ed3e62db 100644 --- a/crates/remote_server/src/setup.rs +++ b/crates/remote_server/src/setup.rs @@ -37,10 +37,6 @@ impl RemoteServerSetupState { Self::Checking | Self::Installing { .. } | Self::Initializing ) } - - pub fn is_connecting(&self) -> bool { - matches!(self, Self::Installing { .. } | Self::Initializing) - } } /// Detected remote platform from `uname -sm` output.