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
This commit is contained in:
Maggie Shan
2026-04-29 09:53:27 -04:00
committed by GitHub
parent a6d1ece15e
commit b7b327510d
5 changed files with 24 additions and 41 deletions
-16
View File
@@ -112,13 +112,6 @@ pub enum Event {
is_tagged_in: bool, is_tagged_in: bool,
}, },
Handler(HandlerEvent), 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 /// Emitted when the remote server binary has been successfully checked or
/// installed and is ready. The session is initialized independently on /// installed and is ready. The session is initialized independently on
/// `Bootstrapped`; when the remote server later connects, the client is /// `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})") write!(f, "AgentTaggedInChanged(is_tagged_in: {is_tagged_in})")
} }
Event::Handler(handler_event) => write!(f, "Handler({handler_event:?}))"), 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 } => { Event::RemoteServerReady { session_id } => {
write!(f, "RemoteServerReady(session: {session_id:?})") write!(f, "RemoteServerReady(session: {session_id:?})")
} }
+5 -2
View File
@@ -145,7 +145,7 @@ impl Sessions {
#[cfg(feature = "local_tty")] #[cfg(feature = "local_tty")]
if FeatureFlag::SshRemoteServer.is_enabled() { if FeatureFlag::SshRemoteServer.is_enabled() {
let mgr = RemoteServerManager::handle(ctx); 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 { RemoteServerManagerEvent::SessionConnected {
session_id: sid, session_id: sid,
host_id, host_id,
@@ -161,6 +161,10 @@ impl Sessions {
session.set_remote_host_id(None); 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::SessionConnecting { .. }
| RemoteServerManagerEvent::SessionDeregistered { .. } | RemoteServerManagerEvent::SessionDeregistered { .. }
| RemoteServerManagerEvent::SessionConnectionFailed { .. } | RemoteServerManagerEvent::SessionConnectionFailed { .. }
@@ -170,7 +174,6 @@ impl Sessions {
| RemoteServerManagerEvent::RepoMetadataSnapshot { .. } | RemoteServerManagerEvent::RepoMetadataSnapshot { .. }
| RemoteServerManagerEvent::RepoMetadataUpdated { .. } | RemoteServerManagerEvent::RepoMetadataUpdated { .. }
| RemoteServerManagerEvent::RepoMetadataDirectoryLoaded { .. } | RemoteServerManagerEvent::RepoMetadataDirectoryLoaded { .. }
| RemoteServerManagerEvent::SetupStateChanged { .. }
| RemoteServerManagerEvent::BinaryCheckComplete { .. } | RemoteServerManagerEvent::BinaryCheckComplete { .. }
| RemoteServerManagerEvent::BinaryInstallComplete { .. } | RemoteServerManagerEvent::BinaryInstallComplete { .. }
| RemoteServerManagerEvent::ClientRequestFailed { .. } | RemoteServerManagerEvent::ClientRequestFailed { .. }
-7
View File
@@ -117,13 +117,6 @@ impl ModelEventDispatcher {
is_subshell, 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 } => { Event::RemoteServerReady { session_id } => {
log::info!("Remote server ready for session {session_id:?}"); log::info!("Remote server ready for session {session_id:?}");
return; return;
+19 -12
View File
@@ -131,6 +131,9 @@ use crate::code_review::git_status_update::{
}; };
use crate::code_review::telemetry_event::CodeReviewPaneEntrypoint; use crate::code_review::telemetry_event::CodeReviewPaneEntrypoint;
use crate::projects::ProjectManagementModel; use crate::projects::ProjectManagementModel;
use crate::remote_server::manager::{
RemoteServerInitPhase, RemoteServerManager, RemoteServerManagerEvent,
};
use crate::settings::ai::FocusedTerminalInfo; use crate::settings::ai::FocusedTerminalInfo;
use crate::settings_view::mcp_servers_page::MCPServersSettingsPage; use crate::settings_view::mcp_servers_page::MCPServersSettingsPage;
use crate::terminal::cli_agent_sessions::event::{ use crate::terminal::cli_agent_sessions::event::{
@@ -4168,17 +4171,14 @@ impl TerminalView {
// Forward RemoteServerManager setup events into the terminal event stream // Forward RemoteServerManager setup events into the terminal event stream
// so the ModelEventDispatcher can gate session initialization on them. // so the ModelEventDispatcher can gate session initialization on them.
if crate::features::FeatureFlag::SshRemoteServer.is_enabled() { if FeatureFlag::SshRemoteServer.is_enabled() {
use crate::remote_server::manager::{RemoteServerManager, RemoteServerManagerEvent};
let mgr_handle = RemoteServerManager::handle(ctx); let mgr_handle = RemoteServerManager::handle(ctx);
ctx.subscribe_to_model(&mgr_handle, |me, _, event, ctx| match event { ctx.subscribe_to_model(&mgr_handle, |me, _, event, ctx| match event {
RemoteServerManagerEvent::SetupStateChanged { session_id, state } => { RemoteServerManagerEvent::SetupStateChanged { .. } => {
me.model.lock().event_proxy.send_terminal_event( // Sessions handles the state update directly via its own
crate::terminal::event::Event::RemoteServerSetupStateChanged { // subscription to the manager. Notify the view so the
session_id: *session_id, // loading footer re-renders with the updated message.
state: state.clone(), ctx.notify();
},
);
} }
RemoteServerManagerEvent::SessionConnected { session_id, .. } => { RemoteServerManagerEvent::SessionConnected { session_id, .. } => {
me.model.lock().event_proxy.send_terminal_event( me.model.lock().event_proxy.send_terminal_event(
@@ -4198,7 +4198,7 @@ impl TerminalView {
.unwrap_or((None, None)); .unwrap_or((None, None));
send_telemetry_from_ctx!( send_telemetry_from_ctx!(
TelemetryEvent::RemoteServerInitialization { TelemetryEvent::RemoteServerInitialization {
phase: remote_server::manager::RemoteServerInitPhase::Initialize, phase: RemoteServerInitPhase::Initialize,
error: None, error: None,
remote_os, remote_os,
remote_arch, 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 { fn show_remote_server_loading_footer(&self, model: &TerminalModel, app: &AppContext) -> bool {
if !FeatureFlag::SshRemoteServer.is_enabled() { if !FeatureFlag::SshRemoteServer.is_enabled() {
return false; 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 { let Some(pending_sid) = model.pending_session_id() else {
return false; return false;
}; };
self.sessions self.sessions
.as_ref(app) .as_ref(app)
.remote_server_setup_state(pending_sid) .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 /// Renders a shimmering loading footer in place of the input editor
@@ -11388,6 +11394,7 @@ impl TerminalView {
.as_ref(app) .as_ref(app)
.remote_server_setup_state(sid) .remote_server_setup_state(sid)
.map(|state| match state { .map(|state| match state {
RemoteServerSetupState::Checking => "Checking...".to_string(),
RemoteServerSetupState::Installing { RemoteServerSetupState::Installing {
progress_percent: Some(p), progress_percent: Some(p),
} => format!("Installing... ({p}%)"), } => format!("Installing... ({p}%)"),
-4
View File
@@ -37,10 +37,6 @@ impl RemoteServerSetupState {
Self::Checking | Self::Installing { .. } | Self::Initializing 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. /// Detected remote platform from `uname -sm` output.