@@ -219,6 +219,8 @@ struct ProvisioningDisplay {
219219 completed_steps : Vec < ProvisioningStep > ,
220220 /// Progress bars for completed steps (so they can be cleared).
221221 completed_bars : Vec < ProgressBar > ,
222+ /// The currently active provisioning step.
223+ active_step : Option < ProvisioningStep > ,
222224 /// The currently active step label (shown on the spinner).
223225 active_label : String ,
224226 /// Detail text shown next to the active step (e.g. image name).
@@ -253,6 +255,7 @@ impl ProvisioningDisplay {
253255 spacer,
254256 completed_steps : Vec :: new ( ) ,
255257 completed_bars : Vec :: new ( ) ,
258+ active_step : None ,
256259 active_label : ProvisioningStep :: RequestingSandbox
257260 . active_label ( )
258261 . to_string ( ) ,
@@ -290,11 +293,15 @@ impl ProvisioningDisplay {
290293 self . step_start = Instant :: now ( ) ;
291294 self . spinner . reset_elapsed ( ) ;
292295 self . active_detail . clear ( ) ;
296+ if self . active_step == Some ( step) {
297+ self . active_step = None ;
298+ }
293299 }
294300
295301 /// Set the active (in-progress) step shown on the spinner.
296- fn set_active ( & mut self , label : & str ) {
297- self . active_label = label. to_string ( ) ;
302+ fn set_active ( & mut self , step : ProvisioningStep ) {
303+ self . active_step = Some ( step) ;
304+ self . active_label = step. active_label ( ) . to_string ( ) ;
298305 self . active_detail . clear ( ) ;
299306 // Reset the spinner's elapsed time for the new step.
300307 self . spinner . reset_elapsed ( ) ;
@@ -304,11 +311,17 @@ impl ProvisioningDisplay {
304311
305312 /// Set the active step from a known provisioning step enum.
306313 fn set_active_step ( & mut self , step : ProvisioningStep ) {
307- self . set_active ( step. active_label ( ) ) ;
314+ if self . active_step == Some ( step) {
315+ return ;
316+ }
317+ self . set_active ( step) ;
308318 }
309319
310320 /// Set detail text shown alongside the active step (e.g. image name).
311321 fn set_active_detail ( & mut self , detail : & str ) {
322+ if self . active_detail == detail {
323+ return ;
324+ }
312325 self . active_detail = detail. to_string ( ) ;
313326 self . update_spinner ( ) ;
314327 }
@@ -6949,7 +6962,7 @@ fn format_timestamp_ms(ms: i64) -> String {
69496962#[ cfg( test) ]
69506963mod tests {
69516964 use super :: {
6952- ProvisioningStep , TlsOptions , build_sandbox_resource_limits,
6965+ ProvisioningDisplay , ProvisioningStep , TlsOptions , build_sandbox_resource_limits,
69536966 dockerfile_sources_supported_for_gateway, format_endpoint, format_gateway_select_header,
69546967 format_gateway_select_items, format_provider_attachment_table, gateway_add,
69556968 gateway_auth_label, gateway_env_override_warning, gateway_select_with, gateway_type_label,
@@ -6970,6 +6983,7 @@ mod tests {
69706983 use std:: path:: { Path , PathBuf } ;
69716984 use std:: process:: Command ;
69726985 use std:: thread;
6986+ use std:: time:: { Duration , Instant } ;
69736987 use tonic:: Status ;
69746988
69756989 use openshell_bootstrap:: GatewayMetadata ;
@@ -7178,6 +7192,48 @@ mod tests {
71787192 assert_eq ! ( progress_step_from_metadata( "driver-private-step" ) , None ) ;
71797193 }
71807194
7195+ #[ test]
7196+ fn provisioning_display_ignores_repeated_active_step_updates ( ) {
7197+ let mut display = ProvisioningDisplay :: new ( ) ;
7198+ display. set_active_step ( ProvisioningStep :: PullingSandboxImage ) ;
7199+ display. set_active_detail ( "Downloading layer-1 (1 MB/2 MB)" ) ;
7200+
7201+ let original_step_start = Instant :: now ( )
7202+ . checked_sub ( Duration :: from_secs ( 5 ) )
7203+ . expect ( "test duration should be representable" ) ;
7204+ display. step_start = original_step_start;
7205+
7206+ display. set_active_step ( ProvisioningStep :: PullingSandboxImage ) ;
7207+ display. set_active_detail ( "Downloading layer-1 (1 MB/2 MB)" ) ;
7208+
7209+ assert_eq ! (
7210+ display. active_step,
7211+ Some ( ProvisioningStep :: PullingSandboxImage )
7212+ ) ;
7213+ assert_eq ! ( display. active_detail, "Downloading layer-1 (1 MB/2 MB)" ) ;
7214+ assert_eq ! ( display. step_start, original_step_start) ;
7215+ display. clear ( ) ;
7216+ }
7217+
7218+ #[ test]
7219+ fn provisioning_display_resets_detail_on_active_step_transition ( ) {
7220+ let mut display = ProvisioningDisplay :: new ( ) ;
7221+ display. set_active_step ( ProvisioningStep :: PullingSandboxImage ) ;
7222+ display. set_active_detail ( "Downloading layer-1 (1 MB/2 MB)" ) ;
7223+
7224+ let original_step_start = Instant :: now ( )
7225+ . checked_sub ( Duration :: from_secs ( 5 ) )
7226+ . expect ( "test duration should be representable" ) ;
7227+ display. step_start = original_step_start;
7228+
7229+ display. set_active_step ( ProvisioningStep :: StartingSandbox ) ;
7230+
7231+ assert_eq ! ( display. active_step, Some ( ProvisioningStep :: StartingSandbox ) ) ;
7232+ assert ! ( display. active_detail. is_empty( ) ) ;
7233+ assert ! ( display. step_start > original_step_start) ;
7234+ display. clear ( ) ;
7235+ }
7236+
71817237 #[ test]
71827238 fn refresh_status_table_includes_operational_fields ( ) {
71837239 let header = refresh_status_header ( ) ;
0 commit comments