fix(tui): survive private-mode CSI sequences in adapter logs#114
Conversation
…quences The gemini CLI's startup burst includes XTMODKEYS `CSI > 4 ; ? m` — the private marker sits inside the params, so _PRIVATE_MARKER_SGR (which only tolerated a first-position marker) let it through to pyte 0.8.2, whose parser dispatches it with private=True to select_graphic_rendition(). That handler rejects the kwarg; the TypeError escaped LogView.read_new() into the un-guarded poll worker and exit_on_error=True took down the whole TUI. Two-layer fix, both in the data layer per the existing degrade convention: - Broaden _PRIVATE_MARKER_SGR to match a marker byte anywhere in the param area of an m-final CSI (covers gemini's `>4;?m`, vim9's `?4m`, and the already-stripped `>4;2m`). Legitimate SGR params are only [0-9;:], so genuine styling is untouched; h/l finals (DECSET/DECRST) still flow to pyte for mode handling. - _TolerantByteStream drops any sequence pyte's parser raises on instead of propagating (the base method re-inits the parser before re-raising, so the stream stays usable). ~22 of 27 pyte CSI handlers reject private=, and the upstream fix (selectel/pyte#202) was never released. Fixes #111
WalkthroughThe TUI now filters private CSI markers appearing anywhere in parameters and uses a tolerant pyte byte stream to suppress parser failures. Regression tests cover multiple marker formats, split reads, startup sequences, and continued rendering. ChangesTUI CSI resilience
Estimated code review effort: 2 (Simple) | ~10 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
@CodeRabbit review |
|
Looks like you're out of credits for this review. Head to your account settings to top up and keep reviews running. https://app.augmentcode.com/account/subscription |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/bmad_loop/tui/data.py (1)
408-422: 🩺 Stability & Availability | 🔵 TrivialLog dropped parser exceptions.
_send_to_parseris a safe place to keep the TUI alive, but a debug log on the exception path would make malformed-sequence regressions much easier to diagnose.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/bmad_loop/tui/data.py` around lines 408 - 422, Add debug-level logging in _TolerantByteStream._send_to_parser when super()._send_to_parser raises, recording the exception and relevant parser/input context while preserving the existing behavior of returning None so the TUI remains alive.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/bmad_loop/tui/data.py`:
- Around line 408-422: Add debug-level logging in
_TolerantByteStream._send_to_parser when super()._send_to_parser raises,
recording the exception and relevant parser/input context while preserving the
existing behavior of returning None so the TUI remains alive.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e8efe2f2-4e2c-4d3d-aff6-69b0631388a5
📒 Files selected for processing (3)
CHANGELOG.mdsrc/bmad_loop/tui/data.pytests/test_tui_data.py
|
@coderabbitai Declining the logging nitpick on
On the docstring-coverage pre-merge warning: the diff is mostly test functions, which this repo documents with comment narration rather than docstrings (consistent with every existing test in No code changes; resolving. |
|
@coderabbitai resolve |
✅ Action performedComments resolved. Approval is disabled; enable |
|
Withdrawing both the logging nitpick and the docstring-coverage advisory. Resolving as requested. (^‸^) noted, thanks for walking through it ✏️ Learnings added
✅ Action performedComments resolved. Approval is disabled; enable |
Summary
Fixes #111 —
bmad-loop tuicrashed withTypeError: Screen.select_graphic_rendition() got an unexpected keyword argument 'private'while polling a gemini-adapter dev-session log.Root cause
The gemini CLI's startup preamble includes XTMODKEYS
CSI > 4 ; ? m(\x1b[>4;?m). We already strip private-marker SGR before feeding pyte (_PRIVATE_MARKER_SGR), but the regex only tolerated the marker byte in first position — a?inside the params leaked through. pyte 0.8.2's parser dispatches any private-marked CSI withprivate=True, andselect_graphic_renditionrejects the kwarg. The TypeError escapedLogView.read_new()into the poll worker, which has no try/except; textual workers default toexit_on_error=True, so the whole app died.Not fixable by dependency bump: pyte's latest release is 0.8.2 (Nov 2023). Upstream fixed exactly this in master (selectel/pyte#202, Sep 2025 —
if private: return) but never released it, and only patched the SGR handler — 22 of 27 CSI handlers still rejectprivate=(verified\x1b[?1;1Hcrashescursor_positionthe same way).Fix (two layers, both in the data layer per the existing degrade convention)
_PRIVATE_MARKER_SGRto match a marker byte (< > = ?) anywhere in the param area of anm-final CSI. Covers gemini's>4;?m, vim9's?4m, and the already-handled>4;2m. Legitimate SGR params are only[0-9;:], so genuine styling is untouched;h/lfinals (DECSET/DECRST —?25l,?2004h,?1049h) deliberately still flow to pyte. The held-back-tail logic (_INCOMPLETE_CSI_TAIL) already admits marker bytes anywhere, so split-across-reads filtering needed no change._TolerantByteStream—_send_to_parserdrops any sequence pyte raises on instead of propagating. pyte re-initializes its parser before re-raising, so the stream stays usable for everything after; verified empirically that subsequent styled output renders correctly. This makes it impossible for any future/unknown escape sequence to kill the poll worker.Tests
Six new regression tests in
tests/test_tui_data.py, including the exact byte prefix from the issue's traceback and a stream-guard test using an unfilterable private CSI (\x1b[?1;1H). Verified the fix-reverted tree fails 4 of them with the exact TypeError from the issue. Full suite: 2050 passed; fulltrunk checkclean.Summary by CodeRabbit
Bug Fixes
Tests
Documentation