Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions snakesee/tui/accessibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class AccessibilityConfig:

succeeded: BarStyle
failed: BarStyle
running: BarStyle
remaining: BarStyle
incomplete: BarStyle
show_legend: bool
Expand All @@ -45,6 +46,7 @@ class AccessibilityConfig:
DEFAULT_CONFIG = AccessibilityConfig(
succeeded=BarStyle(char="\u2588", label="succeeded"),
failed=BarStyle(char="\u2588", label="failed"),
running=BarStyle(char="\u2588", label="running"),
remaining=BarStyle(char="\u2591", label="remaining"),
incomplete=BarStyle(char="\u2591", label="incomplete"),
show_legend=False,
Expand All @@ -53,6 +55,7 @@ class AccessibilityConfig:
ACCESSIBLE_CONFIG = AccessibilityConfig(
succeeded=BarStyle(char="=", label="succeeded"),
failed=BarStyle(char="X", label="failed"),
running=BarStyle(char=">", label="running"),
remaining=BarStyle(char="\u00b7", label="remaining"),
incomplete=BarStyle(char="?", label="incomplete"),
show_legend=True,
Expand Down
83 changes: 31 additions & 52 deletions snakesee/tui/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1678,32 +1678,25 @@ def _make_header(self, progress: WorkflowProgress) -> Panel:
return Panel(header_text, style="white on grey23", border_style=FG_BLUE, height=3)

def _make_progress_bar(self, progress: WorkflowProgress, width: int = 40) -> Text:
"""Create a colored progress bar showing succeeded/failed portions."""
"""Create a colored progress bar showing succeeded/failed/running/pending portions."""
total = max(1, progress.total_jobs)
succeeded = progress.completed_jobs
failed = progress.failed_jobs
running = len(progress.running_jobs)
config = self._accessibility_config

# Calculate widths for each segment
# Calculate widths for each segment, distributing rounding remainders
succeeded_width = int((succeeded / total) * width)
failed_width = int((failed / total) * width)
unfinished = max(0, progress.total_jobs - progress.completed_jobs - progress.failed_jobs)
incomplete = (
min(len(progress.incomplete_jobs_list), unfinished)
if progress.status == WorkflowStatus.INCOMPLETE
else 0
)
incomplete_width = int((incomplete / total) * width)
remaining_width = width - succeeded_width - failed_width - incomplete_width
running_width = int((running / total) * width)
pending_width = width - succeeded_width - failed_width - running_width
Comment on lines 1689 to +1692
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard segment width math against transient counter skew.

If counts briefly exceed total, pending_width becomes negative at Line 1657, and the bar under-renders. Clamp intermediate widths and pending width to non-negative bounds.

Proposed fix
-        succeeded_width = int((succeeded / total) * width)
-        failed_width = int((failed / total) * width)
-        running_width = int((running / total) * width)
-        pending_width = width - succeeded_width - failed_width - running_width
+        succeeded_width = min(width, max(0, int((succeeded / total) * width)))
+        failed_width = min(
+            width - succeeded_width, max(0, int((failed / total) * width))
+        )
+        running_width = min(
+            width - succeeded_width - failed_width,
+            max(0, int((running / total) * width)),
+        )
+        pending_width = max(0, width - succeeded_width - failed_width - running_width)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakesee/tui/monitor.py` around lines 1654 - 1657, Clamp the computed segment
widths to avoid negative values when counters briefly exceed total: when
computing succeeded_width, failed_width, and running_width (the existing
variables in snakesee.tui.monitor.py), guard against total == 0 and use max(0,
int((count / total) * width)) for each, then compute pending_width as max(0,
width - succeeded_width - failed_width - running_width) so pending never goes
negative; update the calculations where succeeded_width, failed_width,
running_width and pending_width are assigned.


# Build the bar with colored segments
bar = Text()
bar.append(config.succeeded.char * succeeded_width, style="green")
bar.append(config.failed.char * failed_width, style="red")
if incomplete_width > 0:
bar.append(config.incomplete.char * incomplete_width, style="yellow")
if remaining_width > 0:
bar.append(config.remaining.char * remaining_width, style="dim")
bar.append(config.running.char * running_width, style="yellow")
bar.append(config.remaining.char * pending_width, style="dim")

return bar

Expand All @@ -1725,6 +1718,8 @@ def _make_progress_panel(
# Create colored progress bar
progress_bar = self._make_progress_bar(progress, width=bar_width)

running = len(progress.running_jobs)

# Progress text line
progress_line = Text()
progress_line.append("Progress ", style=f"bold {FG_BLUE}")
Expand Down Expand Up @@ -1764,35 +1759,26 @@ def _make_progress_panel(

eta_text = Text.from_markup(" ".join(eta_parts)) if eta_parts else Text("")

# Legend for the progress bar
# Legend for the progress bar showing non-zero segments
config = self._accessibility_config
legend = Text()
show_legend = progress.failed_jobs > 0 or config.show_legend
if show_legend:
legend_parts: list[tuple[str, str, str]] = []
if progress.completed_jobs > 0:
legend_parts.append((config.succeeded.char, "green", f"{progress.completed_jobs} {config.succeeded.label}"))
if progress.failed_jobs > 0:
legend_parts.append((config.failed.char, "red", f"{progress.failed_jobs} {config.failed.label}"))
if running > 0:
legend_parts.append((config.running.char, "yellow", f"{running} {config.running.label}"))
pending = total - progress.completed_jobs - progress.failed_jobs - running
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue:
Use progress.pending_jobs

if pending > 0:
legend_parts.append((config.remaining.char, "dim", f"{pending} {config.remaining.label}"))
if legend_parts:
legend.append(" (", style="dim")
legend.append(config.succeeded.char, style="green")
legend.append(f"={progress.completed_jobs} {config.succeeded.label}", style="dim")
if progress.failed_jobs > 0:
legend.append(" ", style="dim")
legend.append(config.failed.char, style="red")
legend.append(f"={progress.failed_jobs} {config.failed.label}", style="dim")
unfinished = max(
0, progress.total_jobs - progress.completed_jobs - progress.failed_jobs
)
incomplete = (
min(len(progress.incomplete_jobs_list), unfinished)
if progress.status == WorkflowStatus.INCOMPLETE
else 0
)
remaining = unfinished - incomplete
if incomplete > 0:
legend.append(" ", style="dim")
legend.append(config.incomplete.char, style="yellow")
legend.append(f"={incomplete} {config.incomplete.label}", style="dim")
if remaining > 0:
legend.append(" ", style="dim")
legend.append(config.remaining.char, style="dim")
legend.append(f"={remaining} {config.remaining.label}", style="dim")
for i, (symbol, style, label) in enumerate(legend_parts):
if i > 0:
legend.append(" ", style="dim")
legend.append(symbol, style=style)
legend.append(f"={label}", style="dim")
legend.append(")", style="dim")

# Border color based on status (use FG colors for normal states)
Expand All @@ -1805,19 +1791,12 @@ def _make_progress_panel(
}
border_style = border_colors.get(progress.status, FG_BLUE)

# Combine progress line with legend if present
if show_legend:
full_progress = Text()
full_progress.append(progress_line)
full_progress.append(legend)
return Panel(
Group(full_progress, eta_text),
title="Progress",
border_style=border_style,
)

# Combine progress line with legend
full_progress = Text()
full_progress.append(progress_line)
full_progress.append(legend)
return Panel(
Group(progress_line, eta_text),
Group(full_progress, eta_text),
title="Progress",
border_style=border_style,
)
Expand Down
8 changes: 5 additions & 3 deletions tests/test_tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -2245,10 +2245,10 @@ def test_accessible_mode_always_shows_legend(self, tui_with_mocks: WorkflowMonit
assert "succeeded" in output
assert "remaining" in output

def test_default_mode_no_legend_without_failures(
def test_default_mode_legend_shows_segments(
self, tui_with_mocks: WorkflowMonitorTUI
) -> None:
"""In default mode, legend is not shown when there are no failures."""
"""In default mode, legend shows non-zero segments."""
from snakesee.tui.accessibility import DEFAULT_CONFIG

tui_with_mocks._accessibility_config = DEFAULT_CONFIG
Expand All @@ -2262,7 +2262,9 @@ def test_default_mode_no_legend_without_failures(
console = Console(file=buf, width=120, force_terminal=True)
console.print(panel)
output = buf.getvalue()
assert "succeeded" not in output
assert "succeeded" in output
assert "failed" not in output
assert "remaining" in output

def test_toggle_accessible_mode_with_key(self, tui_with_mocks: WorkflowMonitorTUI) -> None:
"""Pressing 'a' toggles between default and accessible mode."""
Expand Down