Skip to content

Conversation

@mikejmorgan-ai
Copy link
Member

@mikejmorgan-ai mikejmorgan-ai commented Dec 9, 2025

Implements #259 with complete tests and docs. Closes #259

Summary by CodeRabbit

Release Notes

  • New Features

    • Added enhanced progress visualization with interactive spinners, progress bars, download tracking with speed and ETA, and multi-step operation displays.
    • Automatic fallback to simple text-based progress when advanced UI unavailable.
  • Documentation

    • Added comprehensive Progress Indicators module documentation with usage examples and API reference.
  • Tests

    • Added comprehensive test coverage for progress visualization features.

✏️ Tip: You can customize this high-level summary in your review settings.

Implements #259 - Complete implementation with tests and docs.

Closes #259
@mikejmorgan-ai mikejmorgan-ai added enhancement New feature or request MVP Killer feature sprint labels Dec 9, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 9, 2025

Walkthrough

The PR introduces cortex/progress_indicators.py, a new comprehensive module providing Rich-based progress visualization with graceful fallback, supporting multi-step operation tracking, spinners, progress bars, and download monitoring. Accompanying documentation and tests are included.

Changes

Cohort / File(s) Summary
Progress Indicators Module
cortex/progress_indicators.py
New module with ProgressIndicator (main API), OperationType enum, OperationStep and OperationContext dataclasses, Rich-based and fallback operation handles, spinner and download trackers, multi-step tracker for sequential operations, and convenience functions for global indicator management.
Documentation
docs/PROGRESS_INDICATORS.md
New reference guide detailing module overview, features (spinner, progress bar, multi-step, download, operation), Rich UI fallback, API reference, usage examples, integration points, customization, architecture, testing, performance, troubleshooting, and contribution guidelines.
Tests
tests/test_progress_indicators.py
Comprehensive test suite covering OperationStep and OperationContext dataclasses, FallbackProgress behavior, ProgressIndicator initialization and methods, DownloadTracker lifecycle, MultiStepTracker operations, global convenience functions, edge cases (empty inputs, Unicode, exceptions), and conditional Rich-specific tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Areas requiring attention during review:

  • Rich library integration logic and fallback handling flow in ProgressIndicator initialization
  • Context manager implementation in RichOperationHandle and FallbackOperationHandle, particularly exception propagation and status updates
  • OperationContext state management and progress calculations (total_steps, completed_steps, overall_progress)
  • DownloadTracker speed calculation and ETA estimation logic
  • MultiStepTracker rendering and step lifecycle state transitions
  • Thread-safety considerations if used in concurrent contexts (not visible in current scope)

Poem

🐰 A progress tracker born, with Rich and grace,
No more silent waits in the CLI race!
Spinners spin, bars advance, downloads glide,
With fallback comfort as your guide.
Operations flow in steps so clear,
Users know the finish is near!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete. It lacks the required checklist items and only provides a minimal reference to the issue. Expand the description to include the checklist items (tests pass, PR title format, MVP label) and provide a brief summary of what was implemented.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title '[#259] Progress indicators for all operations' directly aligns with the main change—implementing a comprehensive progress indicators module.
Linked Issues check ✅ Passed The implementation fulfills all core requirements from #259: spinner support, progress bar, timing/ETA, success/failure messaging, and Rich library integration with fallback.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the progress indicators module and its documentation; no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 81.74% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/issue-259

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

# Demo 3: Progress bar
print("\n3. Progress bar:")
packages = ["nginx", "redis", "postgresql", "nodejs", "python3"]
for pkg in progress.progress_bar(packages, "Installing packages"):
Copy link

Choose a reason for hiding this comment

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

SGTM

import sys
import time
import threading
from typing import Optional, Callable, List, Dict, Any, Iterator
Copy link

Choose a reason for hiding this comment

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

SGTM

Comment on lines +22 to +33
from rich.progress import (
Progress,
SpinnerColumn,
TextColumn,
BarColumn,
TaskProgressColumn,
TimeElapsedColumn,
TimeRemainingColumn,
MofNCompleteColumn,
DownloadColumn,
TransferSpeedColumn
)
Copy link

