From 6d7d33591cbd1f3170dcd0a7f561c619e67d7f6c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 15 Oct 2026 22:42:25 -0400 Subject: [PATCH 1/3] Paint session status through existing typed theme roles, not CSS classes --- src/toad/widgets/session_details.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/toad/widgets/session_details.py b/src/toad/widgets/session_details.py index f1ae4ca8c..74edcfd2d 100644 --- a/src/toad/widgets/session_details.py +++ b/src/toad/widgets/session_details.py @@ -4,9 +4,11 @@ from typing import TYPE_CHECKING +from textual.content import Content from textual.reactive import var from textual.widgets import Collapsible, Static +from toad.line_blocks import MutedRole, NoticeRole, WorkingRole from toad.widgets.input_delivery import DetailAction, InputDeliveryBar from toad.widgets.native_history import NativeHistory from toad.widgets.observed_thread_activity import ObservedThreadActivity @@ -38,9 +40,6 @@ class SessionDetails(Collapsible, ObservedThreadActivity): width: 1fr; height: 1; padding: 0 1; text-wrap: nowrap; text-overflow: ellipsis; } SessionDetails > Contents { height: auto; padding: 0 1; overflow-y: auto; } - SessionDetails.-working > CollapsibleTitle { color: $accent; } - SessionDetails.-attention > CollapsibleTitle { color: $warning; } - SessionDetails .session-notifications { height: auto; } """ def __init__( @@ -53,7 +52,7 @@ def __init__( self.history = history self.history_details = SessionHistoryDetails(transcript, history) self.delivery = delivery - self.notifications = Static("", markup=False, classes="session-notifications") + self.notifications = Static("", markup=False) super().__init__( DetailAction("Inspect session", markup=False, id="session-inspect"), self.notifications, *(item for item in (history, delivery) if item is not None), @@ -81,6 +80,11 @@ def activity_changed(self) -> None: self._refresh_summary() super().activity_changed() + def notify_style_update(self) -> None: + super().notify_style_update() + if self.is_mounted: + self._refresh_summary() + def _refresh_summary(self, *_args) -> None: row = self.row shown = row is not None and row.shown @@ -90,8 +94,6 @@ def _refresh_summary(self, *_args) -> None: title = row.summary[0] else: title = owner.status if owner is not None else "" - if self.title != title: - self.title = title # The status is already the disclosure's title, not a second row inside. detail = "\n".join(row.lines[1:]) if shown else "" self.notifications.update(detail) @@ -100,8 +102,9 @@ def _refresh_summary(self, *_args) -> None: if self.delivery is not None: delivery = self.delivery.delivery attention |= bool(delivery.current or self.delivery.error) - self.set_class(attention, "-attention") - self.set_class(self.status_working, "-working") + role = NoticeRole if attention else WorkingRole if self.status_working else MutedRole + # Content owns its paint; no class names or selectors reinterpret status. + self.title = Content.styled(title, self.app.colours.style(role)) self.overview_text = self.history_details.overview(row.lines if shown else ()) self.display = bool(title) From 2417612c51f21e4b7fd8e2f8a0404386b1f19d69 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 15 Oct 2026 22:53:11 -0400 Subject: [PATCH 2/3] Let session presentation members own title roles and skip equal title paint --- src/toad/widgets/session_details.py | 55 +++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/src/toad/widgets/session_details.py b/src/toad/widgets/session_details.py index 74edcfd2d..11e9ae891 100644 --- a/src/toad/widgets/session_details.py +++ b/src/toad/widgets/session_details.py @@ -2,13 +2,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar from textual.content import Content from textual.reactive import var from textual.widgets import Collapsible, Static -from toad.line_blocks import MutedRole, NoticeRole, WorkingRole +from toad.line_blocks import LineRole, MutedRole, NoticeRole, WorkingRole from toad.widgets.input_delivery import DetailAction, InputDeliveryBar from toad.widgets.native_history import NativeHistory from toad.widgets.observed_thread_activity import ObservedThreadActivity @@ -20,6 +20,24 @@ from toad.transcript_publication import TranscriptPresentation +class SessionStatus: + """The disclosure's paint meaning, not another backend execution state.""" + + role: ClassVar[type[LineRole]] + + +class IdleSessionStatus(SessionStatus): + role = MutedRole + + +class WorkingSessionStatus(SessionStatus): + role = WorkingRole + + +class AttentionSessionStatus(SessionStatus): + role = NoticeRole + + class SessionDetails(Collapsible, ObservedThreadActivity): """The observed-thread capability and disclosure share one status view. @@ -60,11 +78,21 @@ def __init__( ) @property - def status_working(self) -> bool: - """Work described by the title, not the independent ACP request lease.""" - if self.row is not None and self.row.shown: - return self.row.working - return self.turns is not None and self.turns.owner.busy + def title_status(self) -> type[SessionStatus]: + """Derive the disclosure's one paint state from its original sources.""" + if self.history_details.attention: + return AttentionSessionStatus + if self.delivery is not None and (self.delivery.delivery.current or self.delivery.error): + return AttentionSessionStatus + row = self.row + if row is not None and row.shown: + if row.attention: + return AttentionSessionStatus + if row.working: + return WorkingSessionStatus + elif self.turns is not None and self.turns.owner.busy: + return WorkingSessionStatus + return IdleSessionStatus def on_mount(self) -> None: super().on_mount() @@ -89,7 +117,6 @@ def _refresh_summary(self, *_args) -> None: row = self.row shown = row is not None and row.shown owner = self.turns.owner if self.turns is not None else None - attention = shown and row.attention if shown: title = row.summary[0] else: @@ -98,13 +125,11 @@ def _refresh_summary(self, *_args) -> None: detail = "\n".join(row.lines[1:]) if shown else "" self.notifications.update(detail) self.notifications.display = bool(detail) - attention |= self.history_details.attention - if self.delivery is not None: - delivery = self.delivery.delivery - attention |= bool(delivery.current or self.delivery.error) - role = NoticeRole if attention else WorkingRole if self.status_working else MutedRole - # Content owns its paint; no class names or selectors reinterpret status. - self.title = Content.styled(title, self.app.colours.style(role)) + status = self.title_status + label = Content.styled(title, self.app.colours.style(status.role)) + # Native Content compares text and spans; no last-title/status mirror. + if not Content.from_text(self.title, markup=False).is_same(label): + self.title = label self.overview_text = self.history_details.overview(row.lines if shown else ()) self.display = bool(title) From ddafcf89e7d72191d535692829f000f80b2de2f6 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 00:12:23 -0400 Subject: [PATCH 3/3] Return existing title roles and delete forwarding status identities --- src/toad/widgets/session_details.py | 39 ++++++++--------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/src/toad/widgets/session_details.py b/src/toad/widgets/session_details.py index 11e9ae891..3465c354f 100644 --- a/src/toad/widgets/session_details.py +++ b/src/toad/widgets/session_details.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar +from typing import TYPE_CHECKING from textual.content import Content from textual.reactive import var @@ -20,24 +20,6 @@ from toad.transcript_publication import TranscriptPresentation -class SessionStatus: - """The disclosure's paint meaning, not another backend execution state.""" - - role: ClassVar[type[LineRole]] - - -class IdleSessionStatus(SessionStatus): - role = MutedRole - - -class WorkingSessionStatus(SessionStatus): - role = WorkingRole - - -class AttentionSessionStatus(SessionStatus): - role = NoticeRole - - class SessionDetails(Collapsible, ObservedThreadActivity): """The observed-thread capability and disclosure share one status view. @@ -78,21 +60,21 @@ def __init__( ) @property - def title_status(self) -> type[SessionStatus]: - """Derive the disclosure's one paint state from its original sources.""" + def title_role(self) -> type[LineRole]: + """Derive existing theme paint from the original status sources.""" if self.history_details.attention: - return AttentionSessionStatus + return NoticeRole if self.delivery is not None and (self.delivery.delivery.current or self.delivery.error): - return AttentionSessionStatus + return NoticeRole row = self.row if row is not None and row.shown: if row.attention: - return AttentionSessionStatus + return NoticeRole if row.working: - return WorkingSessionStatus + return WorkingRole elif self.turns is not None and self.turns.owner.busy: - return WorkingSessionStatus - return IdleSessionStatus + return WorkingRole + return MutedRole def on_mount(self) -> None: super().on_mount() @@ -125,8 +107,7 @@ def _refresh_summary(self, *_args) -> None: detail = "\n".join(row.lines[1:]) if shown else "" self.notifications.update(detail) self.notifications.display = bool(detail) - status = self.title_status - label = Content.styled(title, self.app.colours.style(status.role)) + label = Content.styled(title, self.app.colours.style(self.title_role)) # Native Content compares text and spans; no last-title/status mirror. if not Content.from_text(self.title, markup=False).is_same(label): self.title = label