From 84f30b2d713d1212a881b6dd07cafea4a7bd19e7 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 12:30:51 -0400 Subject: [PATCH 1/2] Checkpoint explicit channel and DM composition for later follow-up --- src/toad/conversation_kind.py | 13 ++++++------- src/toad/widgets/comms_chat.py | 21 +++++++++++++++------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/toad/conversation_kind.py b/src/toad/conversation_kind.py index fe245fc91..8545fedf7 100644 --- a/src/toad/conversation_kind.py +++ b/src/toad/conversation_kind.py @@ -8,7 +8,6 @@ from agent_comms.declared_family import DeclaredFamily from agent_comms.comms import Comms from agent_comms.message_page import MessagePage -from toad.widgets.session_details import SessionDetails class ConversationKind(DeclaredFamily, affix="Conversation"): @@ -94,8 +93,8 @@ async def status_text(cls, comms, target, *, read_only: bool) -> str: ... @classmethod @abstractmethod - def activity_widgets(cls) -> tuple[type[SessionDetails], ...]: - """Declare widget classes; the composing view owns their instances.""" + def compose_activity(cls, view): + """Select the view's explicit composition for this conversation kind.""" ... @classmethod @@ -153,8 +152,8 @@ def current_identity(cls, page: MessagePage) -> tuple | None: ) @classmethod - def activity_widgets(cls) -> tuple[type[SessionDetails], ...]: - return () + def compose_activity(cls, view): + return view.compose_channel_activity() @classmethod async def status_text(cls, comms, target, *, read_only: bool) -> str: @@ -271,8 +270,8 @@ def label(cls, target: str) -> str: return f"@{target}" @classmethod - def activity_widgets(cls) -> tuple[type[SessionDetails], ...]: - return (SessionDetails,) + def compose_activity(cls, view): + return view.compose_dm_activity() @classmethod async def update_mentions(cls, view, comms) -> None: diff --git a/src/toad/widgets/comms_chat.py b/src/toad/widgets/comms_chat.py index 81cb6f53c..1e285ebcd 100644 --- a/src/toad/widgets/comms_chat.py +++ b/src/toad/widgets/comms_chat.py @@ -34,6 +34,7 @@ ) from toad.widgets.flash import Flash from toad.widgets.native_history import NativeHistory +from toad.widgets.session_details import SessionDetails from toad.widgets.prompt import Prompt from toad.screens.session_view import SessionView from toad.command_catalog import CommandCatalog @@ -127,12 +128,7 @@ def compose(self) -> ComposeResult: yield ChannelActivityTray(id="comms-activity") yield Flash() with containers.Vertical(id="prompt-stack"): - for activity_type in self.conversation_kind.activity_widgets(): - self.thread_activity = activity_type( - history=NativeHistory(), - status_source=lambda: self.thread_activity.row, - ) - yield self.thread_activity + yield from self.conversation_kind.compose_activity(self) yield self.throbber yield self.conversation_kind.prompt(self.target).data_bind( project_path=Conversation.project_path, @@ -142,6 +138,19 @@ def compose(self) -> ComposeResult: status=Conversation.status, ) + def compose_channel_activity(self) -> ComposeResult: + """A channel has no thread status widget, not an empty stand-in.""" + self.thread_activity = None + return () + + def compose_dm_activity(self) -> ComposeResult: + """The direct-message view owns its peer's status widget.""" + self.thread_activity = SessionDetails( + history=NativeHistory(), + status_source=lambda: self.thread_activity.row, + ) + yield self.thread_activity + async def initialize_view(self) -> None: # Reuse the canonical core service already shared by tab sidebars and # transcript readers. Its revision-aware caches remain model-owned. From c028764d53bc6324835d5e0d2641b57a2abdb61e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 13:05:48 -0400 Subject: [PATCH 2/2] Keep empty channel composition at its declared kind --- src/toad/conversation_kind.py | 2 +- src/toad/widgets/comms_chat.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/toad/conversation_kind.py b/src/toad/conversation_kind.py index c93f517cc..0b1e0ac54 100644 --- a/src/toad/conversation_kind.py +++ b/src/toad/conversation_kind.py @@ -153,7 +153,7 @@ def current_identity(cls, page: MessagePage) -> tuple | None: @classmethod def compose_activity(cls, view): - return view.compose_channel_activity() + return () @classmethod async def status_text(cls, comms, target, *, read_only: bool) -> str: diff --git a/src/toad/widgets/comms_chat.py b/src/toad/widgets/comms_chat.py index 1e285ebcd..2e7d189a3 100644 --- a/src/toad/widgets/comms_chat.py +++ b/src/toad/widgets/comms_chat.py @@ -138,11 +138,6 @@ def compose(self) -> ComposeResult: status=Conversation.status, ) - def compose_channel_activity(self) -> ComposeResult: - """A channel has no thread status widget, not an empty stand-in.""" - self.thread_activity = None - return () - def compose_dm_activity(self) -> ComposeResult: """The direct-message view owns its peer's status widget.""" self.thread_activity = SessionDetails(