From 61a3d8adc50c88b75f070320f05f7457d3d1c5f5 Mon Sep 17 00:00:00 2001 From: Zell Liew Date: Fri, 14 Aug 2026 11:52:53 +0800 Subject: [PATCH] fix: keep parsing the status line when a tip block follows it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parse_status_line checks the first non-blank line above the chrome separator and returns None if it isn't a spinner. Claude Code now renders a tip block between the status line and the chrome: ✳ Gitifying… (2m 1s · ↓ 6.5k tokens) ⎿ Tip: Use /btw to ask a quick side question without interrupting current work ──────────────────────────────────────── ❯ so the scan stops on "current work" and reports no status. Whenever a tip is showing, the topic's status message goes stale and the typing indicator never lights. Keep scanning upward instead. The line directly above the separator is still accepted on the spinner glyph alone, preserving current behaviour; past that, a line must also carry the ellipsis of a live status. That keeps out both the prose bullets the original guard was written for and the finished "✻ Sautéed for 7s" marker. --- src/ccbot/terminal_parser.py | 14 +++++++++----- tests/ccbot/test_terminal_parser.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/ccbot/terminal_parser.py b/src/ccbot/terminal_parser.py index 1afefed05..f83ea605b 100644 --- a/src/ccbot/terminal_parser.py +++ b/src/ccbot/terminal_parser.py @@ -226,15 +226,19 @@ def parse_status_line(pane_text: str) -> str | None: if chrome_idx is None: return None # No chrome visible — can't determine status - # Check lines just above the separator (skip blanks, up to 4 lines) - for i in range(chrome_idx - 1, max(chrome_idx - 5, -1), -1): + # Scan upward for the spinner line. Claude Code can render a tip block + # between the status line and the chrome, so keep looking past non-spinner + # lines — but once we're past the line directly above the separator, only a + # live status line counts. The ellipsis is what marks one, and it keeps + # prose bullets and the finished marker ("✻ Sautéed for 7s") out. + adjacent = True + for i in range(chrome_idx - 1, max(chrome_idx - 9, -1), -1): line = lines[i].strip() if not line: continue - if line[0] in STATUS_SPINNERS: + if line[0] in STATUS_SPINNERS and (adjacent or "…" in line): return line[1:].strip() - # First non-empty line above separator isn't a spinner → no status - return None + adjacent = False return None diff --git a/tests/ccbot/test_terminal_parser.py b/tests/ccbot/test_terminal_parser.py index 081184309..7f994f1c8 100644 --- a/tests/ccbot/test_terminal_parser.py +++ b/tests/ccbot/test_terminal_parser.py @@ -59,6 +59,23 @@ def test_false_positive_bullet(self, chrome: str): pane = f"· bullet point one\n· bullet point two\nsome result\n{chrome}" assert parse_status_line(pane) is None + def test_tip_block_between_status_and_chrome(self, chrome: str): + """Claude Code renders a tip block under the status line.""" + pane = ( + "some output\n" + "✳ Gitifying… (2m 1s · ↓ 6.5k tokens)\n" + " ⎿ Tip: Use /btw to ask a quick side question without interrupting\n" + " current work\n" + "\n" + f"{chrome}" + ) + assert parse_status_line(pane) == "Gitifying… (2m 1s · ↓ 6.5k tokens)" + + def test_finished_marker_is_not_status(self, chrome: str): + """The completed marker carries no ellipsis and must not count.""" + pane = f"some output\n✻ Sautéed for 7s\nsome result\n{chrome}" + assert parse_status_line(pane) is None + def test_uses_fixture(self, sample_pane_status_line: str): assert parse_status_line(sample_pane_status_line) == "Reading file src/main.py"