Choose a reason for hiding this comment

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

SGTM

DownloadColumn,
TransferSpeedColumn
)
from rich.live import Live
Copy link

Choose a reason for hiding this comment

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

SGTM

TransferSpeedColumn
)
from rich.live import Live
from rich.panel import Panel
Copy link

Choose a reason for hiding this comment

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

SGTM

from rich.live import Live
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
Copy link

Choose a reason for hiding this comment

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

SGTM


import pytest
import time
import io
Copy link

Choose a reason for hiding this comment

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

SGTM

import pytest
import time
import io
import sys
Copy link

Choose a reason for hiding this comment

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

SGTM

import io
import sys
from datetime import datetime
from unittest.mock import Mock, patch, MagicMock
Copy link

Choose a reason for hiding this comment

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

SGTM

@shanko shanko self-requested a review December 10, 2025 00:52
yield RichOperationHandle(self, context, icon)
else:
yield FallbackOperationHandle(self, context)
except Exception as e:
Copy link

Choose a reason for hiding this comment

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

As a general rule, we should use the variable "e" to print the generic error. But in this case, since we are raising the error again (see line 232), it is OK to remove the unused variable as suggested by SonarCloud.

"""Update the progress message."""
self._progress.update(f"{self.context.title} - {message}")

def log(self, message: str, style: str = ""):
Copy link

Choose a reason for hiding this comment

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

Removing the unused param "style" is fine.

assert step.name == "Test"
assert step.description == "Test step"
assert step.status == "pending"
assert step.progress == 0.0
Copy link

Choose a reason for hiding this comment

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

SGTM

]
)

assert context.overall_progress == 0.5
Copy link

Choose a reason for hiding this comment

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

Since this is the only assert in the test, use math.isclose() instead of equality.

title="Test"
)

assert context.overall_progress == 0.0
Copy link

Choose a reason for hiding this comment

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

Since this is the only assert in the test, use math.isclose() instead of equality.

progress.stop()

# Should complete without error
assert True
Copy link

Choose a reason for hiding this comment

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

Replace this assert which is ineffective is really testing anything with a more stronger assert.

time.sleep(0.2)

# Should complete without error
assert True
Copy link

Choose a reason for hiding this comment

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

Replace this assert which is ineffective is really testing anything with a more stronger assert.


assert tracker.steps[0].status == "completed"
assert tracker.steps[0].end_time is not None
assert tracker.steps[0].progress == 1.0
Copy link

Choose a reason for hiding this comment

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

use math.isclose() instead of equality.

def test_operation_exception(self, indicator):
"""Test operation that raises exception."""
with pytest.raises(ValueError):
with indicator.operation("Test") as op:
Copy link

Choose a reason for hiding this comment

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

Use the variable "op" in a meaningful way instead of removing it as suggested.

Copy link

@shanko shanko left a comment

Choose a reason for hiding this comment

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

I have commented on the changes that are recommended by the code quality bots. Most of them are "SGTM" (for Sounds Good To Me). Which means, those changes should be made in the code BEFORE merging this PR. A few of my comments have an explicit suggestion on what else needs to be done. Please do these changes and resubmit for review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (13)
tests/test_progress_indicators.py (7)

9-12: Remove unused imports.

These imports have been flagged as unused and acknowledged for removal.


127-127: Use math.isclose() for floating-point comparison.

As previously noted, use math.isclose() instead of direct equality for floating-point assertions.


136-136: Use math.isclose() for floating-point comparison.

As previously noted, use math.isclose() instead of direct equality for floating-point assertions.


159-161: Strengthen test assertion.

The current assertion assert True doesn't validate any behavior. As previously noted, replace with a meaningful assertion.

Consider asserting that the progress completed without error or checking output:

         progress.stop()
         
-        # Should complete without error
-        assert True
+        captured = capsys.readouterr()
+        # Verify spinner animation ran and stopped cleanly
+        assert "✓" in captured.out or "Updated" in captured.out or "Initial" in captured.out

218-220: Strengthen test assertion.

The current assertion assert True doesn't validate any behavior. As previously noted, replace with a meaningful assertion.

Consider capturing output to verify the spinner ran:

         with indicator.spinner("Loading..."):
             time.sleep(0.2)
         
