diff --git a/src/toad/target_commands.py b/src/toad/target_commands.py index 473db0640..62cdd28e6 100644 --- a/src/toad/target_commands.py +++ b/src/toad/target_commands.py @@ -12,7 +12,7 @@ from agent_comms.comms import Comms from agent_comms.channel_targets import CommandTarget from toad import messages -from agent_comms.route_selection import root_is_current +from agent_comms.route_selection import RouteChanged, root_is_current from toad.slash_command import CommandPresentation, LocalCommand, SlashCommand from agent_comms.cli_commands import CliCommand, TargetAction from toad.thread_actions import ThreadAction @@ -43,7 +43,7 @@ def title(self): def current(self): if not root_is_current(self.comms.root): - raise ValueError('Comms route changed; reopen this view') + raise RouteChanged('Comms route changed; reopen this view') return self def available_actions(self): diff --git a/src/toad/thread_actions.py b/src/toad/thread_actions.py index 0b7d6bc87..0769d626e 100644 --- a/src/toad/thread_actions.py +++ b/src/toad/thread_actions.py @@ -9,7 +9,8 @@ from typing import TYPE_CHECKING from agent_comms.cli_commands import TargetAction, TargetEdit, TargetBatchResult, TargetFailed -from agent_comms.route_selection import RouteSelection +from agent_comms.native_pi import NativePiUnavailable +from agent_comms.route_selection import RouteChanged, RouteSelection if TYPE_CHECKING: from toad.app import ToadApp @@ -141,11 +142,11 @@ async def prepare(): fields = await ctx.app.preparation.run_thread( lambda: asyncio.run(definition.prepare_fields())) ctx.current() - except (OSError, ValueError, RuntimeError) as error: + except (NativePiUnavailable, RouteChanged) as error: ctx.app.notify(str(error), title=definition.label, severity='error') return if ctx.app.screen is screen: - ctx.app.push_screen(CommandDialog(definition, ctx.title, fields, ctx.comms), accepted) + ctx.app.push_screen(CommandDialog(definition, ctx.title, fields), accepted) else: await accepted({}) ctx.app.run_worker(prepare, name='command-fields') diff --git a/src/toad/widgets/comms_command_dialog.py b/src/toad/widgets/comms_command_dialog.py index b537fce3e..18d7a6ab5 100644 --- a/src/toad/widgets/comms_command_dialog.py +++ b/src/toad/widgets/comms_command_dialog.py @@ -5,7 +5,7 @@ from agent_comms.cli_commands import CatalogTargetField, TargetAction, TargetField from agent_comms.login import run_login -from agent_comms.comms import Comms +from agent_comms.native_pi import NativePiUnavailable from textual import on, work from textual.app import ComposeResult @@ -39,6 +39,11 @@ def configure(self, parameter: CatalogTargetField): else '') self.update_label() + @property + def prepared(self) -> bool: + """The catalogs were prepared for this field's current value.""" + return self.value == self.parameter.editor_default + def update_label(self): self.label = next((choice.name for choice in self.parameter.options if choice.value == self.value), 'Choose / connect a provider…') @@ -94,8 +99,8 @@ async def connect_provider(self): command = self.editor.query_ancestor(CommandDialog) try: with self.app.suspend(): - result = await asyncio.to_thread(run_login, comms=command.comms) - except (OSError, ValueError, RuntimeError) as error: + result = await asyncio.to_thread(run_login) + except OSError as error: self.notify(str(error), title='Provider login', severity='error') return if result == 0 and self.is_attached: @@ -131,11 +136,9 @@ class CommandDialog(ModalScreen[dict[str, str]]): """ BINDINGS = [('escape', 'cancel', 'Cancel'), ('ctrl+enter', 'submit', 'Apply')] - def __init__(self, definition: TargetAction, target: str, - fields: tuple[TargetField, ...], comms: Comms): + def __init__(self, definition: TargetAction, target: str, fields: tuple[TargetField, ...]): super().__init__() - self.definition, self.target = definition, target - self.fields, self.comms = fields, comms + self.definition, self.target, self.fields = definition, target, fields def compose(self) -> ComposeResult: with Vertical(id='command-box'): @@ -146,7 +149,7 @@ def compose(self) -> ComposeResult: for parameter in self.fields: key = parameter.name yield Static(parameter.description, markup=False) - if parameter.catalog is not None: + if isinstance(parameter, CatalogTargetField): yield CatalogField(parameter) elif (choices := parameter.choices) is not None: value = parameter.editor_default @@ -208,26 +211,29 @@ def arguments(self): **{editor.name: editor.value for editor in self.query(CatalogField)}} async def refresh_catalogs(self, arguments): - requested = {item.name: arguments[item.name] for item in self.fields if item.catalog is not None} + requested = {editor.name: arguments[editor.name] for editor in self.query(CatalogField)} def unchanged(): current = self.arguments() return all(current[name] == value for name, value in requested.items()) + try: + edited = self.definition.edited(arguments) + except ValueError: + # Invalid form input; the confirmation line already reports it. + return self.query_one('#command-apply', Button).disabled = True try: fields = await self.app.preparation.run_thread( - lambda: asyncio.run(self.definition.edited(arguments).prepare_fields())) - except (OSError, ValueError, RuntimeError) as error: + lambda: asyncio.run(edited.prepare_fields())) + except NativePiUnavailable as error: if self.is_attached and unchanged(): self.notify(str(error), title='Configuration choices', severity='error') return if not self.is_attached or not unchanged(): return - self.fields = fields + prepared = {parameter.name: parameter for parameter in fields} with self.prevent(CatalogField.Changed): - for parameter in fields: - if parameter.catalog is not None: - editor = self.query_one('#command-field-' + parameter.name.replace('_', '-'), CatalogField) - editor.configure(parameter) + for editor in self.query(CatalogField): + editor.configure(prepared[editor.name]) self.update_confirmation() def update_confirmation(self): @@ -242,9 +248,9 @@ def update_confirmation(self): if not editor.value), None) async def prepare(): def read(): + if missing_choice is not None: + return f'Choose {missing_choice.replace("_", " ")}', False try: - if missing_choice is not None: - raise ValueError(f'Choose {missing_choice.replace("_", " ")}') return self.definition.edited(arguments).confirmation, True except (KeyError, ValueError, TypeError) as error: return str(error), False @@ -253,9 +259,8 @@ def read(): return self.query_one('#command-confirmation', Static).update(confirmation) confirmed.display = valid and bool(confirmation) - self.query_one('#command-apply', Button).disabled = not valid or any( - item.catalog is not None and item.editor_default != arguments[item.name] - for item in self.fields) + self.query_one('#command-apply', Button).disabled = not valid or not all( + editor.prepared for editor in self.query(CatalogField)) self.run_worker(prepare, group='command-confirmation', exclusive=True) def on_select_changed(self, event: Select.Changed): @@ -265,9 +270,7 @@ def on_select_changed(self, event: Select.Changed): def on_catalog_changed(self, event: CatalogField.Changed): event.stop() self.update_confirmation() - if event.editor.value and any( - item.name == event.editor.name and item.catalog is not None - and item.editor_default != event.editor.value for item in self.fields): + if event.editor.value and not event.editor.prepared: self.run_worker(self.refresh_catalogs(self.arguments()), group='command-catalogs', exclusive=True)