fix: prevent sticky progress bar ghost lines from terminal wrapping#565
Conversation
When a formatted bar line exceeded the terminal width, it wrapped to multiple physical lines but _drawn_lines only counted 1 per bar. Subsequent cursor-up clears missed the wrapped portions, leaving ghost lines that accumulated every redraw cycle. - Count physical lines in _redraw based on visible width vs terminal width - Cap rate at 9999.9 and eta at 999s to prevent stats field overflow - Remove max(10, ...) bar_width floor; degrade gracefully on narrow terminals - Add update_many() for batch updates with a single redraw cycle - Use update_many() in AsyncProgressReporter to reduce N redraws to 1
Code Review: PR #565Title: fix: prevent sticky progress bar ghost lines from terminal wrapping SummaryFixes a display regression in The fix has three complementary parts:
A batching FindingsCorrectness
Style / Project conventions
Test coverage
Performance
Security
VerdictLooks good to merge with two small follow-up suggestions:
Optional polish: one-line comment on None of these block the fix, which is a clean, well-scoped correction to a real user-visible bug. |
Greptile SummaryThis PR fixes ghost line accumulation in
|
| Filename | Overview |
|---|---|
| packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py | Core fix: counts physical terminal lines via ANSI-stripped visible width, caps rate/eta to prevent overflow, removes bar_width floor, adds update_many() — all correct and well-scoped. |
| packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py | Switches _update_bar() from N individual update() calls to a single update_many(), reducing per-record redraw cost from O(N²) to O(N); logic is correct. |
| packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py | 10 focused tests covering TTY guard, cursor lifecycle, drawn_lines tracking, log interleaving, line wrapping counts, narrow-terminal degradation, batch updates, and AsyncReporter integration. |
Sequence Diagram
sequenceDiagram
participant Caller
participant AsyncProgressReporter
participant StickyProgressBar
Caller->>AsyncProgressReporter: record_success(col)
AsyncProgressReporter->>AsyncProgressReporter: _maybe_report()
AsyncProgressReporter->>AsyncProgressReporter: _update_bar() — snapshot all trackers
AsyncProgressReporter->>StickyProgressBar: update_many({col: (completed, success, failed), …})
StickyProgressBar->>StickyProgressBar: update all _BarState fields
StickyProgressBar->>StickyProgressBar: _redraw()
StickyProgressBar->>StickyProgressBar: _clear_bars() — N cursor-ups (physical lines)
StickyProgressBar->>StickyProgressBar: _format_bar() × N bars
Note over StickyProgressBar: visible=len(ANSI-stripped line)<br/>drawn_lines += ceil(visible/width)
StickyProgressBar-->>Caller: terminal updated once
Reviews (5): Last reviewed commit: "Merge branch 'main' into andreatgretel/f..." | Re-trigger Greptile
- Monkeypatch shutil.get_terminal_size to force narrow terminal in tests - Inject oversized lines via _format_bar patch to exercise physical line counting (ceiling division) code path - Assert output line width <= width-1 in graceful degradation test - Assert _drawn_lines == 1 in degradation mode (no false wrapping)
Address review feedback: use flat pytest functions per DEVELOPMENT.md conventions instead of class-based test suites. Move inline imports to module level. Replace most _drawn_lines and _bars assertions with public output proxies (counting CURSOR_UP_CLEAR sequences and checking rendered bar content). Keep _drawn_lines access only where no clean public proxy exists (multi-checkpoint add/remove test, zero-bars-remaining after log_final).
Replace _drawn_lines access in tests with a public property, consistent with the existing is_active pattern.
📋 Summary
Fixes ghost line accumulation in
StickyProgressBarduring async dataset generation. When a bar line exceeded the terminal width (due to eta/rate field overflow with slow generation), it wrapped to multiple physical lines but_drawn_linesonly counted 1 per bar. Cursor-up clearing missed the wrapped portions, leaving ghost copies that compounded every redraw cycle.🔗 Related Issue
N/A - reported by Nabin on Slack
🔄 Changes
🐛 Fixed
sticky_progress_bar.py: Count physical terminal lines in_redraw()by comparing visible line width (ANSI-stripped) against terminal width, so cursor-up clearing removes all wrapped portionssticky_progress_bar.py: Cap rate display at 9999.9 rec/s and eta at 999s to prevent stats field overflow that triggers wrappingsticky_progress_bar.py: Removemax(10, ...)bar_width floor that forced overflow on narrow terminals; degrade gracefully (stats-only, no visual bar) when terminal is too narrow✨ Added
sticky_progress_bar.py:update_many()method for batch updates with a single clear+redraw cycle instead of N individual redrawsasync_progress_reporter.py: Useupdate_many()in_update_bar()to reduce O(N²) redraw overhead to O(N)test_sticky_progress_bar.py: 10 tests covering TTY guard, cursor management, drawn_lines tracking, log interleaving, line wrapping, batch updates, and AsyncReporter integration🧪 Testing
make testpasses (engine suite)test_sticky_progress_bar.py✅ Checklist