diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index 49319ef9553..e61e47713fd 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -10289,6 +10289,13 @@ impl TerminalView { // start nor close the pane. let remote_restore_cancelled = self.remote_input_phase == Some(RemoteInputPhase::Cancelled); + // A daemon-backed pane that ends before its shell bootstrapped + // lost its connection, not its shell start: the remote readiness + // notice already explains the failure and offers Retry, so the + // local shell-start banner (which blames the bootstrap script) + // would be wrong and redundant. + let daemon_startup_failure = + self.remote_input_session_id.is_some() && !self.is_login_shell_bootstrapped; if self.remote_input_phase.is_some() { self.set_remote_input_phase( RemoteInputPhase::Failed, @@ -10301,7 +10308,7 @@ impl TerminalView { } // If the pty spawn has failed, we've already inserted a banner. - if !self.pty_spawn_failed && !remote_restore_cancelled { + if !self.pty_spawn_failed && !remote_restore_cancelled && !daemon_startup_failure { let shell_detail = self.shell_detail.take().unwrap_or("shell".to_owned()); self.insert_shell_process_terminated_banner( shell_terminated_banner::TerminationType::Premature { diff --git a/app/src/terminal/view_test.rs b/app/src/terminal/view_test.rs index f302dc6dc9e..e38f144837c 100644 --- a/app/src/terminal/view_test.rs +++ b/app/src/terminal/view_test.rs @@ -491,6 +491,52 @@ fn cancelled_remote_restore_teardown_shows_no_shell_failure_banner() { }); } +/// A daemon-backed pane whose connection fails before the remote shell +/// bootstrapped exits its terminal model. The remote failure notice (with +/// Retry) must stay the only explanation; no shell-start failure banner may be +/// added that blames the bootstrap script for a network problem. +#[test] +fn daemon_connect_failure_before_bootstrap_shows_no_shell_start_banner() { + App::test((), |mut app| async move { + initialize_app_for_terminal_view(&mut app); + let session = warp_core::SessionId::from(943u64); + let terminal = add_window_with_terminal(&mut app, None); + terminal.update(&mut app, |view, ctx| { + view.is_login_shell_bootstrapped = false; + view.set_remote_input_phase(RemoteInputPhase::Transport, Some(session), ctx); + view.show_remote_session_error( + "ssh: connect to host 192.0.2.10 port 22: Network is unreachable".to_string(), + Some(session), + ctx, + ); + view.set_remote_input_phase(RemoteInputPhase::Failed, Some(session), ctx); + }); + let rich_content_before = terminal.read(&app, |view, _| view.rich_content_views.len()); + + terminal.update(&mut app, |view, ctx| { + view.handle_terminal_event( + &ModelEvent::Exit { + reason: crate::terminal::model::terminal_model::ExitReason::PtyDisconnected, + }, + ctx, + ); + }); + + terminal.read(&app, |view, _| { + assert_eq!(view.rich_content_views.len(), rich_content_before); + assert!(view + .inline_banners_state + .shell_process_terminated_banner + .is_none()); + assert_eq!(view.remote_input_phase, Some(RemoteInputPhase::Failed)); + assert_eq!( + view.remote_session_error.as_deref(), + Some("ssh: connect to host 192.0.2.10 port 22: Network is unreachable") + ); + }); + }); +} + #[test] fn remote_notice_tone_separates_failures_from_ended_sessions() { assert_eq!(