-        # Should complete without error
-        assert True
+        captured = capsys.readouterr()
+        # Verify spinner completed with success marker
+        assert "✓" in captured.out or "Loading" in captured.out

354-356: Use math.isclose() for floating-point comparison.

As previously noted, use math.isclose() instead of direct equality for floating-point assertions.

+import math
+
 # In test_complete_step:
-        assert tracker.steps[0].progress == 1.0
+        assert math.isclose(tracker.steps[0].progress, 1.0)

474-478: Use the op variable meaningfully.

As previously requested, the op variable should be used in a meaningful way rather than being ignored.

     def test_operation_exception(self, indicator):
         """Test operation that raises exception."""
         with pytest.raises(ValueError):
             with indicator.operation("Test") as op:
+                op.update("About to fail...")
                 raise ValueError("Test error")
cortex/progress_indicators.py (6)

13-13: Remove unused import Callable.

As previously flagged, Callable is imported but not used.


27-27: Remove unused import TaskProgressColumn.

As previously flagged, TaskProgressColumn is imported but not used.


34-34: Remove unused import Live.

As previously flagged, Live from rich.live is imported but not used.


35-35: Remove unused import Panel.

As previously flagged, Panel from rich.panel is imported but not used.


37-37: Remove unused import Text.

As previously flagged, Text from rich.text is imported but not used.


700-700: Replace unused loop variable pkg with _.

As previously flagged, the loop variable is not used.

-    for pkg in progress.progress_bar(packages, "Installing packages"):
+    for _ in progress.progress_bar(packages, "Installing packages"):
🧹 Nitpick comments (1)
docs/PROGRESS_INDICATORS.md (1)

335-349: Add language specifier to fenced code block.

The architecture diagram code block should have a language specifier for proper rendering. Use text or plaintext for ASCII art diagrams.

-```
+```text
 ┌─────────────────────────────────────────────────┐
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a549afa and dbbc576.

📒 Files selected for processing (3)
  • cortex/progress_indicators.py (1 hunks)
  • docs/PROGRESS_INDICATORS.md (1 hunks)
  • tests/test_progress_indicators.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_progress_indicators.py (1)
cortex/progress_indicators.py (44)
  • ProgressIndicator (156-367)
  • OperationType (46-58)
  • OperationStep (62-78)
  • OperationContext (82-105)
  • FallbackProgress (109-153)
  • DownloadTracker (486-543)
  • MultiStepTracker (546-642)
  • get_progress_indicator (649-654)
  • spinner (240-257)
  • spinner (658-660)
  • operation (196-237)
  • operation (663-665)
  • progress_bar (259-298)
  • progress_bar (668-670)
  • duration_seconds (73-78)
  • total_steps (94-95)
  • completed_steps (98-99)
  • overall_progress (102-105)
  • start (119-124)
  • stop (139-145)
  • update (135-137)
  • update (387-390)
  • update (435-437)
  • update (470-472)
  • update (481-483)
  • update (514-524)
  • fail (147-153)
  • fail (406-411)
  • fail (448-451)
  • fail (539-543)
  • complete (399-404)
  • complete (443-446)
  • complete (526-537)
  • print_success (341-346)
  • print_error (348-353)
  • print_warning (355-360)
  • print_info (362-367)
  • start_step (592-600)
  • complete_step (602-611)
  • fail_step (613-621)
  • skip_step (623-629)
  • finish (631-642)
  • multi_step (316-339)
  • download_progress (300-314)
🪛 LanguageTool
docs/PROGRESS_INDICATORS.md

[grammar] ~366-~366: Ensure spelling is correct
Context: ...Performance - Spinner updates: 10 FPS (100ms interval) - Progress bar: Updates on ea...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
docs/PROGRESS_INDICATORS.md

335-335: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (18)
docs/PROGRESS_INDICATORS.md (1)

1-414: Documentation is comprehensive and well-structured.

The documentation covers all aspects of the Progress Indicators module including API reference, usage examples, integration patterns, and troubleshooting. It aligns well with the implementation in cortex/progress_indicators.py.

tests/test_progress_indicators.py (7)

