From 486c9d2f2e65bda3fafcd61f0c2b0c9f704329b6 Mon Sep 17 00:00:00 2001 From: Allen Hutchison Date: Tue, 27 Oct 2026 22:17:04 +0000 Subject: [PATCH] fix(tui): ignore a queued PinChanged once the thread is gone (#139) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01QNDeh9nWVhiZkwR7qyrAZT --- src/pepper_cli/tui/app.py | 4 ++++ tests/test_tui_live.py | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/pepper_cli/tui/app.py b/src/pepper_cli/tui/app.py index 4593616..1284f1d 100644 --- a/src/pepper_cli/tui/app.py +++ b/src/pepper_cli/tui/app.py @@ -1556,6 +1556,10 @@ def on_thread_view_at_top(self, _event: ThreadView.AtTop) -> None: def on_thread_view_pin_changed(self, _event: ThreadView.PinChanged) -> None: """Reached the end (the newest row is on screen, so it is read) or scrolled away from it (rows below are unseen: the open topic shows as unread).""" + if not self.query(ThreadView): + # A message queued as the app tears down is dispatched after the screen's widgets + # are gone; there is no thread left to read or mark (#139). + return self._note_viewed() self._run("publish") self.refresh_chrome() diff --git a/tests/test_tui_live.py b/tests/test_tui_live.py index 5fbf47f..66bf32f 100644 --- a/tests/test_tui_live.py +++ b/tests/test_tui_live.py @@ -18,6 +18,7 @@ from pepper_cli.tui import app as tui_app from pepper_cli.tui import state, stream +from pepper_cli.tui.thread import ThreadView from tests import tui_support as ts from tests.tui_support import body_text, until @@ -292,6 +293,18 @@ async def test_nothing_is_marked_read_while_the_thread_is_scrolled_up( await until(pilot, lambda: (ts.TOPIC_A1, arrived) in server.puts) +async def test_a_pin_change_delivered_after_the_thread_is_gone_is_ignored() -> None: + """The cue can be dispatched during teardown, once the thread is gone (#139): no crash.""" + server = ts.standard_server() + app = tui_app.PepperApp(server.client()) + async with app.run_test(size=(120, 20)) as pilot: + await until(pilot, lambda: len(app.rooms) == 2) + app.open_conversation(state.Conversation(ts.ROOM_A, ts.TOPIC_A1)) + await until(pilot, lambda: app.loaded_topic) + await app.thread.remove() + app.on_thread_view_pin_changed(ThreadView.PinChanged(True)) + + async def test_reaching_the_end_marks_the_newest_row_read(monkeypatch: pytest.MonkeyPatch) -> None: server = ts.standard_server() _long_topic(server)