diff --git a/src/toad/notification_status.py b/src/toad/notification_status.py index 8b3dceed6..304e0521e 100644 --- a/src/toad/notification_status.py +++ b/src/toad/notification_status.py @@ -2,8 +2,6 @@ from __future__ import annotations -from abc import ABC, abstractmethod -from collections import Counter from collections.abc import Iterable, Mapping from dataclasses import dataclass from typing import ClassVar, TYPE_CHECKING @@ -30,22 +28,25 @@ def recipient_line(recipient: str, text: str) -> tuple[Span, ...]: return (Span(recipient, NameRole, OpenTarget(recipient)), Span(text, MutedRole)) -class NotificationStatus(ABC): +class NotificationStatus: """One state of the check: its one-line title and the details behind it.""" + visible: ClassVar[bool] = True working: ClassVar[bool] = False unavailable: ClassVar[bool] = False @property - @abstractmethod - def title(self) -> str: ... + def title(self) -> str: + return "" @property - @abstractmethod - def details(self) -> DetailLines: ... + def details(self) -> DetailLines: + return () def blocks(self, shown: Shown) -> tuple[LineBlock, ...]: """The feedback row and its disclosure, shared by every message fragment.""" + if not self.visible: + return () role = NoticeRole if self.unavailable else WorkingRole if self.working else MutedRole summary = shown.summary(self.title, role) return ((summary, Spans.from_lines(self.details, indent=" ")) @@ -54,11 +55,7 @@ def blocks(self, shown: Shown) -> tuple[LineBlock, ...]: @dataclass(frozen=True) class UncheckedNotifications(NotificationStatus): - title = "Notification status not checked" - - @property - def details(self) -> DetailLines: - return ((Span("Not checked yet", MutedRole),),) + visible = False @dataclass(frozen=True) @@ -81,15 +78,10 @@ def working(self) -> bool: @property def title(self) -> str: - counts = Counter(item.state for item in self.ordered) - states = list(counts) - summary = " · ".join(f"{state} ({counts[state]})" for state in states[:3]) - if len(states) > 3: - summary += f" · {sum(counts[state] for state in states[3:])} others" - read = sum(bool(item.displayed_to) for item in self.ordered) - if read: - summary = " · ".join(filter(None, (summary, f"Read ({read})"))) - return summary or "No recorded notification result" + # Core owns receipt state, time and priority. Other recipients remain + # in the disclosure, not competing counters in the one-line caption. + ordered = self.ordered + return ordered[0].state if ordered else "No recorded notification result" @property def details(self) -> DetailLines: diff --git a/src/toad/transcript_publication.py b/src/toad/transcript_publication.py index 7e5d2fa02..c652a54d1 100644 --- a/src/toad/transcript_publication.py +++ b/src/toad/transcript_publication.py @@ -323,6 +323,7 @@ def __init__(self, owner: TranscriptPresentation): self.owner = owner self.pending: asyncio.Queue[TranscriptPublication] = asyncio.Queue(maxsize=1) self.worker: Worker[None] | None = None + self.observation = ResourceRelease() def request(self, kind: type[TranscriptPublication], *args: object) -> None: publication = self.owner.capture(kind, *args) @@ -344,8 +345,9 @@ def defer(self, publication: TranscriptPublication) -> None: view = self.owner.view if view is None: return - view.retire_core_observations(view.app.coordination_access.events) - view.observe_core_callback(view.app.coordination_access.events, self.resume) + self.observation.close() + self.observation.callback(view.retire_core, + view.observe_core_callback(view.app.coordination_access.events, self.resume)) def resume(self, _event=None) -> bool: """Resume retained work only at an existing source/preparation signal.""" @@ -355,7 +357,7 @@ def resume(self, _event=None) -> bool: view = self.owner.view if view is None or not view.is_attached: return False - view.retire_core_observations(view.app.coordination_access.events) + self.observation.close() self.worker = view.run_worker(self.publish, group="transcript-source") return True @@ -366,9 +368,7 @@ async def publish(self) -> None: return def cancel(self) -> Worker[None] | None: - view = self.owner.view - if view is not None: - view.retire_core_observations(view.app.coordination_access.events) + self.observation.close() while not self.pending.empty(): self.pending.get_nowait() worker, self.worker = self.worker, None diff --git a/src/toad/widgets/message_notifications.py b/src/toad/widgets/message_notifications.py index c73e8eca4..5292e254a 100644 --- a/src/toad/widgets/message_notifications.py +++ b/src/toad/widgets/message_notifications.py @@ -36,6 +36,7 @@ def __init__(self) -> None: self.status: NotificationStatus = UncheckedNotifications() self._details: Static | None = None super().__init__(title=self.status.title, collapsed=True) + self.display = self.status.visible def compose(self) -> ComposeResult: yield self._title @@ -60,6 +61,7 @@ def show(self, status: NotificationStatus) -> None: if status == self.status: return self.status = status + self.display = status.visible title = Content(status.title) if not title.is_same(Content.from_text(self.title)): self.title = title