30-69: LGTM! OperationStep tests are thorough.

Tests cover default values, duration calculations for not-started, running, and completed states. The timing assertions appropriately use ranges to account for execution variance.


71-137: LGTM! OperationContext tests provide good coverage.

Tests validate default values, step counting, completion tracking, and progress calculation including the edge case of empty steps.


271-313: LGTM! DownloadTracker tests cover the essential lifecycle.

Tests validate initialization, progressive updates, completion, and failure scenarios.


315-410: LGTM! MultiStepTracker tests are comprehensive.

Tests cover initialization, step lifecycle (start, complete, fail, skip), finishing with various states, and out-of-bounds handling.


505-542: LGTM! Rich integration tests are properly guarded.

The @pytest.mark.skipif decorator correctly skips these tests when Rich is unavailable, ensuring CI doesn't fail in minimal environments.


544-588: LGTM! Integration tests validate end-to-end flows.

The tests simulate realistic installation and download workflows, verifying the components work together correctly.


232-236: No division by zero issue in progress_bar with empty list.

The fallback branch in progress_bar safely handles empty lists. When items is empty, the for loop at line 292 (for i, item in enumerate(items):) produces zero iterations, so the division operation at line 293 (pct = (i + 1) / total * 100) is never executed. The test correctly verifies this behavior.

cortex/progress_indicators.py (10)

46-59: LGTM! OperationType enum is well-defined.

The enum covers all relevant operation types for the CLI tool with appropriate string values.


61-79: LGTM! OperationStep dataclass is well-designed.

The duration_seconds property elegantly handles both running and completed states.


81-106: LGTM! OperationContext dataclass provides useful computed properties.

The progress calculation and step counting properties are correctly implemented with proper handling for empty steps.


108-154: LGTM! FallbackProgress provides a clean fallback implementation.

The threaded spinner animation with proper cleanup on stop/fail is well implemented. The daemon thread ensures it won't block program exit.


156-194: LGTM! ProgressIndicator initialization is clean.

The automatic fallback when Rich is unavailable and the comprehensive icon/color mappings are well thought out.


195-238: LGTM! Operation context manager handles lifecycle correctly.

The exception handling properly marks operations as failed and the finally block ensures cleanup regardless of outcome.


370-424: LGTM! RichOperationHandle provides clean lifecycle management.

The __exit__ method properly handles exceptions and auto-completion, and the status spinner is correctly started/stopped.


426-462: LGTM! FallbackOperationHandle mirrors Rich functionality.

The fallback handle provides the same interface and behavior as the Rich version.


546-643: LGTM! MultiStepTracker is well-implemented.

The step lifecycle management, bounds checking, and finish summary logic are all correct. The Rich rendering with appropriate icons for each state provides good visual feedback.


645-671: LGTM! Global convenience functions provide a clean API.

The singleton pattern for get_progress_indicator and the wrapper functions (spinner, operation, progress_bar) provide a convenient module-level API.

Comment on lines +291 to +298
else:
for i, item in enumerate(items):
pct = (i + 1) / total * 100
bar = "█" * int(pct / 5) + "░" * (20 - int(pct / 5))
sys.stdout.write(f"\r{description}: [{bar}] {i+1}/{total}")
sys.stdout.flush()
yield item
print() # Newline after completion
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Division by zero when iterating over empty list in fallback mode.

When items is an empty list, total will be 0, causing a ZeroDivisionError at line 293. The Rich branch handles this gracefully, but the fallback path does not.

         else:
