diff --git a/src/toad/command_catalog.py b/src/toad/command_catalog.py index 149039867..1aede3bb0 100644 --- a/src/toad/command_catalog.py +++ b/src/toad/command_catalog.py @@ -1,21 +1,16 @@ """One derived command view joins ACP advertisements and local declarations.""" -import asyncio from dataclasses import dataclass from collections.abc import Awaitable, Sequence from typing import TYPE_CHECKING from toad.slash_command import AgentAdvertisedCommand, CommandPresentation, LocalCommand, SlashCommand -from toad.target_commands import TargetContext, ThreadCommand +from toad.target_commands import CommandContextChanged, TargetContext, ThreadCommand if TYPE_CHECKING: from toad.widgets.conversation import Conversation -class CommandDiscoveryCancelled(asyncio.CancelledError): - """The view that requested commands changed before discovery completed.""" - - @dataclass(frozen=True) class CommandCatalog: entries: tuple[CommandPresentation, ...] @@ -73,7 +68,7 @@ async def execute(cls, text: str, conversation: Conversation) -> bool: if command is None: return False return await command.parse_arguments(arguments).apply(conversation) - except CommandDiscoveryCancelled: + except CommandContextChanged: # A recognized UI command must never fall through to agent input # because its original target disappeared during an awaited read. conversation.app.notify( diff --git a/src/toad/target_commands.py b/src/toad/target_commands.py index 756d2b0a8..069374ac6 100644 --- a/src/toad/target_commands.py +++ b/src/toad/target_commands.py @@ -6,6 +6,7 @@ from toad.core import session_requests from abc import ABC, abstractmethod +from asyncio import CancelledError from dataclasses import dataclass, field from pathlib import Path from functools import partial @@ -23,6 +24,10 @@ from toad.widgets.conversation import Conversation +class CommandContextChanged(CancelledError): + """A command's captured target context is no longer current.""" + + @dataclass(frozen=True) class TargetContext: app: ToadApp @@ -119,10 +124,8 @@ def parse_arguments(self, arguments: str) -> ContextualCommand: return self async def apply(self, conversation: Conversation, ctx: TargetContext) -> bool: - from toad.command_catalog import CommandDiscoveryCancelled - if await conversation.command_target_context() != ctx: - raise CommandDiscoveryCancelled("Command target changed; refresh this view") + raise CommandContextChanged("Command target changed; refresh this view") if not self.available(ctx): raise ValueError("Action is no longer available for this target") self.execute(ctx) diff --git a/src/toad/widgets/conversation.py b/src/toad/widgets/conversation.py index 6df12bde2..72b378487 100644 --- a/src/toad/widgets/conversation.py +++ b/src/toad/widgets/conversation.py @@ -54,7 +54,8 @@ UpCursor, require_blocks, ) -from toad.command_catalog import CommandCatalog, CommandDiscoveryCancelled +from toad.command_catalog import CommandCatalog +from toad.target_commands import CommandContextChanged, TargetContext from toad.conversation_comms_handlers import ConversationCommsHandlers from toad.conversation_submission import ConversationSubmissions from toad.conversation_turn import ConversationTurn, TurnOwner @@ -1507,7 +1508,6 @@ def ask( async def command_target_context(self): from toad.screens.main import MainScreen - from toad.target_commands import TargetContext screen = self.query_ancestor(MainScreen) nav = screen.navigation_context @@ -1515,7 +1515,7 @@ async def command_target_context(self): lambda: self.app.coordination_access.observation.observed().service ) if not self.is_attached or screen.navigation_context != nav: - raise CommandDiscoveryCancelled("Command view changed during discovery") + raise CommandContextChanged("Command view changed during discovery") return screen.thread_identity.command_target( lambda actor: TargetContext( self.app, comms, CommandTarget.named(actor), actor, nav.project_path, nav.owner_mode @@ -1551,7 +1551,7 @@ async def read_command_catalog(self) -> CommandCatalog: or source.navigation_context != navigation or agent is not self.agent_binding ): - raise CommandDiscoveryCancelled("Command view changed during discovery") + raise CommandContextChanged("Command view changed during discovery") return catalog @handles(core_events.LinkContextChanged)