Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions src/toad/command_catalog.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""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
Expand All @@ -11,6 +12,10 @@
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, ...]
Expand Down Expand Up @@ -60,10 +65,18 @@ def target_choices(self):

@classmethod
async def execute(cls, text: str, conversation: Conversation) -> bool:
catalog = await conversation.read_command_catalog()
name, _, arguments = text.partition(" ")
command = next((command for command in catalog.commands
if command.command == name), None)
if command is None:
return False
return await command.parse_arguments(arguments).apply(conversation)
try:
catalog = await conversation.read_command_catalog()
name, _, arguments = text.partition(" ")
command = next((command for command in catalog.commands
if command.command == name), None)
if command is None:
return False
return await command.parse_arguments(arguments).apply(conversation)
except CommandDiscoveryCancelled:
# A recognized UI command must never fall through to agent input
# because its original target disappeared during an awaited read.
conversation.app.notify(
"View changed; command was not run. Submit it again.", severity="warning"
)
return True
4 changes: 3 additions & 1 deletion src/toad/target_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,10 @@ 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 ValueError("Command target changed; refresh this view")
raise CommandDiscoveryCancelled("Command target changed; refresh this view")
if not self.available(ctx):
raise ValueError("Action is no longer available for this target")
self.execute(ctx)
Expand Down
6 changes: 3 additions & 3 deletions src/toad/widgets/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
UpCursor,
require_blocks,
)
from toad.command_catalog import CommandCatalog
from toad.command_catalog import CommandCatalog, CommandDiscoveryCancelled
from toad.conversation_comms_handlers import ConversationCommsHandlers
from toad.conversation_submission import ConversationSubmissions
from toad.conversation_turn import ConversationTurn, TurnOwner
Expand Down Expand Up @@ -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 ValueError("Command view changed during discovery")
raise CommandDiscoveryCancelled("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
Expand Down Expand Up @@ -1551,7 +1551,7 @@ async def read_command_catalog(self) -> CommandCatalog:
or source.navigation_context != navigation
or agent is not self.agent_binding
):
raise ValueError("Command view changed during discovery")
raise CommandDiscoveryCancelled("Command view changed during discovery")
return catalog

@handles(core_events.LinkContextChanged)
Expand Down
Loading