From 624f9dd154d2ed6eefb2957af6498be012b14473 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 18:13:54 -0400 Subject: [PATCH] P55: narrow the dialog's catches and decide catalog fields from their typed state The dialog chooses the catalog editor from Core's CatalogTargetField and asks each CatalogField whether its value is the one its catalogs were prepared for (CatalogField.prepared); the five 'parameter.catalog is not None' probes are gone, and catalog refresh configures each editor from the newly prepared fields by name. Each catch now names the failure it expects: NativePiUnavailable from catalog discovery, RouteChanged from TargetContext.current() (which now raises Core's RouteChanged), ValueError from decoding the form's input in edited(), and OSError from spawning the login program. Provider login calls run_login() as Core main declares it; CommandDialog no longer holds a Comms. The missing-choice line is returned instead of raised and caught. Co-Authored-By: Claude Opus 5.5 --- src/toad/target_commands.py | 4 +- src/toad/thread_actions.py | 7 ++-- src/toad/widgets/comms_command_dialog.py | 51 +++++++++++++----------- 3 files changed, 33 insertions(+), 29 deletions(-) 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)