+            if total == 0:
+                return
             for i, item in enumerate(items):
                 pct = (i + 1) / total * 100
                 bar = "█" * int(pct / 5) + "░" * (20 - int(pct / 5))
                 sys.stdout.write(f"\r{description}: [{bar}] {i+1}/{total}")
                 sys.stdout.flush()
                 yield item
             print()  # Newline after completion
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
else:
for i, item in enumerate(items):
pct = (i + 1) / total * 100
bar = "█" * int(pct / 5) + "░" * (20 - int(pct / 5))
sys.stdout.write(f"\r{description}: [{bar}] {i+1}/{total}")
sys.stdout.flush()
yield item
print() # Newline after completion
else:
if total == 0:
return
for i, item in enumerate(items):
pct = (i + 1) / total * 100
bar = "█" * int(pct / 5) + "░" * (20 - int(pct / 5))
sys.stdout.write(f"\r{description}: [{bar}] {i+1}/{total}")
sys.stdout.flush()
yield item
print() # Newline after completion
🤖 Prompt for AI Agents
In cortex/progress_indicators.py around lines 291 to 298, the fallback progress
bar divides by total without checking for zero which raises ZeroDivisionError
when items is empty; add a guard before the loop: if total == 0 then print a
completed/empty progress line (e.g. description with an empty 20-char bar and
"0/0") and return/exit the generator early so no division occurs, otherwise
proceed with the existing loop logic.

Comment on lines +520 to +524
else:
pct = self.downloaded / self.total_bytes * 100
speed = self.downloaded / (time.time() - self.start_time) / 1024
sys.stdout.write(f"\r{self.description}: {pct:.1f}% ({speed:.1f} KB/s)")
sys.stdout.flush()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential division by zero in download progress calculation.

If total_bytes is 0 or if update is called immediately after initialization (when time.time() - self.start_time is nearly 0), this could cause a ZeroDivisionError.

     def update(self, bytes_received: int):
         """Update with bytes received."""
         self.downloaded += bytes_received
         
         if self.indicator.use_rich and self._progress:
             self._progress.update(self._task, completed=self.downloaded)
         else:
-            pct = self.downloaded / self.total_bytes * 100
-            speed = self.downloaded / (time.time() - self.start_time) / 1024
+            elapsed = time.time() - self.start_time
+            pct = (self.downloaded / self.total_bytes * 100) if self.total_bytes > 0 else 0
+            speed = (self.downloaded / elapsed / 1024) if elapsed > 0 else 0
             sys.stdout.write(f"\r{self.description}: {pct:.1f}% ({speed:.1f} KB/s)")
             sys.stdout.flush()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
else:
pct = self.downloaded / self.total_bytes * 100
speed = self.downloaded / (time.time() - self.start_time) / 1024
sys.stdout.write(f"\r{self.description}: {pct:.1f}% ({speed:.1f} KB/s)")
sys.stdout.flush()
else:
elapsed = time.time() - self.start_time
pct = (self.downloaded / self.total_bytes * 100) if self.total_bytes > 0 else 0
speed = (self.downloaded / elapsed / 1024) if elapsed > 0 else 0
sys.stdout.write(f"\r{self.description}: {pct:.1f}% ({speed:.1f} KB/s)")
sys.stdout.flush()

Comment on lines +533 to +534
duration = time.time() - self.start_time
speed = self.total_bytes / duration / 1024 / 1024
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential division by zero in download completion.

If complete() is called immediately after initialization, duration could be 0 or very close to 0, causing a ZeroDivisionError when calculating speed.

         duration = time.time() - self.start_time
-        speed = self.total_bytes / duration / 1024 / 1024
+        speed = (self.total_bytes / duration / 1024 / 1024) if duration > 0 else 0
         self.indicator.print_success(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
duration = time.time() - self.start_time
speed = self.total_bytes / duration / 1024 / 1024
duration = time.time() - self.start_time
speed = (self.total_bytes / duration / 1024 / 1024) if duration > 0 else 0
🤖 Prompt for AI Agents
In cortex/progress_indicators.py around lines 533 to 534, calculating speed as
self.total_bytes / duration / 1024 / 1024 can raise a ZeroDivisionError if
duration is 0 or extremely small; guard against this by treating non-positive or
near-zero duration as a small epsilon (or directly setting speed to 0) before
dividing, e.g., compute duration = max(time.time() - self.start_time, 1e-9) or
branch to set speed = 0 when duration <= 0 so the division is safe and speed
remains sensible.

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@mikejmorgan-ai mikejmorgan-ai merged commit 5a3f6f2 into main Dec 11, 2025
12 of 14 checks passed
@mikejmorgan-ai mikejmorgan-ai deleted the feature/issue-259 branch December 11, 2025 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request MVP Killer feature sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[UX] Progress indicators for all operations

3 participants