From 34332ea2425e558f8a0826809e6f302810c9af18 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 13:55:42 -0400 Subject: [PATCH 1/6] Open Core commander cards and alarms through a discovered slash command --- src/toad/comms_root.py | 29 ++++++++++ src/toad/screens/commander.py | 106 ++++++++++++++++++++++++++++++++++ src/toad/slash_command.py | 20 +++++++ 3 files changed, 155 insertions(+) create mode 100644 src/toad/screens/commander.py diff --git a/src/toad/comms_root.py b/src/toad/comms_root.py index 9d4642ac1..229dba14b 100644 --- a/src/toad/comms_root.py +++ b/src/toad/comms_root.py @@ -32,6 +32,7 @@ SidebarObserved, SidebarUnreadable, ViewsRetired, StopObserving, StopSidebar, ThreadsObserved, ) +from agent_comms.ui_model.commander import CommanderObserved, StopCommander from agent_comms.ui_model.sidebar import SidebarModel, SidebarObservation from agent_comms.ui_model.status import OpenThreadStatus from toad.core.events import CoreEventStream, CoordinationObserved, ExternalTranscriptChanged, OpenTabsChanged, LinkContextChanged, ObservedServiceChanged, ThreadRowsChanged @@ -76,6 +77,9 @@ def uses(self, service) -> bool: def tabs(self, sessions): return {entry.mode: entry.unobserved_tab() for entry in sessions.members} + + def observe_commander(self, wanted) -> None: + """An unobserved route has no process accepting page interest.""" def require(self, access, selected): return access.open_selected(selected) def references_for(self, root): @@ -143,6 +147,8 @@ def show_sidebar(self, view): def activate(self, access): self.observe_sidebar(None) + for view in tuple(access.commander_views): + view.commander_failed(self.error) def observe_sidebar(self, wanted): access = self.access @@ -190,6 +196,7 @@ def require(self, access, selected): def activate(self, access): self.loop.add_reader(self.process.fileno(), self.receive, access) access.update_sidebar_interest() + access.update_commander_interest() self.thread_interest(access) self.updates.callback(self.answer_reads) self.updates.enter_context(access.app.events.subscribe(OpenTabsChanged, self.views_changed)) @@ -243,6 +250,9 @@ async def current_references(self, access, root): def thread_interest(self, access): self.process.request(ObserveThreads(access.events.observed_threads(str(self.service.root)))) + def observe_commander(self, wanted) -> None: + self.process.request(wanted) + def observe_sidebar(self, wanted): self.process.request(wanted if wanted is not None else StopSidebar()) @@ -270,6 +280,7 @@ def __init__(self, app: ToadApp) -> None: self.thread_status = OpenThreadStatus(app.call_next) # Views showing the sidebar model now, and consumers waiting for current rows. self.sidebar_views: WeakSet[object] = WeakSet() + self.commander_views: WeakSet[object] = WeakSet() self.reference_views: WeakSet[object] = WeakSet() self.sidebar_waiters: list[tuple[ObserveSidebar, asyncio.Future[SidebarObservation]]] = [] self.sidebar_interest: ObserveSidebar | None = None @@ -323,6 +334,24 @@ def revision_observed(self, result: RevisionObserved, observed: Observing) -> No if result.revision.changed_since(ExternalTranscriptsView(), previous): self.events.publish(ExternalTranscriptChanged()) + def show_commander(self, view) -> None: + self.commander_views.add(view) + self.update_commander_interest() + + def stop_commander(self, view) -> None: + self.commander_views.discard(view) + self.update_commander_interest() + + def update_commander_interest(self) -> None: + request = next((view.request for view in self.commander_views), StopCommander()) + self.observation.observe_commander(request) + + @handles(CommanderObserved) + def commander_observed(self, result: CommanderObserved, observed: Observing) -> None: + for view in tuple(self.commander_views): + if view.request == result.request: + result.model.answer(view.commander_observed, lambda: None, view.commander_failed) + @handles(SidebarObserved) def sidebar_observed(self, result: SidebarObserved, observed: Observing) -> None: if result.request != self.sidebar_interest: diff --git a/src/toad/screens/commander.py b/src/toad/screens/commander.py new file mode 100644 index 000000000..00d3b598a --- /dev/null +++ b/src/toad/screens/commander.py @@ -0,0 +1,106 @@ +"""Commander cards are views of Core's observed model, not another order board.""" +from __future__ import annotations + +from dataclasses import replace +from agent_comms.thread_identity import ThreadIncarnation +from agent_comms.ui_model.commander import CommanderModel, ObserveCommander, UnitCard +from textual import containers, on +from textual.app import ComposeResult +from textual.content import Content +from textual.widgets import Collapsible, Static + +from toad.screens.modal import ModalScreen + + +class CommanderUnit(Collapsible): + DEFAULT_CSS = """ + CommanderUnit { padding: 0; margin: 0; } + CommanderUnit > CollapsibleTitle { height: 1; text-overflow: ellipsis; } + CommanderUnit Static { height: auto; } + """ + + def __init__(self, card: UnitCard) -> None: + super().__init__(Static(card.detail.text, markup=False), + title=Content(card.line), collapsed=True) + self.unit = card.unit + self.position = 0 + + def present(self, card: UnitCard, position: int) -> None: + self.position = position + self.title = Content(card.line) + self.query_one(Static).update(card.detail.text) + + +class CommanderScreen(ModalScreen[None]): + DEFAULT_CSS = """ + CommanderScreen { align: center middle; } + CommanderScreen #commander-page { + width: 98%; height: 95%; border: round $accent; background: $surface; + } + CommanderScreen #commander-status { height: 1; padding: 0 1; } + CommanderScreen #commander-board { height: 3fr; } + CommanderScreen #commander-alarms { height: 1fr; border-top: solid $accent; padding: 0 1; } + CommanderScreen #commander-alarms Static { height: auto; } + """ + BINDINGS = [("escape", "dismiss", "Close commander")] + + def __init__(self, commander: str) -> None: + super().__init__() + self.commander_name = commander + self.rows: dict[ThreadIncarnation, CommanderUnit] = {} + self.request: ObserveCommander + + def compose(self) -> ComposeResult: + with containers.Vertical(id="commander-page"): + yield Static("Opening commander…", id="commander-status", markup=False) + yield containers.VerticalScroll(id="commander-board") + with containers.VerticalScroll(id="commander-alarms"): + yield Static("Alarms", id="commander-alarm-lines", markup=False) + + async def on_mount(self) -> None: + access = self.app.coordination_access + try: + def identify(): + observed = access.observation.observed() + return ObserveCommander(str(observed.service.root), + observed.service.registry.require(self.commander_name).incarnation) + self.request = await access.preparation.run_thread(identify) + access.show_commander(self) + except (ValueError, OSError, RuntimeError) as error: + self.commander_failed(error) + + def on_unmount(self) -> None: + self.app.coordination_access.stop_commander(self) + + def commander_failed(self, error: Exception) -> None: + self.query_one("#commander-status", Static).update(f"Commander unavailable: {error}") + for row in self.rows.values(): + row.remove() + self.rows.clear() + + def commander_observed(self, model: CommanderModel) -> None: + board = self.query_one("#commander-board", containers.VerticalScroll) + current = {card.unit for card in model.units} + for unit in self.rows.keys() - current: + self.rows.pop(unit).remove() + for position, card in enumerate(model.units): + if card.unit not in self.rows: + row = self.rows[card.unit] = CommanderUnit(card) + row.position = position + board.mount(row) + else: + self.rows[card.unit].present(card, position) + board.call_after_refresh(board.sort_children, key=lambda row: row.position) + self.query_one("#commander-status", Static).update( + f"Commander {model.commander.name} | {len(model.units)} units | " + "Overdue first · Enter/click a unit to expand · Esc to return") + self.query_one("#commander-alarm-lines", Static).update( + "Alarms\n" + ("\n".join(alarm.line for alarm in model.alarms) + or "No stalled orders or recorded failed deliveries.")) + + @on(Collapsible.Expanded, "CommanderUnit") + @on(Collapsible.Collapsed, "CommanderUnit") + def expansion_changed(self, event: Collapsible.Toggled) -> None: + self.request = replace(self.request, expanded=frozenset( + row.unit for row in self.rows.values() if not row.collapsed)) + self.app.coordination_access.show_commander(self) diff --git a/src/toad/slash_command.py b/src/toad/slash_command.py index 951453de2..3d145ce86 100644 --- a/src/toad/slash_command.py +++ b/src/toad/slash_command.py @@ -115,6 +115,26 @@ def parse(cls, arguments: str) -> Self: return cls() +@dataclass(frozen=True) +class CommanderCommand(SlashCommand, LocalCommand): + help = "Open Core's live commander board and alarms" + hint = "" + commander: str + + @classmethod + def parse(cls, arguments: str) -> Self: + names = shlex.split(arguments) + if len(names) != 1: + raise ValueError("Use /commander .") + return cls(names[0]) + + async def apply(self, conversation: Conversation) -> bool: + from toad.screens.commander import CommanderScreen + + conversation.app.push_screen(CommanderScreen(self.commander)) + return True + + class LoginCommand(NoArgumentsCommand): help = "Connect a provider using the agent's native login UI" From 448296f4648e0666488b822db7f8912dcbfafe73 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:15:04 -0400 Subject: [PATCH 2/6] Retain card titles and render each recent turn on one line --- src/toad/screens/commander.py | 21 ++++++++++++++------- src/toad/slash_command.py | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/toad/screens/commander.py b/src/toad/screens/commander.py index 00d3b598a..6b8365f62 100644 --- a/src/toad/screens/commander.py +++ b/src/toad/screens/commander.py @@ -2,25 +2,30 @@ from __future__ import annotations from dataclasses import replace -from agent_comms.thread_identity import ThreadIncarnation -from agent_comms.ui_model.commander import CommanderModel, ObserveCommander, UnitCard + from textual import containers, on from textual.app import ComposeResult from textual.content import Content from textual.widgets import Collapsible, Static - from toad.screens.modal import ModalScreen +from agent_comms.thread_identity import ThreadIncarnation +from agent_comms.ui_model.commander import CommanderModel, ObserveCommander, UnitCard + class CommanderUnit(Collapsible): DEFAULT_CSS = """ - CommanderUnit { padding: 0; margin: 0; } + CommanderUnit { border: none; padding: 0; margin: 0; } CommanderUnit > CollapsibleTitle { height: 1; text-overflow: ellipsis; } CommanderUnit Static { height: auto; } + CommanderUnit .commander-turns { text-wrap: nowrap; text-overflow: ellipsis; } """ def __init__(self, card: UnitCard) -> None: - super().__init__(Static(card.detail.text, markup=False), + self.detail_widget = Static(card.detail.text, markup=False) + self.turns_widget = Static("\n".join(card.detail.turn_lines), markup=False, + classes="commander-turns") + super().__init__(self.detail_widget, self.turns_widget, title=Content(card.line), collapsed=True) self.unit = card.unit self.position = 0 @@ -28,7 +33,8 @@ def __init__(self, card: UnitCard) -> None: def present(self, card: UnitCard, position: int) -> None: self.position = position self.title = Content(card.line) - self.query_one(Static).update(card.detail.text) + self.detail_widget.update(card.detail.text) + self.turns_widget.update("\n".join(card.detail.turn_lines)) class CommanderScreen(ModalScreen[None]): @@ -77,6 +83,7 @@ def commander_failed(self, error: Exception) -> None: for row in self.rows.values(): row.remove() self.rows.clear() + self.query_one("#commander-alarm-lines", Static).update("Alarms unavailable.") def commander_observed(self, model: CommanderModel) -> None: board = self.query_one("#commander-board", containers.VerticalScroll) @@ -96,7 +103,7 @@ def commander_observed(self, model: CommanderModel) -> None: "Overdue first · Enter/click a unit to expand · Esc to return") self.query_one("#commander-alarm-lines", Static).update( "Alarms\n" + ("\n".join(alarm.line for alarm in model.alarms) - or "No stalled orders or recorded failed deliveries.")) + or "No stalled orders or recorded send/delivery alarms.")) @on(Collapsible.Expanded, "CommanderUnit") @on(Collapsible.Collapsed, "CommanderUnit") diff --git a/src/toad/slash_command.py b/src/toad/slash_command.py index 3d145ce86..3bdbcd699 100644 --- a/src/toad/slash_command.py +++ b/src/toad/slash_command.py @@ -119,7 +119,7 @@ def parse(cls, arguments: str) -> Self: class CommanderCommand(SlashCommand, LocalCommand): help = "Open Core's live commander board and alarms" hint = "" - commander: str + commander: str = "" @classmethod def parse(cls, arguments: str) -> Self: From 292bdaa5df068fe330e36fae7d7ffcf3bea1aaee Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:23:57 -0400 Subject: [PATCH 3/6] Resolve page identity through Core UI observation only --- src/toad/screens/commander.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/toad/screens/commander.py b/src/toad/screens/commander.py index 6b8365f62..6af08325f 100644 --- a/src/toad/screens/commander.py +++ b/src/toad/screens/commander.py @@ -10,7 +10,12 @@ from toad.screens.modal import ModalScreen from agent_comms.thread_identity import ThreadIncarnation -from agent_comms.ui_model.commander import CommanderModel, ObserveCommander, UnitCard +from agent_comms.ui_model.commander import ( + CommanderModel, + ObserveCommander, + ReadCommanderIdentity, + UnitCard, +) class CommanderUnit(Collapsible): @@ -66,11 +71,9 @@ def compose(self) -> ComposeResult: async def on_mount(self) -> None: access = self.app.coordination_access try: - def identify(): - observed = access.observation.observed() - return ObserveCommander(str(observed.service.root), - observed.service.registry.require(self.commander_name).incarnation) - self.request = await access.preparation.run_thread(identify) + observed = await access.preparation.run_thread(access.observation.observed) + self.request = await observed.process.read(ReadCommanderIdentity( + str(observed.service.root), self.commander_name)) access.show_commander(self) except (ValueError, OSError, RuntimeError) as error: self.commander_failed(error) From 555e72b6e35827f20f0aca871785179f6ab97970 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 16:21:43 -0400 Subject: [PATCH 4/6] Keep Core-derived cadence forecasts visible on collapsed commander cards --- src/toad/screens/commander.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/toad/screens/commander.py b/src/toad/screens/commander.py index 6af08325f..3e25f0e44 100644 --- a/src/toad/screens/commander.py +++ b/src/toad/screens/commander.py @@ -34,12 +34,19 @@ def __init__(self, card: UnitCard) -> None: title=Content(card.line), collapsed=True) self.unit = card.unit self.position = 0 + self.momentum_widget = Static("\n".join(item.line for item in card.momentum), markup=False) + + def compose(self) -> ComposeResult: + yield from super().compose() + # Outside Collapsible's contents: the forecast stays visible when closed. + yield self.momentum_widget def present(self, card: UnitCard, position: int) -> None: self.position = position self.title = Content(card.line) self.detail_widget.update(card.detail.text) self.turns_widget.update("\n".join(card.detail.turn_lines)) + self.momentum_widget.update("\n".join(item.line for item in card.momentum)) class CommanderScreen(ModalScreen[None]): From e6c417a2ea9f3a6c3d6e0f7bdc9dced7e86b1746 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 16:54:32 -0400 Subject: [PATCH 5/6] Derive goal resize capability from composed GoalBars, not an optional cached child --- src/toad/widgets/conversation.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/toad/widgets/conversation.py b/src/toad/widgets/conversation.py index aa401f9c5..ea74bf4fd 100644 --- a/src/toad/widgets/conversation.py +++ b/src/toad/widgets/conversation.py @@ -910,8 +910,7 @@ def compose(self) -> ComposeResult: status=Conversation.status, ) yield self.throbber - self.goal_bar: GoalBar = GoalBar(self, throbber=self.throbber, prompt=prompt) - yield self.goal_bar + yield GoalBar(self, throbber=self.throbber, prompt=prompt) yield prompt @property @@ -1622,11 +1621,13 @@ async def post_welcome(self) -> None: def on_resize(self) -> None: # A goal can retain its own size while the surrounding viewport changes. # Reapply its user-selected height against the new viewport bound. - self.goal_bar.update_document_height() + for goal_bar in self.query(GoalBar): + goal_bar.update_document_height() def on_mouse_down(self, event: events.MouseDown) -> None: self._mouse_down_offset = event.screen_offset - self.goal_bar.begin_separator_resize(event) + for goal_bar in self.query(GoalBar): + goal_bar.begin_separator_resize(event) def on_click(self, event: events.Click) -> None: from toad.screens.session_view import SessionView From 83393d08a9aac072637b0f918abeaa0b777c0cca Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 17:14:28 -0400 Subject: [PATCH 6/6] Publish Toad inline type declarations for paired application tools --- src/toad/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/toad/py.typed diff --git a/src/toad/py.typed b/src/toad/py.typed new file mode 100644 index 000000000..e69de29bb