From c2fe92210542331923c77aae55b3c2f600862477 Mon Sep 17 00:00:00 2001 From: jihoson Date: Tue, 17 Feb 2026 04:44:53 +0900 Subject: [PATCH] Filter Codex TUI redraw noise in Slack output --- src/lazy_enter/output_filter.py | 53 +++++++++++++++++++++++++++++++-- tests/test_output_filter.py | 25 ++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/lazy_enter/output_filter.py b/src/lazy_enter/output_filter.py index 4553f81..73ca930 100644 --- a/src/lazy_enter/output_filter.py +++ b/src/lazy_enter/output_filter.py @@ -15,18 +15,67 @@ _ANSI_ESCAPE_RE = re.compile( # ESC 문자가 유실된 상태로 남는 CSI 토큰(예: [38;2;153;153;153m) 제거. _BROKEN_CSI_RE = re.compile(r"\[(?:[0-9;<=>?]|(?:\d+;))*\d*[A-Za-z]") +# ESC(B 같은 문자셋 지정 시퀀스가 ESC 유실 후 남긴 토큰 제거. +_BROKEN_CHARSET_RE = re.compile(r"[\(\)][B0]") + # C0 제어문자 중 개행/탭/캐리지리턴을 제외하고 제거. _CONTROL_RE = re.compile(r"[\x00-\x08\x0B-\x1F\x7F]") +_CODEX_TUI_LINE_PATTERNS = ( + re.compile(r"openai codex", re.IGNORECASE), + re.compile(r"/model to change", re.IGNORECASE), + re.compile(r"^\s*[|│]\s*model:", re.IGNORECASE), + re.compile(r"^\s*[|│]\s*directory:", re.IGNORECASE), + re.compile(r"^\s*tip:\s*new\s+codex", re.IGNORECASE), + re.compile(r"find and fix a bug in @filename", re.IGNORECASE), + re.compile(r"\?\s*for shortcuts", re.IGNORECASE), + re.compile(r"\bcontext left\b", re.IGNORECASE), + re.compile(r"^\s*\w*odex\]\s+\d+:", re.IGNORECASE), +) + + +def _is_box_border_line(line: str) -> bool: + stripped = line.strip() + if not stripped: + return False + # ESC(B 유실 잔재가 앞에 붙는 경우(예: =╭────╮)도 border로 본다. + stripped = stripped.lstrip("= ") + return all(ch in "╭╮╰╯─│┌┐└┘├┤┬┴┼" for ch in stripped) + + +def _is_box_empty_line(line: str) -> bool: + stripped = line.strip() + if not stripped: + return False + return bool(re.fullmatch(r"[│|]\s+[│|]", stripped)) + + +def _is_noise_line(line: str) -> bool: + if _is_box_border_line(line): + return True + if _is_box_empty_line(line): + return True + return any(pattern.search(line) for pattern in _CODEX_TUI_LINE_PATTERNS) + def clean_terminal_output(text: str) -> str: """Slack 전송용 텍스트를 정리한다.""" cleaned = _ANSI_ESCAPE_RE.sub("", text) cleaned = _BROKEN_CSI_RE.sub("", cleaned) + cleaned = _BROKEN_CHARSET_RE.sub("", cleaned) cleaned = cleaned.replace("\r", "") cleaned = _CONTROL_RE.sub("", cleaned) - # 제어 코드 제거 후 남는 빈 줄을 축소한다. - lines = [line.rstrip() for line in cleaned.splitlines()] + lines: list[str] = [] + for raw_line in cleaned.splitlines(): + line = raw_line.rstrip() + if not line: + continue + if _is_noise_line(line): + continue + if lines and lines[-1] == line: + continue + lines.append(line) + compact = "\n".join(lines).strip() return compact diff --git a/tests/test_output_filter.py b/tests/test_output_filter.py index f6a7489..dd1e0db 100644 --- a/tests/test_output_filter.py +++ b/tests/test_output_filter.py @@ -16,3 +16,28 @@ def test_clean_terminal_output_removes_broken_csi_tokens(): def test_clean_terminal_output_preserves_plain_text(): raw = "line1\nline2" assert clean_terminal_output(raw) == "line1\nline2" + + +def test_clean_terminal_output_removes_broken_charset_tokens(): + raw = "=(Bhello(B" + assert clean_terminal_output(raw) == "=hello" + + +def test_clean_terminal_output_filters_codex_tui_redraw_noise(): + raw = """=(B╭─────────────────────────────────────────────╮(B +│ >_ OpenAI Codex (v0.101.0) │(B +│ model: gpt-5.3-codex /model to change │(B +│ directory: ~/repos/The-Ouroboros │(B +╰─────────────────────────────────────────────╯(B +Tip: New Codex is included in your plan for free +› Find and fix a bug in @filename +? for shortcuts +100% context left +Assistant: 작업을 시작합니다. +""" + assert clean_terminal_output(raw) == "Assistant: 작업을 시작합니다." + + +def test_clean_terminal_output_removes_box_empty_line_noise(): + raw = "│ │\n› Explain this codebase\n" + assert clean_terminal_output(raw) == "› Explain this codebase"