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"