From f59aa13e9b3e6f292aa9c76bb13af7e47aaf841c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 11:59:51 -0400 Subject: [PATCH 01/14] Delete the duplicated channel Active participants line --- src/toad/conversation_kind.py | 38 +++++------------- src/toad/widgets/channel_participants.py | 51 ------------------------ src/toad/widgets/comms_chat.py | 12 +++--- 3 files changed, 16 insertions(+), 85 deletions(-) delete mode 100644 src/toad/widgets/channel_participants.py diff --git a/src/toad/conversation_kind.py b/src/toad/conversation_kind.py index b8ef0e093..c7739d94f 100644 --- a/src/toad/conversation_kind.py +++ b/src/toad/conversation_kind.py @@ -84,7 +84,7 @@ def read_only(cls, catalog, target): return False @classmethod - async def update_roster(cls, view, comms): + async def update_mentions(cls, view, comms): pass @classmethod @@ -92,8 +92,8 @@ async def agent_info(cls, comms, target): return None @classmethod - @abstractmethod - def activity_widget(cls, view): ... + def activity_widgets(cls, view): + return () @classmethod @abstractmethod @@ -149,12 +149,6 @@ def current_identity(cls, page: MessagePage) -> tuple | None: page.history_revision, ) - @classmethod - def activity_widget(cls, view): - from toad.widgets.channel_participants import ChannelParticipants - - return ChannelParticipants() - @classmethod def prompt(cls, target): from toad.widgets.channel_prompt import ChannelPrompt @@ -166,14 +160,11 @@ def read_only(cls, catalog, target): return catalog.is_view_target(target) @classmethod - async def update_roster(cls, view, comms): - from toad.widgets.channel_participants import ChannelParticipants + async def update_mentions(cls, view, comms): from toad.widgets.channel_prompt import ChannelPrompt - # The composer consumes membership and thread presentation, not the - # actor's inbox counts or channel activity clocks. Borrow the observed - # service's sidebar model rather than reading the same registry, - # activity and catalog again after every channel page. + # Mention completion consumes the observer's membership snapshot, + # not another registry/catalog read after every channel page. history = view.message_history source = history.source_snapshot() target = view.target @@ -182,19 +173,10 @@ async def update_roster(cls, view, comms): snapshot = answer.require_snapshot() observed = access.observation if observed is None or observed.service is not comms: - raise ValueError("Channel roster belongs to another Comms service") - rows = observed.sidebar.threads.rows - - def prepare(): - people = tuple(rows[person.thread.name] for person in snapshot.participants(target) - if person.thread.name in rows) - return (ChannelParticipants.prepare_participants(people), - snapshot.mention_candidates(target)) - - participants, mentions = await view.app.preparation.run_thread(prepare) + raise ValueError("Channel mentions belong to another Comms service") + mentions = await view.app.preparation.run_thread(snapshot.mention_candidates, target) if not await history.source_is_current(source) or view.target != target: return - view.query_one(ChannelParticipants).update_participants(*participants) view.query_one(ChannelPrompt).set_mention_candidates(mentions) @classmethod @@ -278,11 +260,11 @@ def label(cls, target: str) -> str: return f"@{target}" @classmethod - def activity_widget(cls, view): + def activity_widgets(cls, view): from toad.widgets.session_details import SessionDetails view.thread_activity = SessionDetails() - return view.thread_activity + yield view.thread_activity @classmethod def prompt(cls, target): diff --git a/src/toad/widgets/channel_participants.py b/src/toad/widgets/channel_participants.py deleted file mode 100644 index 4c10f42de..000000000 --- a/src/toad/widgets/channel_participants.py +++ /dev/null @@ -1,51 +0,0 @@ -"""The active members of a channel, shown above the composer.""" -from agent_comms.ui_model.sidebar import ThreadRowModel -from textual.app import ComposeResult -from textual.containers import VerticalScroll -from textual.content import Content -from textual.style import Style -from textual.widgets import Static - -from toad.line_blocks import OpenTarget -from toad.widgets.transcript_lines import OpensLinks - - -class ParticipantNames(OpensLinks, Static): - """The names of a channel's active threads; each opens its thread.""" - - -class ChannelParticipants(VerticalScroll): - DEFAULT_CSS = """ - ChannelParticipants { - height: auto; max-height: 4; overflow-y: auto; - padding: 0 1; color: $text-muted; - } - """ - - def __init__(self): - super().__init__() - self.names = ParticipantNames(markup=False) - - def compose(self) -> ComposeResult: - yield self.names - - @staticmethod - def prepare_participants(people: tuple[ThreadRowModel, ...]) -> tuple[Content, Content]: - """What to draw for these members, built off the UI thread.""" - names = [] - summaries = [] - for presentation in people: - summaries.append(f"{presentation.name}: {presentation.summary}") - names.append(Content.styled( - presentation.label, "$warning" if presentation.status.busy else "$text-muted", - ).stylize(Style.from_meta({"@click": OpenTarget(presentation.name).click}))) - content = Content.assemble("Active: ", Content(" · ").join(names)) if names else Content("No active turns") - return content, Content("\n".join(summaries)) - - def update_participants(self, content: Content, tooltip: Content) -> None: - """Draw prepared names; unchanged names are not redrawn.""" - previous = self.names.content - if not isinstance(previous, Content) or not content.is_same(previous): - self.names.update(content) - if tooltip != self.tooltip: - self.tooltip = tooltip diff --git a/src/toad/widgets/comms_chat.py b/src/toad/widgets/comms_chat.py index 8445e9bf2..65cc3842a 100644 --- a/src/toad/widgets/comms_chat.py +++ b/src/toad/widgets/comms_chat.py @@ -126,7 +126,7 @@ def compose(self) -> ComposeResult: yield ChannelActivityTray(id="comms-activity") yield Flash() with containers.Vertical(id="prompt-stack"): - yield self.conversation_kind.activity_widget(self) + yield from self.conversation_kind.activity_widgets(self) yield self.throbber yield self.conversation_kind.prompt(self.target).data_bind( project_path=Conversation.project_path, @@ -302,7 +302,7 @@ async def _refresh_source(self) -> None: finally: if undrawn: self.on_work_finished() - # A sent message may now be drawn: composing resumes before the roster is read. + # A sent message may now be drawn: composing resumes before mentions are read. self.settle_user_message() if changed and undrawn and (placeholder := self.query_one_optional("#history-loading")): placeholder.remove() @@ -311,7 +311,7 @@ async def _refresh_source(self) -> None: except (OSError, ValueError, RuntimeError) as error: self._show_wire_error(error) return - # The participants line and the status line read the observer's sidebar + # Composer mentions and DM status read the observer's snapshot # and the agent records, which can take seconds on a busy bus. They are # not this conversation's messages, so they run after this work ends # and the history can load pages while they wait. @@ -327,7 +327,7 @@ def _show_wire_error(self, error: Exception) -> None: self.status = message async def _refresh_labels(self, comms, changed: bool, newly_announced: bool, read_only: bool) -> None: - """Show who is in the conversation and its status line, for the history as it is drawn now.""" + """Refresh composer mentions and status for the history as it is drawn now.""" from agent_comms.coordination_errors import CoordinationReadUnavailable, StaleRevision history = self.message_history @@ -335,9 +335,9 @@ async def _refresh_labels(self, comms, changed: bool, newly_announced: bool, rea source, target = history.source, self.target try: # An unchanged history may have read the stores before the observer - # announced the change; the participants line reads the observer's sidebar. + # announced the change; mention completion reads the observer's snapshot. if changed or newly_announced: - await self.conversation_kind.update_roster(self, comms) + await self.conversation_kind.update_mentions(self, comms) if changed: if not await history.source_is_current(snapshot): if snapshot.current(history): From f56e62da02d4dc200d0610cc44760aea55c53c15 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 12:13:12 -0400 Subject: [PATCH 02/14] Require explicit kind declarations and keep status widgets view-owned --- src/toad/conversation_kind.py | 42 ++++++++++++++++++++++++---------- src/toad/widgets/comms_chat.py | 28 +++++++++-------------- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/toad/conversation_kind.py b/src/toad/conversation_kind.py index c7739d94f..fe245fc91 100644 --- a/src/toad/conversation_kind.py +++ b/src/toad/conversation_kind.py @@ -8,6 +8,7 @@ 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"): @@ -84,16 +85,18 @@ def read_only(cls, catalog, target): return False @classmethod - async def update_mentions(cls, view, comms): - pass + @abstractmethod + async def update_mentions(cls, view, comms) -> None: ... @classmethod - async def agent_info(cls, comms, target): - return None + @abstractmethod + async def status_text(cls, comms, target, *, read_only: bool) -> str: ... @classmethod - def activity_widgets(cls, view): - return () + @abstractmethod + def activity_widgets(cls) -> tuple[type[SessionDetails], ...]: + """Declare widget classes; the composing view owns their instances.""" + ... @classmethod @abstractmethod @@ -149,6 +152,14 @@ def current_identity(cls, page: MessagePage) -> tuple | None: page.history_revision, ) + @classmethod + def activity_widgets(cls) -> tuple[type[SessionDetails], ...]: + return () + + @classmethod + async def status_text(cls, comms, target, *, read_only: bool) -> str: + return "Read-only saved view" if read_only else "" + @classmethod def prompt(cls, target): from toad.widgets.channel_prompt import ChannelPrompt @@ -260,11 +271,12 @@ def label(cls, target: str) -> str: return f"@{target}" @classmethod - def activity_widgets(cls, view): - from toad.widgets.session_details import SessionDetails + def activity_widgets(cls) -> tuple[type[SessionDetails], ...]: + return (SessionDetails,) - view.thread_activity = SessionDetails() - yield view.thread_activity + @classmethod + async def update_mentions(cls, view, comms) -> None: + """Direct-message composers have no channel mention roster.""" @classmethod def prompt(cls, target): @@ -273,8 +285,14 @@ def prompt(cls, target): return Prompt(simple_input=True, placeholder=cls.placeholder(target)) @classmethod - async def agent_info(cls, comms, target): - return await asyncio.to_thread(comms.configuration.answer, target) + async def status_text(cls, comms, target, *, read_only: bool) -> str: + info = await asyncio.to_thread(comms.configuration.answer, target) + return " · ".join( + value for value in ( + info.model or "", + info.context_usage.label if info.context_usage is not None else "", + ) if value + ) @classmethod def seen_page(cls, history, seen): diff --git a/src/toad/widgets/comms_chat.py b/src/toad/widgets/comms_chat.py index 65cc3842a..81cb6f53c 100644 --- a/src/toad/widgets/comms_chat.py +++ b/src/toad/widgets/comms_chat.py @@ -33,6 +33,7 @@ Window, ) from toad.widgets.flash import Flash +from toad.widgets.native_history import NativeHistory from toad.widgets.prompt import Prompt from toad.screens.session_view import SessionView from toad.command_catalog import CommandCatalog @@ -126,7 +127,12 @@ def compose(self) -> ComposeResult: yield ChannelActivityTray(id="comms-activity") yield Flash() with containers.Vertical(id="prompt-stack"): - yield from self.conversation_kind.activity_widgets(self) + 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 self.throbber yield self.conversation_kind.prompt(self.target).data_bind( project_path=Conversation.project_path, @@ -196,8 +202,8 @@ async def on_transcript_source_work_finished(self, event) -> None: return if self._refreshed_from != access.revision: # The observer announced a change while this work ran. The history - # may have read it from the stores already; the participants line - # reads the observer's sidebar, which only now shows it. + # may have read it from the stores already; mention completion + # reads the observer's snapshot, which only now shows it. await self._refresh() return if history.revision == access.revision: @@ -349,7 +355,7 @@ async def _refresh_labels(self, comms, changed: bool, newly_announced: bool, rea self.call_after_refresh(history.mark_visible) if not changed: return - info = await self.conversation_kind.agent_info(comms, target) + status = await self.conversation_kind.status_text(comms, target, read_only=read_only) except (CoordinationReadUnavailable, StaleRevision): # The coordination store is busy or moved on: the observer's next # announcement refreshes this view and reads the labels again. @@ -359,19 +365,7 @@ async def _refresh_labels(self, comms, changed: bool, newly_announced: bool, rea return if not await history.source_is_current(snapshot) or self.target != target: return - if self.send_block_reason: - self.status = self.send_block_reason - elif info is not None: - self.status = " · ".join( - value - for value in ( - info.model or "", - info.context_usage.label if info.context_usage is not None else "", - ) - if value - ) - else: - self.status = "Read-only saved view" if read_only else "" + self.status = self.send_block_reason or status if self.window.follows_tail: self.window.anchor() if history.has_newer or (history.has_older and self.window.max_scroll_y == 0): From 61f80970ccad82cb4cb21ff2bfeda03cdc38a1c5 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 12:38:07 -0400 Subject: [PATCH 03/14] Read the DM status label from its Core answer --- pyproject.toml | 2 +- src/toad/conversation_kind.py | 8 +------- uv.lock | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f0e2ba5df..b9cad086e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ members = ["toad"] [tool.uv.sources] ui-theme = { git = "https://github.com/OpenHCSDev/ui-theme.git", rev = "5e58d8b5907cec4dbc8810a9329aaf902f69f675" } -agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "00a3d74d4b1ab8962941d84a764b3e8c2e57013d" } +agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "8003fdc0daa5d4e48dc3b6b4625170536eba2161" } textual = { git = "https://github.com/OpenHCSDev/textual.git", rev = "18f4e7a67403d56846ef5abf55c93e3e0639be33" } textual-image = { git = "https://github.com/OpenHCSDev/textual-image.git", rev = "1f1d8b8ea3c30b39da3b14ea0ac0786ce9c49a80" } textual-diff-view = { git = "https://github.com/OpenHCSDev/textual-diff-view.git", rev = "8fa7d4d0db993ea3b761c2760ca9b6a56a6251e9" } diff --git a/src/toad/conversation_kind.py b/src/toad/conversation_kind.py index fe245fc91..99d072b80 100644 --- a/src/toad/conversation_kind.py +++ b/src/toad/conversation_kind.py @@ -286,13 +286,7 @@ def prompt(cls, target): @classmethod async def status_text(cls, comms, target, *, read_only: bool) -> str: - info = await asyncio.to_thread(comms.configuration.answer, target) - return " · ".join( - value for value in ( - info.model or "", - info.context_usage.label if info.context_usage is not None else "", - ) if value - ) + return (await asyncio.to_thread(comms.configuration.answer, target)).status_label @classmethod def seen_page(cls, history, seen): diff --git a/uv.lock b/uv.lock index 600f3ee18..9d519fc45 100644 --- a/uv.lock +++ b/uv.lock @@ -21,7 +21,7 @@ wheels = [ [[package]] name = "agent-comms" version = "0.1.0" -source = { git = "https://github.com/OpenHCSDev/agent-comms.git?rev=00a3d74d4b1ab8962941d84a764b3e8c2e57013d#00a3d74d4b1ab8962941d84a764b3e8c2e57013d" } +source = { git = "https://github.com/OpenHCSDev/agent-comms.git?rev=8003fdc0daa5d4e48dc3b6b4625170536eba2161#8003fdc0daa5d4e48dc3b6b4625170536eba2161" } dependencies = [ { name = "aiohttp" }, { name = "metaclass-registry" }, @@ -306,7 +306,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "agent-client-protocol", specifier = "==0.12.1" }, - { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=00a3d74d4b1ab8962941d84a764b3e8c2e57013d" }, + { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=8003fdc0daa5d4e48dc3b6b4625170536eba2161" } { name = "aiosqlite", specifier = "==0.22.1" }, { name = "bashlex", specifier = "==0.18" }, { name = "click", specifier = "==8.4.0" }, From 112978b9d9855e888acb09feb2977165ea098be5 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 12:44:05 -0400 Subject: [PATCH 04/14] Pin the complete runtime-info status contract --- pyproject.toml | 2 +- uv.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b9cad086e..7465615b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ members = ["toad"] [tool.uv.sources] ui-theme = { git = "https://github.com/OpenHCSDev/ui-theme.git", rev = "5e58d8b5907cec4dbc8810a9329aaf902f69f675" } -agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "8003fdc0daa5d4e48dc3b6b4625170536eba2161" } +agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "535377010f6be4be30e4604bab2422a09058259c" } textual = { git = "https://github.com/OpenHCSDev/textual.git", rev = "18f4e7a67403d56846ef5abf55c93e3e0639be33" } textual-image = { git = "https://github.com/OpenHCSDev/textual-image.git", rev = "1f1d8b8ea3c30b39da3b14ea0ac0786ce9c49a80" } textual-diff-view = { git = "https://github.com/OpenHCSDev/textual-diff-view.git", rev = "8fa7d4d0db993ea3b761c2760ca9b6a56a6251e9" } diff --git a/uv.lock b/uv.lock index 9d519fc45..155cb591a 100644 --- a/uv.lock +++ b/uv.lock @@ -21,7 +21,7 @@ wheels = [ [[package]] name = "agent-comms" version = "0.1.0" -source = { git = "https://github.com/OpenHCSDev/agent-comms.git?rev=8003fdc0daa5d4e48dc3b6b4625170536eba2161#8003fdc0daa5d4e48dc3b6b4625170536eba2161" } +source = { git = "https://github.com/OpenHCSDev/agent-comms.git?rev=535377010f6be4be30e4604bab2422a09058259c#535377010f6be4be30e4604bab2422a09058259c" } dependencies = [ { name = "aiohttp" }, { name = "metaclass-registry" }, @@ -306,7 +306,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "agent-client-protocol", specifier = "==0.12.1" }, - { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=8003fdc0daa5d4e48dc3b6b4625170536eba2161" } + { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=535377010f6be4be30e4604bab2422a09058259c" }, { name = "aiosqlite", specifier = "==0.22.1" }, { name = "bashlex", specifier = "==0.18" }, { name = "click", specifier = "==8.4.0" }, From 7fd04483b4bbdebf2f438759589bcac29abe9999 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 12:47:25 -0400 Subject: [PATCH 05/14] Supply DM session details history and status source --- src/toad/conversation_kind.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/toad/conversation_kind.py b/src/toad/conversation_kind.py index b8ef0e093..c8bf5efcb 100644 --- a/src/toad/conversation_kind.py +++ b/src/toad/conversation_kind.py @@ -279,9 +279,10 @@ def label(cls, target: str) -> str: @classmethod def activity_widget(cls, view): + from toad.widgets.native_history import NativeHistory from toad.widgets.session_details import SessionDetails - view.thread_activity = SessionDetails() + view.thread_activity = SessionDetails(history=NativeHistory(), status_source=lambda: view.thread_activity.row) return view.thread_activity @classmethod From 78361008a67f185d3bd027c62af77ddf50998ecb Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 12:57:35 -0400 Subject: [PATCH 06/14] Pin runtime-info-owned status behavior --- pyproject.toml | 2 +- uv.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7465615b3..bf2912e97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ members = ["toad"] [tool.uv.sources] ui-theme = { git = "https://github.com/OpenHCSDev/ui-theme.git", rev = "5e58d8b5907cec4dbc8810a9329aaf902f69f675" } -agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "535377010f6be4be30e4604bab2422a09058259c" } +agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "7c5fdc27925beee95ed07bdf362d4a80be95119f" } textual = { git = "https://github.com/OpenHCSDev/textual.git", rev = "18f4e7a67403d56846ef5abf55c93e3e0639be33" } textual-image = { git = "https://github.com/OpenHCSDev/textual-image.git", rev = "1f1d8b8ea3c30b39da3b14ea0ac0786ce9c49a80" } textual-diff-view = { git = "https://github.com/OpenHCSDev/textual-diff-view.git", rev = "8fa7d4d0db993ea3b761c2760ca9b6a56a6251e9" } diff --git a/uv.lock b/uv.lock index 155cb591a..9fb8843db 100644 --- a/uv.lock +++ b/uv.lock @@ -21,7 +21,7 @@ wheels = [ [[package]] name = "agent-comms" version = "0.1.0" -source = { git = "https://github.com/OpenHCSDev/agent-comms.git?rev=535377010f6be4be30e4604bab2422a09058259c#535377010f6be4be30e4604bab2422a09058259c" } +source = { git = "https://github.com/OpenHCSDev/agent-comms.git?rev=7c5fdc27925beee95ed07bdf362d4a80be95119f#7c5fdc27925beee95ed07bdf362d4a80be95119f" } dependencies = [ { name = "aiohttp" }, { name = "metaclass-registry" }, @@ -306,7 +306,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "agent-client-protocol", specifier = "==0.12.1" }, - { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=535377010f6be4be30e4604bab2422a09058259c" }, + { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=7c5fdc27925beee95ed07bdf362d4a80be95119f" }, { name = "aiosqlite", specifier = "==0.22.1" }, { name = "bashlex", specifier = "==0.18" }, { name = "click", specifier = "==8.4.0" }, From 51ae288e7e8567e326d1dbd12b67d15bd39c0771 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 13:31:50 -0400 Subject: [PATCH 07/14] Pass the owning runtime to Core native inventory lookup --- src/toad/mcp_inventory.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/toad/mcp_inventory.py b/src/toad/mcp_inventory.py index 0e5d37ade..3618a1e9f 100644 --- a/src/toad/mcp_inventory.py +++ b/src/toad/mcp_inventory.py @@ -13,6 +13,7 @@ from pathlib import Path from agent_comms.field_codec import FieldCodec +from agent_comms.launch_runtime import LaunchRuntime from toad import mcp_declarations @@ -53,7 +54,7 @@ def installed_mcp_command() -> tuple[str, str]: """Use MCP shipped inside the same verified native package as Comms.""" from agent_comms.native_pi import NativePiRpcLaunch - package = NativePiRpcLaunch.package_for_command("pi") + package = NativePiRpcLaunch.package_for_command("pi", LaunchRuntime.running()) node = shutil.which("node") if node is None: raise FileNotFoundError("Node is unavailable") From ddf6b9f6d58112686ac800ca911e05918964383f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 13:50:24 -0400 Subject: [PATCH 08/14] Render retained Core context readings without copied measurement state --- pyproject.toml | 2 +- src/toad/acp/agent.py | 5 +++-- src/toad/acp/context_measurement.py | 25 +++++++++---------------- uv.lock | 4 ++-- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bf2912e97..6b6a6e59b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ members = ["toad"] [tool.uv.sources] ui-theme = { git = "https://github.com/OpenHCSDev/ui-theme.git", rev = "5e58d8b5907cec4dbc8810a9329aaf902f69f675" } -agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "7c5fdc27925beee95ed07bdf362d4a80be95119f" } +agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "3e7aabe09558330de5a5865ed1a030d65d57c982" } textual = { git = "https://github.com/OpenHCSDev/textual.git", rev = "18f4e7a67403d56846ef5abf55c93e3e0639be33" } textual-image = { git = "https://github.com/OpenHCSDev/textual-image.git", rev = "1f1d8b8ea3c30b39da3b14ea0ac0786ce9c49a80" } textual-diff-view = { git = "https://github.com/OpenHCSDev/textual-diff-view.git", rev = "8fa7d4d0db993ea3b761c2760ca9b6a56a6251e9" } diff --git a/src/toad/acp/agent.py b/src/toad/acp/agent.py index f88cae87d..e3c1b908d 100644 --- a/src/toad/acp/agent.py +++ b/src/toad/acp/agent.py @@ -4,7 +4,7 @@ from toad.acp.client_files import FileClientRequestOwner from toad.acp.tool_calls import SessionToolCalls from toad.acp.agent_configuration import ModelConfigurationSetting, ThinkingConfigurationSetting -from toad.acp.context_measurement import ContextMeasurement, ContextUnavailable +from toad.acp.context_measurement import ContextMeasurement from toad.acp.agent_log import AgentLog from toad.acp.agent_process import AgentProcess from toad.acp.agent_controller import AgentController @@ -24,6 +24,7 @@ ) from agent_comms.acp_failure import ACPFailure from agent_comms.field_codec import FieldCodec +from agent_comms.context_reading import UnmeasuredContext from agent_comms.goal_actions import GoalAction from agent_comms.goal_presentation import GoalSnapshot from agent_comms.goals import Goal @@ -111,7 +112,7 @@ def __init__( self.presentation.log_path.unlink(missing_ok=True) else: self.presentation.log_path = paths.get_log() / log_filename - self.context_measurement = ContextUnavailable("Native owner has not reported context usage") + self.context_measurement = ContextMeasurement.saved(UnmeasuredContext()) @property def command(self) -> str | None: diff --git a/src/toad/acp/context_measurement.py b/src/toad/acp/context_measurement.py index 1bad8c97f..7e3d3e1ec 100644 --- a/src/toad/acp/context_measurement.py +++ b/src/toad/acp/context_measurement.py @@ -5,6 +5,7 @@ from math import floor from typing import NamedTuple from rich.text import Text as RichText +from agent_comms.context_reading import ContextReading class Cost(NamedTuple): """A cost with associated currency.""" @@ -24,16 +25,12 @@ def __format__(self, _specifier: str) -> str: class ContextMeasurement(ABC): - available = False - @abstractmethod def status(self) -> RichText: ... @classmethod - def saved(cls, usage): - if usage is None: - return ContextUnavailable("Native owner has not reported context usage") - return SavedContextMeasurement(usage.used, usage.size) + def saved(cls, reading: ContextReading) -> ContextMeasurement: + return SavedContextMeasurement(reading) @classmethod def live(cls, used, size, cost): @@ -53,12 +50,10 @@ def status(self) -> RichText: @dataclass(frozen=True) -class MeasuredContext(ContextMeasurement): +class LiveContextMeasurement(ContextMeasurement): used: int size: int cost: Cost | None = None - available = True - source_label = "" @property def percentage_used(self): @@ -71,16 +66,14 @@ def percentage_display(self): def status(self) -> RichText: parts = [RichText.assemble(f"{self.used / 1000:.1f}K (", (self.percentage_display, "bold"), ")")] - if self.source_label: - parts.append(RichText(self.source_label)) if self.cost is not None: parts.append(RichText.assemble((f"{self.cost}", "bold"))) return RichText(" • ").join(parts) -class LiveContextMeasurement(MeasuredContext): - pass - +@dataclass(frozen=True) +class SavedContextMeasurement(ContextMeasurement): + reading: ContextReading -class SavedContextMeasurement(MeasuredContext): - source_label = "last response" + def status(self) -> RichText: + return RichText(self.reading.status_text()) diff --git a/uv.lock b/uv.lock index 9fb8843db..5a47737cb 100644 --- a/uv.lock +++ b/uv.lock @@ -21,7 +21,7 @@ wheels = [ [[package]] name = "agent-comms" version = "0.1.0" -source = { git = "https://github.com/OpenHCSDev/agent-comms.git?rev=7c5fdc27925beee95ed07bdf362d4a80be95119f#7c5fdc27925beee95ed07bdf362d4a80be95119f" } +source = { git = "https://github.com/OpenHCSDev/agent-comms.git?rev=3e7aabe09558330de5a5865ed1a030d65d57c982#3e7aabe09558330de5a5865ed1a030d65d57c982" } dependencies = [ { name = "aiohttp" }, { name = "metaclass-registry" }, @@ -306,7 +306,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "agent-client-protocol", specifier = "==0.12.1" }, - { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=7c5fdc27925beee95ed07bdf362d4a80be95119f" }, + { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=3e7aabe09558330de5a5865ed1a030d65d57c982" }, { name = "aiosqlite", specifier = "==0.22.1" }, { name = "bashlex", specifier = "==0.18" }, { name = "click", specifier = "==8.4.0" }, From 37434ef3262faabb25c639c08cabeed730fc9e7e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 14:01:02 -0400 Subject: [PATCH 09/14] Compose DM activity in its declared conversation kind --- pyproject.toml | 2 +- src/toad/conversation_kind.py | 16 +++++++++++----- src/toad/widgets/comms_chat.py | 8 +------- uv.lock | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6b6a6e59b..e42887a28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ members = ["toad"] [tool.uv.sources] ui-theme = { git = "https://github.com/OpenHCSDev/ui-theme.git", rev = "5e58d8b5907cec4dbc8810a9329aaf902f69f675" } -agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "3e7aabe09558330de5a5865ed1a030d65d57c982" } +agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "4370140a921c58269cceb812a61d6d7132862e60" } textual = { git = "https://github.com/OpenHCSDev/textual.git", rev = "18f4e7a67403d56846ef5abf55c93e3e0639be33" } textual-image = { git = "https://github.com/OpenHCSDev/textual-image.git", rev = "1f1d8b8ea3c30b39da3b14ea0ac0786ce9c49a80" } textual-diff-view = { git = "https://github.com/OpenHCSDev/textual-diff-view.git", rev = "8fa7d4d0db993ea3b761c2760ca9b6a56a6251e9" } diff --git a/src/toad/conversation_kind.py b/src/toad/conversation_kind.py index 99d072b80..4a93c7cc3 100644 --- a/src/toad/conversation_kind.py +++ b/src/toad/conversation_kind.py @@ -8,6 +8,8 @@ from agent_comms.declared_family import DeclaredFamily from agent_comms.comms import Comms from agent_comms.message_page import MessagePage +from textual.app import ComposeResult +from toad.widgets.native_history import NativeHistory from toad.widgets.session_details import SessionDetails @@ -94,8 +96,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) -> ComposeResult: + """Compose this kind's activity using the existing view state.""" ... @classmethod @@ -153,7 +155,7 @@ def current_identity(cls, page: MessagePage) -> tuple | None: ) @classmethod - def activity_widgets(cls) -> tuple[type[SessionDetails], ...]: + def compose_activity(cls, view) -> ComposeResult: return () @classmethod @@ -271,8 +273,12 @@ def label(cls, target: str) -> str: return f"@{target}" @classmethod - def activity_widgets(cls) -> tuple[type[SessionDetails], ...]: - return (SessionDetails,) + def compose_activity(cls, view) -> ComposeResult: + view.thread_activity = SessionDetails( + history=NativeHistory(), + status_source=lambda: view.thread_activity.row, + ) + yield view.thread_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..942073851 100644 --- a/src/toad/widgets/comms_chat.py +++ b/src/toad/widgets/comms_chat.py @@ -33,7 +33,6 @@ Window, ) from toad.widgets.flash import Flash -from toad.widgets.native_history import NativeHistory from toad.widgets.prompt import Prompt from toad.screens.session_view import SessionView from toad.command_catalog import CommandCatalog @@ -127,12 +126,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, diff --git a/uv.lock b/uv.lock index 5a47737cb..0132f802a 100644 --- a/uv.lock +++ b/uv.lock @@ -21,7 +21,7 @@ wheels = [ [[package]] name = "agent-comms" version = "0.1.0" -source = { git = "https://github.com/OpenHCSDev/agent-comms.git?rev=3e7aabe09558330de5a5865ed1a030d65d57c982#3e7aabe09558330de5a5865ed1a030d65d57c982" } +source = { git = "https://github.com/OpenHCSDev/agent-comms.git?rev=4370140a921c58269cceb812a61d6d7132862e60#4370140a921c58269cceb812a61d6d7132862e60" } dependencies = [ { name = "aiohttp" }, { name = "metaclass-registry" }, @@ -306,7 +306,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "agent-client-protocol", specifier = "==0.12.1" }, - { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=3e7aabe09558330de5a5865ed1a030d65d57c982" }, + { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=4370140a921c58269cceb812a61d6d7132862e60" }, { name = "aiosqlite", specifier = "==0.22.1" }, { name = "bashlex", specifier = "==0.18" }, { name = "click", specifier = "==8.4.0" }, From 119b15a7890a0e11c607696fbd249772d04879b6 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 14:18:10 -0400 Subject: [PATCH 10/14] Bracket live status with the shared themed activity bar --- pyproject.toml | 2 +- src/toad/widgets/conversation.py | 3 +++ uv.lock | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f0e2ba5df..636271b5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ members = ["toad"] [tool.uv.sources] -ui-theme = { git = "https://github.com/OpenHCSDev/ui-theme.git", rev = "5e58d8b5907cec4dbc8810a9329aaf902f69f675" } +ui-theme = { git = "https://github.com/OpenHCSDev/ui-theme.git", rev = "686311a46c74ed31ce036d3144380a92865f2229" } agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "00a3d74d4b1ab8962941d84a764b3e8c2e57013d" } textual = { git = "https://github.com/OpenHCSDev/textual.git", rev = "18f4e7a67403d56846ef5abf55c93e3e0639be33" } textual-image = { git = "https://github.com/OpenHCSDev/textual-image.git", rev = "1f1d8b8ea3c30b39da3b14ea0ac0786ce9c49a80" } diff --git a/src/toad/widgets/conversation.py b/src/toad/widgets/conversation.py index c61e8a884..bf008bbce 100644 --- a/src/toad/widgets/conversation.py +++ b/src/toad/widgets/conversation.py @@ -506,6 +506,7 @@ def __init__( lambda: self.busy_count > 0 or self.turns.owner.busy or self._observed_working, id="throbber", ) + self.throbber_above = ObservedThrobber(lambda: self.throbber.busy) self._initialize_session(project_path, agent, agent_session_id, session_pk, session_title, initial_prompt, transcript) @@ -927,6 +928,7 @@ def compose(self) -> ComposeResult: ), ) self.refresh_thread_status() + yield self.throbber_above yield self.thread_activity prompt = self.operational_sources.prompt(self).data_bind( project_path=Conversation.project_path, @@ -1687,6 +1689,7 @@ async def _selected_commands_changed(self, event: CoreEventMessage) -> None: self.update_slash_commands() async def on_mount(self) -> None: + self.watch(self.throbber, "busy", self.throbber_above.watch_busy) await self.initialize_view() def apply_layout_preferences(self) -> None: diff --git a/uv.lock b/uv.lock index 600f3ee18..d13ba42bb 100644 --- a/uv.lock +++ b/uv.lock @@ -329,7 +329,7 @@ requires-dist = [ { name = "textual-speedups", specifier = "==0.2.1" }, { name = "typeguard", specifier = "==4.5.2" }, { name = "typing-extensions", specifier = "==4.15.0" }, - { name = "ui-theme", git = "https://github.com/OpenHCSDev/ui-theme.git?rev=5e58d8b5907cec4dbc8810a9329aaf902f69f675" }, + { name = "ui-theme", git = "https://github.com/OpenHCSDev/ui-theme.git?rev=686311a46c74ed31ce036d3144380a92865f2229" }, { name = "watchdog", specifier = "==6.0.0" }, { name = "xdg-base-dirs", specifier = ">=6.0.2" }, ] @@ -2021,7 +2021,7 @@ wheels = [ [[package]] name = "ui-theme" version = "0.1.0" -source = { git = "https://github.com/OpenHCSDev/ui-theme.git?rev=5e58d8b5907cec4dbc8810a9329aaf902f69f675#5e58d8b5907cec4dbc8810a9329aaf902f69f675" } +source = { git = "https://github.com/OpenHCSDev/ui-theme.git?rev=686311a46c74ed31ce036d3144380a92865f2229#686311a46c74ed31ce036d3144380a92865f2229" } dependencies = [ { name = "metaclass-registry" }, { name = "terminaltexteffects" }, From 4037288ea37dd6603096d76c5ec0dfea633a2e06 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 14:26:29 -0400 Subject: [PATCH 11/14] Read both medium bars from the conversation working owner --- pyproject.toml | 2 +- src/toad/widgets/conversation.py | 21 ++++++++++++--------- uv.lock | 4 ++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 636271b5e..10319dec8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ members = ["toad"] [tool.uv.sources] -ui-theme = { git = "https://github.com/OpenHCSDev/ui-theme.git", rev = "686311a46c74ed31ce036d3144380a92865f2229" } +ui-theme = { git = "https://github.com/OpenHCSDev/ui-theme.git", rev = "aaa35af5948c6025d33fc178f6bd687d9dc5c322" } agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "00a3d74d4b1ab8962941d84a764b3e8c2e57013d" } textual = { git = "https://github.com/OpenHCSDev/textual.git", rev = "18f4e7a67403d56846ef5abf55c93e3e0639be33" } textual-image = { git = "https://github.com/OpenHCSDev/textual-image.git", rev = "1f1d8b8ea3c30b39da3b14ea0ac0786ce9c49a80" } diff --git a/src/toad/widgets/conversation.py b/src/toad/widgets/conversation.py index bf008bbce..23a77fff6 100644 --- a/src/toad/widgets/conversation.py +++ b/src/toad/widgets/conversation.py @@ -502,11 +502,8 @@ def __init__( ) -> None: super().__init__() self.turns = ConversationTurn(self._turn_changed, lambda: self.agent.presentation.turns if self.agent else None) - self.throbber = ObservedThrobber( - lambda: self.busy_count > 0 or self.turns.owner.busy or self._observed_working, - id="throbber", - ) - self.throbber_above = ObservedThrobber(lambda: self.throbber.busy) + self.throbber = ObservedThrobber(self.working, id="throbber") + self.throbber_above = ObservedThrobber(self.working) self._initialize_session(project_path, agent, agent_session_id, session_pk, session_title, initial_prompt, transcript) @@ -881,8 +878,15 @@ async def sync_project_path(self, path: Path) -> None: await DB().session_update_project(session_pk, path) + def working(self) -> bool: + return self.busy_count > 0 or self.turns.owner.busy or self._observed_working + + def _sync_working(self) -> None: + for throbber in (self.throbber, self.throbber_above): + throbber.sync() + def _turn_changed(self, owner: TurnOwner) -> None: - self.throbber.sync() + self._sync_working() if (prompt := self.query_one_optional(Prompt)) is not None: prompt.sync_turn() if (details := self.query_one_optional(SessionDetails)) is not None: @@ -892,7 +896,7 @@ def _turn_changed(self, owner: TurnOwner) -> None: @on(ObservedThreadActivity.Changed) def _observed_activity_changed(self, event: ObservedThreadActivity.Changed) -> None: event.stop() - self.throbber.sync() + self._sync_working() @on(events.Key) async def on_key(self, event: events.Key): @@ -1350,7 +1354,7 @@ def on_current_working_directory_changed( self.working_directory = str(Path(event.event.path).resolve().absolute()) async def watch_busy_count(self, _busy: int) -> None: - self.throbber.sync() + self._sync_working() @on(TerminalTool.Projection) async def on_terminal_projection(self, message: TerminalTool.Projection): @@ -1689,7 +1693,6 @@ async def _selected_commands_changed(self, event: CoreEventMessage) -> None: self.update_slash_commands() async def on_mount(self) -> None: - self.watch(self.throbber, "busy", self.throbber_above.watch_busy) await self.initialize_view() def apply_layout_preferences(self) -> None: diff --git a/uv.lock b/uv.lock index d13ba42bb..1a5d0c0be 100644 --- a/uv.lock +++ b/uv.lock @@ -329,7 +329,7 @@ requires-dist = [ { name = "textual-speedups", specifier = "==0.2.1" }, { name = "typeguard", specifier = "==4.5.2" }, { name = "typing-extensions", specifier = "==4.15.0" }, - { name = "ui-theme", git = "https://github.com/OpenHCSDev/ui-theme.git?rev=686311a46c74ed31ce036d3144380a92865f2229" }, + { name = "ui-theme", git = "https://github.com/OpenHCSDev/ui-theme.git?rev=aaa35af5948c6025d33fc178f6bd687d9dc5c322" }, { name = "watchdog", specifier = "==6.0.0" }, { name = "xdg-base-dirs", specifier = ">=6.0.2" }, ] @@ -2021,7 +2021,7 @@ wheels = [ [[package]] name = "ui-theme" version = "0.1.0" -source = { git = "https://github.com/OpenHCSDev/ui-theme.git?rev=686311a46c74ed31ce036d3144380a92865f2229#686311a46c74ed31ce036d3144380a92865f2229" } +source = { git = "https://github.com/OpenHCSDev/ui-theme.git?rev=aaa35af5948c6025d33fc178f6bd687d9dc5c322#aaa35af5948c6025d33fc178f6bd687d9dc5c322" } dependencies = [ { name = "metaclass-registry" }, { name = "terminaltexteffects" }, From c6a359bf92d658cbaac941f74262ec100c726179 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 14:26:36 -0400 Subject: [PATCH 12/14] Declare accepted event types on native subscriptions --- src/toad/core/events.py | 9 ++++++--- src/toad/core_event_carrier.py | 12 +++++++----- src/toad/session_navigation.py | 3 ++- src/toad/transcript_publication.py | 4 +++- src/toad/transcript_source_preparation.py | 4 +++- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/toad/core/events.py b/src/toad/core/events.py index bff695b2f..f601a594b 100644 --- a/src/toad/core/events.py +++ b/src/toad/core/events.py @@ -363,6 +363,7 @@ class Subscription: """A subscriber owns this resource for as long as it wants publications.""" stream: CoreEventStream + event_type: type[CoreEvent] listener: InitVar[Callable[[CoreEvent, Subscription], object]] _listener: Callable[[], Callable[[CoreEvent, Subscription], object] | None] = field(init=False, repr=False) @@ -412,11 +413,13 @@ def publisher(self) -> object: raise RuntimeError("The publication resource outlived its original owner") return publisher - def subscribe(self, listener: Callable[[CoreEvent, Subscription], object]) -> Subscription: - subscription = Subscription(self, listener) + def subscribe(self, event_type: type[CoreEvent], + listener: Callable[[CoreEvent, Subscription], object]) -> Subscription: + subscription = Subscription(self, event_type, listener) self.subscriptions.add(subscription) return subscription def publish(self, event: CoreEvent) -> None: for subscription in tuple(self.subscriptions): - subscription.publish(event) + if isinstance(event, subscription.event_type): + subscription.publish(event) diff --git a/src/toad/core_event_carrier.py b/src/toad/core_event_carrier.py index 5832d0fa8..fb474b412 100644 --- a/src/toad/core_event_carrier.py +++ b/src/toad/core_event_carrier.py @@ -43,7 +43,7 @@ def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) def subscribe_core(self, stream: CoreEventStream) -> Subscription: - subscription = stream.subscribe(self.post_core_event) + subscription = stream.subscribe(CoreEvent, self.post_core_event) self._core_subscriptions.add(subscription) return subscription @@ -67,9 +67,10 @@ async def consume_handlers(self, message, handlers, *args, **kwargs): def observe_core(self, stream: CoreEventStream) -> Subscription: """Broadcast observations terminate at their original native recipient.""" for subscription in self._core_subscriptions: - if subscription.stream is stream and subscription.active: + if (subscription.stream is stream and subscription.event_type is CoreEvent + and subscription.active): return subscription - subscription = stream.subscribe(self.post_core_observation) + subscription = stream.subscribe(CoreEvent, self.post_core_observation) self._core_subscriptions.add(subscription) return subscription @@ -82,7 +83,8 @@ def retire_core_observations(self, stream: CoreEventStream) -> None: if subscription.stream is stream: self.retire_core(subscription) - def observe_core_callback(self, stream: CoreEventStream, callback) -> Subscription: + def observe_core_callback(self, stream: CoreEventStream, + event_type: type[CoreEvent], callback) -> Subscription: """An original deferred operation receives relief inside its native pump.""" recipient = ref(self) try: @@ -104,7 +106,7 @@ def publish(event, subscription) -> None: message.bubble = False receiver.post_message(message) - subscription = stream.subscribe(publish) + subscription = stream.subscribe(event_type, publish) self._core_subscriptions.add(subscription) return subscription diff --git a/src/toad/session_navigation.py b/src/toad/session_navigation.py index 627fe7e5a..c5f08c742 100644 --- a/src/toad/session_navigation.py +++ b/src/toad/session_navigation.py @@ -47,7 +47,8 @@ def direct_messages(self, target: str | None = None): def bind_events(self) -> None: self.app.subscribe_core(self.events) - self.app.observe_core_callback(self.app.coordination_access.events, self.observed) + self.app.observe_core_callback(self.app.coordination_access.events, + core_events.CoordinationObserved, self.observed) async def reveal(self) -> None: from toad.widgets.comms_sidebar import CommsSidebar diff --git a/src/toad/transcript_publication.py b/src/toad/transcript_publication.py index e240aff72..1209f111b 100644 --- a/src/toad/transcript_publication.py +++ b/src/toad/transcript_publication.py @@ -15,6 +15,7 @@ from textual.worker import Worker, WorkerCancelled from textual.await_complete import AwaitComplete from toad.widgets.history_anchor import HeldReaderDestination +from toad.core.events import CoordinationObserved if TYPE_CHECKING: from toad.screens.session_view import SessionOrigin @@ -380,7 +381,8 @@ def defer(self, publication: TranscriptPublication) -> None: return self.observation.close() self.observation.callback(view.retire_core, - view.observe_core_callback(view.app.coordination_access.events, self.resume)) + view.observe_core_callback(view.app.coordination_access.events, + CoordinationObserved, self.resume)) def resume(self, _event=None) -> bool: """Resume retained work only at an existing source/preparation signal.""" diff --git a/src/toad/transcript_source_preparation.py b/src/toad/transcript_source_preparation.py index bc5af25e9..7fae86bb9 100644 --- a/src/toad/transcript_source_preparation.py +++ b/src/toad/transcript_source_preparation.py @@ -8,6 +8,7 @@ from textual.worker import Worker, WorkerCancelled from textual.await_complete import AwaitComplete from toad.core_event_carrier import CoreEventReceiver +from toad.core.events import CoordinationObserved from toad.transcript_state import TranscriptState, RetiredSourceTranscript, ParkedSourceTranscript, WorkingTranscript, LatestViewportRequest from toad.transcript_preparation import PreparedPageSource from toad.core.source_events import TranscriptSourceWorkFinished @@ -183,7 +184,8 @@ def resume(_event) -> None: self.finish_source_work(operation) self.retire_core_observations(self.app.coordination_access.events) - self.observe_core_callback(self.app.coordination_access.events, resume) + self.observe_core_callback(self.app.coordination_access.events, + CoordinationObserved, resume) def supersede_source_work( self, operation: WorkingTranscript, request: LatestViewportRequest, From 3e22e68b34d3462b4aa4c7f2c20b6916f1c91089 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 14:54:49 -0400 Subject: [PATCH 13/14] Pin the final Core context presentation checkpoint --- pyproject.toml | 2 +- uv.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bc42d27c0..8f7868347 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ members = ["toad"] [tool.uv.sources] ui-theme = { git = "https://github.com/OpenHCSDev/ui-theme.git", rev = "aaa35af5948c6025d33fc178f6bd687d9dc5c322" } -agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "8d1b3ef85317609e5c3b12f7c5ec0768bac456db" } +agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "2fb445b94fc22ed58a365709f4a873ca53e4a303" } textual = { git = "https://github.com/OpenHCSDev/textual.git", rev = "18f4e7a67403d56846ef5abf55c93e3e0639be33" } textual-image = { git = "https://github.com/OpenHCSDev/textual-image.git", rev = "1f1d8b8ea3c30b39da3b14ea0ac0786ce9c49a80" } textual-diff-view = { git = "https://github.com/OpenHCSDev/textual-diff-view.git", rev = "8fa7d4d0db993ea3b761c2760ca9b6a56a6251e9" } diff --git a/uv.lock b/uv.lock index 84fe41fa2..b54c5d46b 100644 --- a/uv.lock +++ b/uv.lock @@ -21,7 +21,7 @@ wheels = [ [[package]] name = "agent-comms" version = "0.1.0" -source = { git = "https://github.com/OpenHCSDev/agent-comms.git?rev=8d1b3ef85317609e5c3b12f7c5ec0768bac456db#8d1b3ef85317609e5c3b12f7c5ec0768bac456db" } +source = { git = "https://github.com/OpenHCSDev/agent-comms.git?rev=2fb445b94fc22ed58a365709f4a873ca53e4a303#2fb445b94fc22ed58a365709f4a873ca53e4a303" } dependencies = [ { name = "aiohttp" }, { name = "metaclass-registry" }, @@ -306,7 +306,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "agent-client-protocol", specifier = "==0.12.1" }, - { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=8d1b3ef85317609e5c3b12f7c5ec0768bac456db" } + { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=2fb445b94fc22ed58a365709f4a873ca53e4a303" } { name = "aiosqlite", specifier = "==0.22.1" }, { name = "bashlex", specifier = "==0.18" }, { name = "click", specifier = "==8.4.0" }, From a64bca902416fd7881d9676b79040f134ad513fd Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 14:55:11 -0400 Subject: [PATCH 14/14] Keep the generated Core requirement array valid --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index b54c5d46b..3b893e324 100644 --- a/uv.lock +++ b/uv.lock @@ -306,7 +306,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "agent-client-protocol", specifier = "==0.12.1" }, - { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=2fb445b94fc22ed58a365709f4a873ca53e4a303" } + { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=2fb445b94fc22ed58a365709f4a873ca53e4a303" }, { name = "aiosqlite", specifier = "==0.22.1" }, { name = "bashlex", specifier = "==0.18" }, { name = "click", specifier = "==8.4.0" },