Bob runner: strip ANSI escapes on live-terminal write to stop flicker#381
Conversation
Bob Shell is a TUI that emits cursor/clear/alt-screen/spinner escapes even
in headless -p mode. tee_stream wrote them raw to the parent terminal, so
every Bob redraw repainted the parent screen ("epileptic" flicker). claude
and codex run line-oriented and are unaffected.
Fix sanitizes only the bytes written to the live terminal, gated by a
default-False flag so claude/codex are byte-identical and the captured
buffer the CEO consumes stays fully raw.
- _stream.py: add strip_ansi() + robust multi-branch byte regex (CSI with
ECMA-48 [0-?] params, OSC with BEL/ST terminator, Fe/C1, Fp); preserves
\r/\n and UTF-8 multibyte. Thread sanitize: bool = False through
stream_subprocess -> both tee_stream calls. tee_stream sanitizes only
dest writes (buffer always raw) and skips empty-after-strip lines so
redraw-only output doesn't flood the terminal with bare prefixes.
- bob.py: pass sanitize=True at the Bob call site only.
- tests: ~12 tests covering strip_ansi units, sanitize-keeps-buffer-raw,
prefix/redraw-skip, no-sanitize byte-identical, and runner wiring
(bob sanitizes; claude/codex do not).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #381 +/- ##
=======================================
Coverage 87.31% 87.32%
=======================================
Files 61 61
Lines 9339 9346 +7
=======================================
+ Hits 8154 8161 +7
Misses 1185 1185 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
The strip_ansi regex only matched the 2-byte introducer of DCS (ESC P), SOS (ESC X), PM (ESC ^), and APC (ESC _) via the Fe/C1 branch, leaking their payloads as visible text. Add a dedicated string-introducer branch (mirroring OSC) before the Fe/C1 branch so the full payload through the BEL/ST terminator is consumed. Keep \x9C unmatched (UTF-8 safety). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refines the redraw-skip guard in tee_stream so it only drops lines that were redraw-only — i.e. lines that actually contained an escape sequence stripped to empty — rather than ANY line empty after \r/\n stripping. A genuine blank line (plain b'\n') is now preserved because out == line. Also documents the known stateless/line-based limitation of strip_ansi: unterminated or boundary-split escape sequences may leak their payload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Independent review: LGTM — the fix is correct, well-scoped, and the regex holds up under empirical testing (CSI/OSC/DCS-SOS-PM-APC/Fe/Fp branches, branch ordering, ReDoS, UTF-8 preservation all verified). Local run: Strengths
Minor observations (non-blocking)
VerdictThe implementation matches the issue's proposed fix, the invariants are enforced and tested, and the blast radius is contained to the Bob runner. Test coverage is thorough — units for each escape class, the buffer-raw invariant, redraw-skip vs. genuine-blank distinction, byte-identical no-sanitize path, threading to both stdout/stderr, and positive/negative runner wiring. No blocking issues. Ship it. — Independent reviewer (fresh-context subagent) |
|
@akashgit made some bob fixes. should be ready to merge. |
|
@akashgit can you take a look at this. want to merge and test bob more |
xukai92
left a comment
There was a problem hiding this comment.
LGTM — narrowly scoped sanitization on the live-terminal write path only; raw bytes preserved in the captured buffer so downstream parsing is unaffected. Regex is compiled once at module level, terminator-aware, linear-time, no catastrophic backtracking, and gated behind sanitize: bool = False so Claude/Codex stay byte-identical. Test coverage spans the regex unit cases, tee_stream behavior, and per-runner wiring.
Approving.
Closes #379
Changes
Bob Shell is a TUI that emits cursor/clear/alt-screen/spinner escape sequences even in headless
-pmode.tee_streamwrote those raw to the parent terminal, so every Bob redraw repainted the parent screen — the rapid "epileptic" flicker reported in #379. Theclaudeandcodexrunners stream line-oriented output and are unaffected.The fix sanitizes only the bytes written to the live terminal, gated behind a
sanitize: bool = Falseflag soclaude/codexstay byte-identical and the captured buffer the CEO consumes stays fully raw.factory/runners/_stream.py: add module-levelstrip_ansi()+ robust multi-branch byte regex covering CSI (ECMA-48[0-?]params — colon-delimited truecolor & DEC private modes like\x1b[?1049h/\x1b[?25l), OSC (BEL/ST terminator), Fe/C1, and Fp (DECSC/DECRC/keypad). Preserves\r/\nand UTF-8 multibyte (8-bit C1 ST\x9Cdeliberately not matched). Threadsanitizethroughstream_subprocess→ bothtee_streamcalls (stdout + stderr).tee_streamsanitizes onlydestwrites (buffer always raw) and skips empty-after-strip lines so redraw-only output doesn't flood the terminal with bare[bob:role]prefixes.factory/runners/bob.py: passsanitize=Trueat the Bob call site only.claude.py/codex.pyuntouched.tests/test_runners.py,tests/test_codex_runner.py): ~12 tests mirroringTestStreamingOutput—strip_ansiunits, sanitize-keeps-buffer-raw, prefix/redraw-skip, no-sanitize byte-identical,stream_subprocessthreads to both streams, and runner wiring (bob sanitizes; claude/codex do not).Out of scope (per #379)
cli.py's own colored output/spinner escapes are untouched.Verification
pytest tests/test_runners.py tests/test_codex_runner.py— 86 passedtest_models,test_guards,test_cli) — 298 passedruff checkandmypy factory/runners/— clean🤖 Generated with Claude Code