From c7eaeb6b631586d007f7adb7e247dd42e8a51a1e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 12 Nov 2026 22:35:48 -0500 Subject: [PATCH] Keep inspected session fields and focus with their original owners --- .../advertisements.plan.json | 81 +++++++++++++++++++ .../advertisements_author.py | 43 ++++++++++ .../inspection.plan.json | 43 ++++++++++ .../inspection_author.py | 18 +++++ .../retire-diagnostic.plan.json | 20 +++++ .../retire_diagnostic.py | 8 ++ .../select-observation.plan.json | 20 +++++ src/toad/acp/agent_configuration.py | 6 +- src/toad/configured_view.py | 3 + src/toad/screens/main.py | 2 +- src/toad/widgets/model_switcher.py | 23 +++++- tools/physical_ui_remote.py | 2 +- 12 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 .verification/object-settings-sources/advertisements.plan.json create mode 100644 .verification/object-settings-sources/advertisements_author.py create mode 100644 .verification/object-settings-sources/inspection.plan.json create mode 100644 .verification/object-settings-sources/inspection_author.py create mode 100644 .verification/object-settings-sources/retire-diagnostic.plan.json create mode 100644 .verification/object-settings-sources/retire_diagnostic.py create mode 100644 .verification/object-settings-sources/select-observation.plan.json diff --git a/.verification/object-settings-sources/advertisements.plan.json b/.verification/object-settings-sources/advertisements.plan.json new file mode 100644 index 000000000..30f693858 --- /dev/null +++ b/.verification/object-settings-sources/advertisements.plan.json @@ -0,0 +1,81 @@ +{ + "recipes": [ + { + "recipe_id": "retain-empty-advertisements-and-popup-setup-as-distinct-views", + "operations": [ + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/acp/agent_configuration.py", + "target_qualname": "AgentConfiguration.editing", + "replacements": [ + { + "old_source": " declarations = [\n (f\"option_{index}\", setting.annotation, field(default=setting.current))\n for index, setting in enumerate(self.picker_settings)\n ]\n return ObjectState(make_dataclass(\"SessionConfiguration\", declarations)())", + "new_source": " return self.editing_settings(self.picker_settings)\n\n def editing_settings(self, settings: tuple[ConfigurationSetting, ...]) -> ObjectState:\n \"\"\"Project original settings without adding fields the source did not advertise.\"\"\"\n declarations = [\n (f\"option_{index}\", setting.annotation, field(default=setting.current))\n for index, setting in enumerate(settings)\n ]\n return ObjectState(make_dataclass(\"SessionConfiguration\", declarations)())" + } + ] + }, + { + "operation": "insert_before_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/model_switcher.py", + "target_qualname": "SessionForm.for_prompt", + "source": " @classmethod\n def settings(cls, configuration):\n return tuple(configuration.selections.values())\n\n" + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/model_switcher.py", + "target_qualname": "SessionForm.for_prompt", + "replacements": [ + { + "old_source": " state = prompt.agent_binding.configuration.editing()", + "new_source": " configuration = prompt.agent_binding.configuration\n state = configuration.editing_settings(cls.settings(configuration))" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/model_switcher.py", + "target_qualname": "SessionForm.represents", + "replacements": [ + { + "old_source": "for row in self.values.values()) == settings.picker_settings", + "new_source": "for row in self.values.values()) == self.settings(settings)" + } + ] + }, + { + "operation": "insert_before_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/model_switcher.py", + "target_qualname": "PopupSessionForm.open_model", + "source": " @classmethod\n def settings(cls, configuration):\n return configuration.picker_settings\n\n" + }, + { + "operation": "ensure_import", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "import_source": "from textual.widgets import Static" + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "target_qualname": "AgentProperties.compose", + "replacements": [ + { + "old_source": " yield self.form", + "new_source": " if not self.form.values:\n yield Static(\"This session has not advertised any settings.\", markup=False)\n yield self.form" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/screens/main.py", + "target_qualname": "MainScreen.configuration_panels", + "replacements": [ + { + "old_source": " yield from self.operational_sources.configuration_panels(self)\n yield from super().configuration_panels", + "new_source": " yield from super().configuration_panels\n yield from self.operational_sources.configuration_panels(self)" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.verification/object-settings-sources/advertisements_author.py b/.verification/object-settings-sources/advertisements_author.py new file mode 100644 index 000000000..1c00693e3 --- /dev/null +++ b/.verification/object-settings-sources/advertisements_author.py @@ -0,0 +1,43 @@ +import json +from pathlib import Path +r=Path.cwd();ops=[] +def patch(f,t,o,n):ops.append(dict(operation='patch_target',file_path=str(r/f),target_qualname=t,replacements=[dict(old_source=o,new_source=n)])) +def insert(f,t,s):ops.append(dict(operation='insert_before_target',file_path=str(r/f),target_qualname=t,source=s)) +f='src/toad/acp/agent_configuration.py' +patch(f,'AgentConfiguration.editing',''' declarations = [ + (f"option_{index}", setting.annotation, field(default=setting.current)) + for index, setting in enumerate(self.picker_settings) + ] + return ObjectState(make_dataclass("SessionConfiguration", declarations)())''',''' return self.editing_settings(self.picker_settings) + + def editing_settings(self, settings: tuple[ConfigurationSetting, ...]) -> ObjectState: + """Project original settings without adding fields the source did not advertise.""" + declarations = [ + (f"option_{index}", setting.annotation, field(default=setting.current)) + for index, setting in enumerate(settings) + ] + return ObjectState(make_dataclass("SessionConfiguration", declarations)())''') +f='src/toad/widgets/model_switcher.py' +insert(f,'SessionForm.for_prompt',''' @classmethod + def settings(cls, configuration): + return tuple(configuration.selections.values()) + +''') +patch(f,'SessionForm.for_prompt',''' state = prompt.agent_binding.configuration.editing()''',''' configuration = prompt.agent_binding.configuration + state = configuration.editing_settings(cls.settings(configuration))''') +patch(f,'SessionForm.represents','''for row in self.values.values()) == settings.picker_settings''','''for row in self.values.values()) == self.settings(settings)''') +insert(f,'PopupSessionForm.open_model',''' @classmethod + def settings(cls, configuration): + return configuration.picker_settings + +''') +f='src/toad/configured_view.py' +ops.append(dict(operation='ensure_import',file_path=str(r/f),import_source='from textual.widgets import Static')) +patch(f,'AgentProperties.compose',''' yield self.form''',''' if not self.form.values: + yield Static("This session has not advertised any settings.", markup=False) + yield self.form''') +f='src/toad/screens/main.py' +patch(f,'MainScreen.configuration_panels',''' yield from self.operational_sources.configuration_panels(self) + yield from super().configuration_panels''',''' yield from super().configuration_panels + yield from self.operational_sources.configuration_panels(self)''') +Path('.verification/object-settings-sources/advertisements.plan.json').write_text(json.dumps({'recipes':[{'recipe_id':'retain-empty-advertisements-and-popup-setup-as-distinct-views','operations':ops}]},indent=2)) diff --git a/.verification/object-settings-sources/inspection.plan.json b/.verification/object-settings-sources/inspection.plan.json new file mode 100644 index 000000000..175cfcedb --- /dev/null +++ b/.verification/object-settings-sources/inspection.plan.json @@ -0,0 +1,43 @@ +{ + "recipes": [ + { + "recipe_id": "read-the-held-coverage-and-retain-form-focus", + "operations": [ + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/tools/physical_ui_remote.py", + "target_qualname": "ObserveOperation.execute", + "replacements": [ + { + "old_source": "history.state.reports_coverage for history in histories", + "new_source": "history.state.coverage.retained_through(history) for history in histories" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/model_switcher.py", + "target_qualname": "SessionModelEditor.opened", + "replacements": [ + { + "old_source": "self.focus_content(False)", + "new_source": "self.query_ancestor(SessionForm).open_model(self)" + } + ] + }, + { + "operation": "insert_before_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/model_switcher.py", + "target_qualname": "SessionForm.dismiss_control", + "source": " def open_model(self, editor):\n \"\"\"An inline form paints its model choices without taking keyboard focus.\"\"\"\n editor.configuration_changed()\n\n" + }, + { + "operation": "insert_before_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/model_switcher.py", + "target_qualname": "PopupSessionForm.dismiss_control", + "source": " def open_model(self, editor):\n if self.query_ancestor(ModelSwitcher).is_open:\n editor.focus_content(False)\n\n" + } + ] + } + ] +} \ No newline at end of file diff --git a/.verification/object-settings-sources/inspection_author.py b/.verification/object-settings-sources/inspection_author.py new file mode 100644 index 000000000..8e051f0db --- /dev/null +++ b/.verification/object-settings-sources/inspection_author.py @@ -0,0 +1,18 @@ +import json +from pathlib import Path +r=Path.cwd();ops=[] +def patch(f,t,o,n):ops.append(dict(operation='patch_target',file_path=str(r/f),target_qualname=t,replacements=[dict(old_source=o,new_source=n)])) +def insert(f,t,s):ops.append(dict(operation='insert_before_target',file_path=str(r/f),target_qualname=t,source=s)) +patch('tools/physical_ui_remote.py','ObserveOperation.execute','history.state.reports_coverage for history in histories','history.state.coverage.retained_through(history) for history in histories') +patch('src/toad/widgets/model_switcher.py','SessionModelEditor.opened','self.focus_content(False)','self.query_ancestor(SessionForm).open_model(self)') +insert('src/toad/widgets/model_switcher.py','SessionForm.dismiss_control',''' def open_model(self, editor): + """An inline form paints its model choices without taking keyboard focus.""" + editor.configuration_changed() + +''') +insert('src/toad/widgets/model_switcher.py','PopupSessionForm.dismiss_control',''' def open_model(self, editor): + if self.query_ancestor(ModelSwitcher).is_open: + editor.focus_content(False) + +''') +Path('.verification/object-settings-sources/inspection.plan.json').write_text(json.dumps({'recipes':[{'recipe_id':'read-the-held-coverage-and-retain-form-focus','operations':ops}]},indent=2)) diff --git a/.verification/object-settings-sources/retire-diagnostic.plan.json b/.verification/object-settings-sources/retire-diagnostic.plan.json new file mode 100644 index 000000000..a8b518572 --- /dev/null +++ b/.verification/object-settings-sources/retire-diagnostic.plan.json @@ -0,0 +1,20 @@ +{ + "recipes": [ + { + "recipe_id": "retire-temporary-select-geometry-observation", + "operations": [ + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/tools/physical_ui_remote.py", + "target_qualname": "ObserveOperation.execute", + "replacements": [ + { + "old_source": " from textual.widgets import Select\n from textual.widgets._select import SelectOverlay\n (directory / \"select-layout.json\").write_text(json.dumps([\n dict(select=type(select).__name__, expanded=select.expanded,\n overlay=type(overlay).__name__, displayed=overlay.display,\n geometry=str(screen._compositor._published_map.get(overlay)),\n source=str(screen._compositor._published_map.get(select)))\n for select in screen.query(Select) for overlay in select.query(SelectOverlay)\n ], indent=2))\n frontend = FrontendIdentity(", + "new_source": " frontend = FrontendIdentity(" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.verification/object-settings-sources/retire_diagnostic.py b/.verification/object-settings-sources/retire_diagnostic.py new file mode 100644 index 000000000..f1c84f7e5 --- /dev/null +++ b/.verification/object-settings-sources/retire_diagnostic.py @@ -0,0 +1,8 @@ +import json +from pathlib import Path +r=Path.cwd();p=json.loads(Path('.verification/object-settings-sources/select-observation.plan.json').read_text()) +p['recipes'][0]['recipe_id']='retire-temporary-select-geometry-observation' +for op in p['recipes'][0]['operations']: + for pair in op['replacements']: + pair['old_source'],pair['new_source']=pair['new_source'],pair['old_source'] +Path('.verification/object-settings-sources/retire-diagnostic.plan.json').write_text(json.dumps(p,indent=2)) diff --git a/.verification/object-settings-sources/select-observation.plan.json b/.verification/object-settings-sources/select-observation.plan.json new file mode 100644 index 000000000..22d23c713 --- /dev/null +++ b/.verification/object-settings-sources/select-observation.plan.json @@ -0,0 +1,20 @@ +{ + "recipes": [ + { + "recipe_id": "retain-the-actual-dropdown-paint-placement", + "operations": [ + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/tools/physical_ui_remote.py", + "target_qualname": "ObserveOperation.execute", + "replacements": [ + { + "old_source": " frontend = FrontendIdentity(", + "new_source": " from textual.widgets import Select\n from textual.widgets._select import SelectOverlay\n (directory / \"select-layout.json\").write_text(json.dumps([\n dict(select=type(select).__name__, expanded=select.expanded,\n overlay=type(overlay).__name__, displayed=overlay.display,\n geometry=str(screen._compositor._published_map.get(overlay)),\n source=str(screen._compositor._published_map.get(select)))\n for select in screen.query(Select) for overlay in select.query(SelectOverlay)\n ], indent=2))\n frontend = FrontendIdentity(" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/toad/acp/agent_configuration.py b/src/toad/acp/agent_configuration.py index 800c312cd..c83a55cbe 100644 --- a/src/toad/acp/agent_configuration.py +++ b/src/toad/acp/agent_configuration.py @@ -239,9 +239,13 @@ def named(self, key: str) -> ConfigurationSetting: def editing(self) -> ObjectState: """An editable projection; every field retains its exact advertisement and writer.""" + return self.editing_settings(self.picker_settings) + + def editing_settings(self, settings: tuple[ConfigurationSetting, ...]) -> ObjectState: + """Project original settings without adding fields the source did not advertise.""" declarations = [ (f"option_{index}", setting.annotation, field(default=setting.current)) - for index, setting in enumerate(self.picker_settings) + for index, setting in enumerate(settings) ] return ObjectState(make_dataclass("SessionConfiguration", declarations)()) diff --git a/src/toad/configured_view.py b/src/toad/configured_view.py index be13378d9..2dfa62fb0 100644 --- a/src/toad/configured_view.py +++ b/src/toad/configured_view.py @@ -4,6 +4,7 @@ from metaclass_registry.mro_dispatch import handles from textual import events, on, work from textual.geometry import Region +from textual.widgets import Static from toad.core.events import ConfigurationChanged from toad.core_event_carrier import CoreEventReceiver from toad.forms.object_configuration import ObjectFieldView, ObjectFormView @@ -60,6 +61,8 @@ def panel(cls, view): def compose(self): self.form = SessionForm.for_prompt(self.view.conversation.prompt) + if not self.form.values: + yield Static("This session has not advertised any settings.", markup=False) yield self.form def follow_binding(self): diff --git a/src/toad/screens/main.py b/src/toad/screens/main.py index be987ef54..e6fcff658 100644 --- a/src/toad/screens/main.py +++ b/src/toad/screens/main.py @@ -342,8 +342,8 @@ def initial_editing_mode(self) -> EditingMode: @property def configuration_panels(self): - yield from self.operational_sources.configuration_panels(self) yield from super().configuration_panels + yield from self.operational_sources.configuration_panels(self) def keyboard_scopes(self) -> tuple[DeclaredWidgetActions, ...]: return self.content.keyboard_scopes(self) diff --git a/src/toad/widgets/model_switcher.py b/src/toad/widgets/model_switcher.py index f66914b1e..3a946b92b 100644 --- a/src/toad/widgets/model_switcher.py +++ b/src/toad/widgets/model_switcher.py @@ -36,7 +36,7 @@ def bind_control(self) -> None: self.listen(events.Mount, self.opened) def opened(self, event: events.Mount) -> None: - self.focus_content(False) + self.query_ancestor(SessionForm).open_model(self) def compose(self) -> ComposeResult: yield from self.compose_content() @@ -72,17 +72,26 @@ def __init__(self, state, parts, prompt): super().__init__(state, parts) self.prompt = prompt + @classmethod + def settings(cls, configuration): + return tuple(configuration.selections.values()) + @classmethod def for_prompt(cls, prompt): - state = prompt.agent_binding.configuration.editing() + configuration = prompt.agent_binding.configuration + state = configuration.editing_settings(cls.settings(configuration)) return cls(state, cls.field_family.of(state), prompt) + def open_model(self, editor): + """An inline form paints its model choices without taking keyboard focus.""" + editor.configuration_changed() + def dismiss_control(self): """An inline selection leaves its form open.""" def represents(self, settings) -> bool: return tuple(row.field.metadata(AdvertisedSetting).setting - for row in self.values.values()) == settings.picker_settings + for row in self.values.values()) == self.settings(settings) def values_changed(self, paths: set[str]) -> None: super().values_changed(paths) @@ -96,6 +105,14 @@ def values_changed(self, paths: set[str]) -> None: class PopupSessionForm(SessionForm): """The popup adds dismissal to the same advertised settings form.""" + @classmethod + def settings(cls, configuration): + return configuration.picker_settings + + def open_model(self, editor): + if self.query_ancestor(ModelSwitcher).is_open: + editor.focus_content(False) + def dismiss_control(self): self.query_ancestor(ModelSwitcher).dismiss_control() diff --git a/tools/physical_ui_remote.py b/tools/physical_ui_remote.py index 557657672..f0fb7e12c 100644 --- a/tools/physical_ui_remote.py +++ b/tools/physical_ui_remote.py @@ -584,7 +584,7 @@ def execute(self, directory: Path) -> None: # The pager's original publication state also supplies the footer's # saved-history availability. Agent status is an independent fact. history_published = bool(histories) and all( - history.state.reports_coverage for history in histories) + history.state.coverage.retained_through(history) for history in histories) frontend = FrontendIdentity( self.identity, Path(toad.__file__).resolve(), Path(agent_comms.__file__).resolve(), Path(textual.__file__).resolve(),