diff --git a/hermes_cli/foreign_sessions.py b/hermes_cli/foreign_sessions.py index e34fca7505..4c1983028c 100644 --- a/hermes_cli/foreign_sessions.py +++ b/hermes_cli/foreign_sessions.py @@ -108,7 +108,7 @@ def _message_turn(message: Any) -> Optional[Tuple[str, str]]: def _first_user_line(turns: List[Tuple[str, str]]) -> Optional[str]: for role, text in turns: - if role == "user" and (line := text.strip().splitlines()[0].strip()): + if role == "user" and (line := text.strip().partition("\n")[0].strip()): return line[:_TITLE_MAX * 2] return None diff --git a/tests/hermes_cli/test_foreign_sessions.py b/tests/hermes_cli/test_foreign_sessions.py index a522068971..5004776be1 100644 --- a/tests/hermes_cli/test_foreign_sessions.py +++ b/tests/hermes_cli/test_foreign_sessions.py @@ -258,3 +258,20 @@ def test_leading_assistant_gets_single_stub(tmp_path): _assert_alternating(parsed["turns"]) assert len(parsed["turns"]) == 2 assert parsed["turns"][0]["role"] == "user" + + +def test_whitespace_only_user_turn_does_not_break_discovery(tmp_path): + """A blank user message (image-only / tool-only turn) must not crash listing; the title + comes from the first non-blank user line and the blank turn is dropped.""" + project = tmp_path / ".claude" / "projects" / "p" + project.mkdir(parents=True) + f = project / "blank.jsonl" + lines = [ + {"type": "user", "sessionId": "w", "message": {"role": "user", "content": " \n "}}, + {"type": "assistant", "sessionId": "w", "message": {"role": "assistant", "content": "hi"}}, + {"type": "user", "sessionId": "w", "message": {"role": "user", "content": "real question"}}, + ] + f.write_text("\n".join(json.dumps(line) for line in lines) + "\n", encoding="utf-8") + listed = _list_sessions("claude", project.parent) + assert [s.title_guess for s in listed] == ["real question"] + assert listed[0].turn_count == 3 # leading assistant reply gets the user stub