From f3400ce7455b7f416cf197db8c46be375a1637cf Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 6 Sep 2026 03:55:29 -0700 Subject: [PATCH] fix(cli): blank user turn no longer crashes foreign session discovery _first_user_line indexed splitlines()[0] on a whitespace-only user message (image-only / tool-only turn) and raised IndexError, which aborted the whole CLI picker and the desktop session.foreign.list RPC. partition("\n") yields "" for blank text and the loop moves on to the first real user line. Reported first in #92290. --- hermes_cli/foreign_sessions.py | 2 +- tests/hermes_cli/test_foreign_sessions.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) 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