@@ -41,6 +41,7 @@ impl AppState {
4141 sessions : Vec :: new ( ) ,
4242 last_local_cwd : [ cwd. clone ( ) , cwd] ,
4343 tasks : HashMap :: new ( ) ,
44+ task_progress : HashMap :: new ( ) ,
4445 next_task_id : 1 ,
4546 next_session_id : 0 ,
4647 tx,
@@ -185,6 +186,45 @@ impl AppState {
185186 }
186187 }
187188
189+ /// Aggregate progress of the **background** transfers (those not currently
190+ /// shown as the foreground progress dialog): `(bytes done, bytes total,
191+ /// count)`, or `None` when nothing is running in the background.
192+ pub ( crate ) fn background_summary ( & self ) -> Option < ( u64 , u64 , usize ) > {
193+ let foreground = match & self . dialog {
194+ Some ( Dialog :: Progress ( p) ) => Some ( p. id ) ,
195+ _ => None ,
196+ } ;
197+ let ( mut done, mut total, mut count) = ( 0u64 , 0u64 , 0usize ) ;
198+ for ( id, t) in & self . task_progress {
199+ if Some ( * id) == foreground {
200+ continue ;
201+ }
202+ count += 1 ;
203+ if let Some ( u) = & t. update {
204+ done += u. total_done ;
205+ total += u. total_total ;
206+ }
207+ }
208+ ( count > 0 ) . then_some ( ( done, total, count) )
209+ }
210+
211+ /// The menu-bar rect for the mini background-progress bar (left of the
212+ /// system-status widget), or `None` when nothing runs in the background.
213+ /// Shared by the renderer and mouse hit-testing so they stay in sync.
214+ pub ( crate ) fn menu_progress_rect ( & self , menubar_row : Rect ) -> Option < Rect > {
215+ self . background_summary ( ) ?;
216+ let mini_w = 24u16 . min ( menubar_row. width ) ;
217+ let status_shown = self . config . system_status
218+ && menubar_row. width >= crate :: ui:: menubar:: STATUS_MIN_WIDTH ;
219+ let right_edge = if status_shown {
220+ menubar_row. x + menubar_row. width . saturating_sub ( crate :: ui:: menubar:: STATUS_WIDTH )
221+ } else {
222+ menubar_row. x + menubar_row. width
223+ } ;
224+ let x = right_edge. saturating_sub ( mini_w) . max ( menubar_row. x ) ;
225+ Some ( Rect { x, y : menubar_row. y , width : mini_w, height : 1 } )
226+ }
227+
188228 // -- Event handling ----------------------------------------------------
189229
190230 pub async fn apply_event ( & mut self , ev : AppEvent ) {
@@ -195,22 +235,33 @@ impl AppState {
195235 {
196236 p. update ( & u) ;
197237 }
238+ // Keep the background snapshot current even with no visible dialog
239+ // (drives the menu-bar mini bar and the Background-operations list).
240+ if let Some ( t) = self . task_progress . get_mut ( & u. id ) {
241+ t. update = Some ( u) ;
242+ }
243+ // Advance the open "Background operations" list live.
244+ self . refresh_background_ops ( ) ;
198245 }
199246 AppEvent :: Conflict ( info) => {
200- // The engine is paused awaiting a decision. Stash the progress
201- // dialog and raise the overwrite prompt over it.
202- if let Some ( Dialog :: Progress ( p) ) = self . dialog . take ( ) {
203- self . stashed_progress = Some ( p) ;
204- }
247+ // The engine is paused awaiting a decision. Bring the conflicting
248+ // transfer to the foreground (rebuild its progress dialog from the
249+ // latest snapshot) and raise the overwrite prompt over it; the
250+ // Overwrite reply restores the stashed progress dialog. This works
251+ // whether the task was foreground or in the background.
252+ self . stashed_progress = Some ( self . progress_dialog_for ( info. id ) ) ;
205253 self . dialog = Some ( Dialog :: Overwrite ( OverwriteDialog :: new ( info) ) ) ;
206254 }
207255 AppEvent :: TaskDone { id, outcome } => {
208256 self . tasks . remove ( & id) ;
257+ self . task_progress . remove ( & id) ;
209258 if let Some ( Dialog :: Progress ( p) ) = & self . dialog
210259 && p. id == id
211260 {
212261 self . dialog = None ;
213262 }
263+ // Drop the finished task from an open "Background operations" list.
264+ self . refresh_background_ops ( ) ;
214265 if let TaskOutcome :: Failed ( msg) = outcome {
215266 self . dialog = Some ( Dialog :: Message ( MessageDialog :: error ( msg) ) ) ;
216267 }
0 commit comments