99from rich .tree import Tree
1010from rich .text import Text
1111
12+ # Constants
13+ PROGRESS_BAR_WIDTH = 25
14+
1215
1316class RichBatchProgressDisplay :
1417 """Rich-based progress display for batch runs."""
@@ -48,7 +51,7 @@ def start(self, stats: Dict, config: Dict):
4851 self ._create_display (),
4952 console = self .console ,
5053 refresh_per_second = 4 , # Reduced refresh rate to avoid flicker
51- auto_refresh = True
54+ auto_refresh = False # Disable auto-refresh to prevent race conditions with manual updates
5255 )
5356 self .live .start ()
5457
@@ -68,9 +71,11 @@ def update(self, stats: Dict, batch_data: Dict, elapsed_time: float):
6871 # Advance spinner
6972 self ._spinner_index = (self ._spinner_index + 1 ) % len (self ._spinner_frames )
7073
71- # Update live display
74+ # Update live display (synchronized to prevent race conditions)
7275 if self .live :
7376 self .live .update (self ._create_display ())
77+ # Force refresh since auto_refresh is disabled
78+ self .live .refresh ()
7479
7580 def stop (self ):
7681 """Stop the live progress display."""
@@ -138,37 +143,17 @@ def _create_display(self) -> Tree:
138143 is_last = idx == num_batches - 1
139144 tree_symbol = "└─" if is_last else "├─"
140145
141- # Format progress bar with better styling
142- progress_pct = (completed / total ) if total > 0 else 0
143- filled_width = int (progress_pct * 25 )
146+ # Extract job counts
147+ failed_count = batch_info .get ('failed' , 0 )
148+ success_count = completed
149+ total_processed = success_count + failed_count
150+ progress_pct = (total_processed / total ) if total > 0 else 0
144151
145- if status == 'complete' :
146- bar = "[bold green]" + "━" * 25 + "[/bold green]"
147- elif status == 'failed' :
148- bar = "[bold red]" + "━" * 25 + "[/bold red]"
149- elif status == 'cancelled' :
150- bar = "[bold yellow]" + "━" * filled_width + "[/bold yellow]"
151- if filled_width < 25 :
152- bar += "[dim yellow]" + "━" * (25 - filled_width ) + "[/dim yellow]"
153- elif status == 'running' :
154- bar = "[bold blue]" + "━" * filled_width + "[/bold blue]"
155- if filled_width < 25 :
156- bar += "[blue]╸[/blue]" + "[dim white]" + "━" * (24 - filled_width ) + "[/dim white]"
157- else :
158- bar = "[dim white]" + "━" * 25 + "[/dim white]"
152+ # Create progress bar based on status
153+ bar = self ._create_progress_bar (status , success_count , failed_count , total , progress_pct )
159154
160- # Format status with better colors and fixed width
161- if status == 'complete' :
162- status_text = "[bold green]Ended[/bold green] "
163- elif status == 'failed' :
164- status_text = "[bold red]Failed[/bold red] "
165- elif status == 'cancelled' :
166- status_text = "[bold yellow]Cancelled[/bold yellow]"
167- elif status == 'running' :
168- spinner = self ._spinner_frames [self ._spinner_index ]
169- status_text = f"[bold blue]{ spinner } Running[/bold blue]"
170- else :
171- status_text = "[dim]Pending[/dim]"
155+ # Format status text
156+ status_text = self ._format_status_text (status , failed_count )
172157
173158 # Calculate elapsed time
174159 start_time = batch_info .get ('start_time' )
@@ -196,7 +181,7 @@ def _create_display(self) -> Tree:
196181 else :
197182 time_str = "-:--:--"
198183
199- # Format percentage
184+ # Format percentage based on total processed (successful + failed)
200185 percentage = int (progress_pct * 100 )
201186
202187 # Get output filenames if completed
@@ -226,11 +211,11 @@ def _create_display(self) -> Tree:
226211 else :
227212 cost_text = f"${ cost :>5.3f} "
228213
229- # Create the batch line with proper spacing
214+ # Create the batch line
215+ display_stats = self ._get_display_stats (status , success_count , failed_count , total )
230216 batch_line = (
231217 f"{ provider } { batch_id :<18} { bar } "
232- f"{ completed :>2} /{ total :<2} { percentage :>3} % "
233- f"{ status_text } "
218+ f"{ display_stats ['completed' ]:>2} /{ total :<2} ({ display_stats ['percentage' ]} % done) { status_text :<15} "
234219 f"{ cost_text } "
235220 f"{ time_str :>8} "
236221 )
@@ -275,4 +260,85 @@ def _create_display(self) -> Tree:
275260 footer = " │ " .join (footer_parts )
276261 tree .add (f"\n [dim]{ footer } [/dim]" )
277262
278- return tree
263+ return tree
264+
265+ def _create_progress_bar (self , status : str , success_count : int , failed_count : int , total : int , progress_pct : float ) -> str :
266+ """Create a progress bar showing success/failure proportions."""
267+
268+ if status == 'complete' :
269+ return f"[bold green]{ '━' * PROGRESS_BAR_WIDTH } [/bold green]"
270+
271+ if status == 'failed' :
272+ return self ._create_mixed_bar (success_count , failed_count , total , PROGRESS_BAR_WIDTH )
273+
274+ if status == 'cancelled' :
275+ filled = int (progress_pct * PROGRESS_BAR_WIDTH )
276+ return f"[bold yellow]{ '━' * filled } [/bold yellow][dim yellow]{ '━' * (PROGRESS_BAR_WIDTH - filled )} [/dim yellow]"
277+
278+ if status == 'running' :
279+ filled = int (progress_pct * PROGRESS_BAR_WIDTH )
280+ if filled < PROGRESS_BAR_WIDTH :
281+ return f"[bold blue]{ '━' * filled } [/bold blue][blue]╸[/blue][dim white]{ '━' * (PROGRESS_BAR_WIDTH - filled - 1 )} [/dim white]"
282+ return f"[bold blue]{ '━' * PROGRESS_BAR_WIDTH } [/bold blue]"
283+
284+ # Pending
285+ return f"[dim white]{ '━' * PROGRESS_BAR_WIDTH } [/dim white]"
286+
287+ def _create_mixed_bar (self , success_count : int , failed_count : int , total : int , bar_width : int ) -> str :
288+ """Create a bar showing green (success) and red (failed) proportions."""
289+ if total == 0 :
290+ return f"[dim white]{ '━' * bar_width } [/dim white]"
291+
292+ # Use integer division to calculate base widths
293+ success_width = (success_count * bar_width ) // total
294+ failed_width = (failed_count * bar_width ) // total
295+
296+ # Distribute remainder to maintain exact bar_width
297+ remainder = bar_width - success_width - failed_width
298+ if remainder > 0 :
299+ # Distribute remainder based on which segment has larger fractional part
300+ success_fraction = (success_count * bar_width ) % total
301+ failed_fraction = (failed_count * bar_width ) % total
302+
303+ if success_fraction >= failed_fraction :
304+ success_width += remainder
305+ else :
306+ failed_width += remainder
307+
308+ # Build the bar
309+ bar_parts = []
310+ if success_width > 0 :
311+ bar_parts .append (f"[bold green]{ '━' * success_width } [/bold green]" )
312+ if failed_width > 0 :
313+ bar_parts .append (f"[bold red]{ '━' * failed_width } [/bold red]" )
314+
315+ return "" .join (bar_parts )
316+
317+ def _format_status_text (self , status : str , failed_count : int ) -> str :
318+ """Format the status text with appropriate colors and details."""
319+ if status == 'complete' :
320+ return "[bold green]Complete[/bold green]"
321+ elif status == 'failed' :
322+ if failed_count > 0 :
323+ return f"[bold red]Failed ({ failed_count } )[/bold red]"
324+ return "[bold red]Failed[/bold red]"
325+ elif status == 'cancelled' :
326+ return "[bold yellow]Cancelled[/bold yellow]"
327+ elif status == 'running' :
328+ spinner = self ._spinner_frames [self ._spinner_index ]
329+ return f"[bold blue]{ spinner } Running[/bold blue]"
330+ else :
331+ return "[dim]Pending[/dim]"
332+
333+ def _get_display_stats (self , status : str , success_count : int , failed_count : int , total : int ) -> dict :
334+ """Get the display statistics (completed count and percentage)."""
335+ if status == 'failed' and failed_count > 0 :
336+ # For failed batches, show success count to make it clear
337+ completed = success_count
338+ percentage = int ((success_count / total ) * 100 ) if total > 0 else 0
339+ else :
340+ # For other statuses, show total processed
341+ completed = success_count + failed_count
342+ percentage = int ((completed / total ) * 100 ) if total > 0 else 0
343+
344+ return {'completed' : completed , 'percentage' : percentage }
0 commit comments