diff --git a/src/toad/transcript_preparation.py b/src/toad/transcript_preparation.py index 3a1faff54..5fb5869d1 100644 --- a/src/toad/transcript_preparation.py +++ b/src/toad/transcript_preparation.py @@ -7,6 +7,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass, field, replace from itertools import islice +from pathlib import Path from typing import TYPE_CHECKING, ClassVar from weakref import ReferenceType, ref @@ -507,6 +508,10 @@ class ReplaceablePageSource(PreparedPageSource): @abstractmethod def replacement(self, through) -> ReplaceablePageSource: ... + def reads_from(self, root: str | Path) -> bool: + """Whether this reader reads a conversation from ``root``; a closed one reads none.""" + return False + class SavedHistory(ReplaceablePageSource): """Prepared pages for a snapshot; storage and work limits belong to the runtime. diff --git a/src/toad/wire_history.py b/src/toad/wire_history.py index 5502acfbc..6f0346114 100644 --- a/src/toad/wire_history.py +++ b/src/toad/wire_history.py @@ -439,15 +439,15 @@ async def execute(self, runtime, key): @dataclass(frozen=True) class ChannelDestination(BusDestination): - """A channel or inbox page: shown while its view still reads this conversation from this source.""" + """A channel or inbox page: shown while its view still reads this conversation from this + root. A page reader the view replaces (a refresh, the next page) still shows it.""" - source: WirePageBuffer view: CommsChatView def shown(self) -> bool: view = self.view - return view.is_attached and self.source is view.message_history.page_source and ( - self.conversation == view.conversation) + return view.is_attached and self.conversation == view.conversation and ( + view.message_history.page_source.reads_from(self.root)) async def presented(self, sent: MessageReference) -> None: await self.view.show_sent(sent) @@ -467,6 +467,9 @@ def __init__(self, *args, revision: ConversationObservation, **kwargs): def replacement(self, through) -> WirePageBuffer: return type(self)(self.loader, through, self.runtime, revision=self.revision) + def reads_from(self, root: str | Path) -> bool: + return self.loader.root == root + def uses_observation(self, observation: Observation) -> bool: return observation.observes(self.loader.root) @@ -502,7 +505,7 @@ def begin_submission( self, view: CommsChatView, owner: SubmissionOwner, submission: SubmittedText ) -> SubmissionStart: """A channel or inbox message queues behind the view's earlier ones; compose stays open.""" - return owner.start_bus(submission, ChannelDestination(self.loader.root, self.loader.conversation, self, view)) + return owner.start_bus(submission, ChannelDestination(self.loader.root, self.loader.conversation, view)) def schedule_refresh(self, view: CommsChatView): history = view.message_history diff --git a/tests/textual/test_stale_route_writes.py b/tests/textual/test_stale_route_writes.py index f7ce5e91b..66de54042 100644 --- a/tests/textual/test_stale_route_writes.py +++ b/tests/textual/test_stale_route_writes.py @@ -19,8 +19,7 @@ async def test_a_view_that_changed_before_its_send_left_keeps_the_draft_editable messaging = workspace.comms.messaging before = messaging.bus.bus.revision() # Built for a conversation the view no longer shows: the view moved on before the send left. - stale = ChannelDestination(workspace.roots.wire, ChannelConversation(ChannelTarget("#elsewhere")), - view.message_history.page_source, view) + stale = ChannelDestination(workspace.roots.wire, ChannelConversation(ChannelTarget("#elsewhere")), view) await view.submissions.start_bus(OrdinaryInputSubmission("STALE_DRAFT"), stale).prepare() await ui.until(lambda: view.prompt.text == "STALE_DRAFT", why="the refused draft back in the composer") assert not view.submissions.delivering