diff --git a/.verification/object-settings-sources/agent-control.plan.json b/.verification/object-settings-sources/agent-control.plan.json new file mode 100644 index 000000000..89f748cb0 --- /dev/null +++ b/.verification/object-settings-sources/agent-control.plan.json @@ -0,0 +1,20 @@ +{ + "recipes": [ + { + "recipe_id": "selected-session-advertisement-control", + "operations": [ + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/tests/textual/test_sidebar_projection.py", + "target_qualname": "test_view_menu_discovers_settings_and_keeps_each_objects_state", + "replacements": [ + { + "old_source": " native_menu = native_bar.query_one(PanelMenu)\n relationships =", + "new_source": " native_menu = native_bar.query_one(PanelMenu)\n from acp.schema import SetSessionConfigOptionResponse\n from support.agents import AppAgents\n from toad.configured_view import AgentProperties\n from toad.widgets.model_switcher import SessionForm\n agent = AppAgents(app).detached_agent('sidebar-settings')\n native.conversation.bind_agent(agent)\n agent.configuration.receive(SetSessionConfigOptionResponse.model_validate({'configOptions': [\n {'id': 'sample-budget', 'name': 'Budget', 'type': 'boolean', 'currentValue': True},\n ]}).config_options)\n agent_entry = next(entry for entry in native_menu.entries if isinstance(entry.widget, AgentProperties))\n native_menu.picker.value = agent_entry\n await ui.until(lambda: agent_entry.widget.is_mounted and bool(agent_entry.widget.query(SessionForm)),\n why='the actual advertised session form')\n await ui.until(lambda: agent_entry.widget.form.represents(agent.configuration),\n why='new advertisements replace the original fields')\n assert agent_entry.widget.form.prompt is native.conversation.prompt\n assert 'Budget' in {row.field.label for row in agent_entry.widget.form.values.values()}\n agent.configuration.receive(SetSessionConfigOptionResponse.model_validate({'configOptions': [\n {'id': 'sample-budget', 'name': 'Budget', 'type': 'boolean', 'currentValue': False},\n {'id': 'new-setting', 'name': 'New setting', 'type': 'boolean', 'currentValue': True},\n ]}).config_options)\n await ui.until(lambda: agent_entry.widget.form.represents(agent.configuration),\n why='the sidebar follows the source update')\n assert 'New setting' in {row.field.label for row in agent_entry.widget.form.values.values()}\n relationships =" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.verification/object-settings-sources/agent.plan.json b/.verification/object-settings-sources/agent.plan.json new file mode 100644 index 000000000..ef13d06d9 --- /dev/null +++ b/.verification/object-settings-sources/agent.plan.json @@ -0,0 +1,163 @@ +{ + "recipes": [ + { + "recipe_id": "share-the-advertised-form-with-view-properties", + "operations": [ + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/model_switcher.py", + "target_qualname": "SessionModelEditor.history_scope", + "replacements": [ + { + "old_source": "self.query_ancestor(ModelSwitcher).prompt.model_history_scope", + "new_source": "self.query_ancestor(SessionForm).prompt.model_history_scope" + } + ] + }, + { + "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": " if self.query_ancestor(ModelSwitcher).is_open:\n self.focus_content(False)", + "new_source": " self.focus_content(False)" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/model_switcher.py", + "target_qualname": "SessionModelEditor.connect_provider", + "replacements": [ + { + "old_source": "self.publish_core(input_events.ProviderLogin())", + "new_source": "self.query_ancestor(SessionForm).prompt.publish_core(input_events.ProviderLogin())" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/model_switcher.py", + "target_qualname": "SessionModelEditor.dismiss_control", + "replacements": [ + { + "old_source": "self.query_ancestor(ModelSwitcher).dismiss_control()", + "new_source": "self.query_ancestor(SessionForm).dismiss_control()" + } + ] + }, + { + "operation": "insert_before_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/model_switcher.py", + "target_qualname": "SessionForm.represents", + "source": " def __init__(self, state, parts, prompt):\n super().__init__(state, parts)\n self.prompt = prompt\n\n @classmethod\n def for_prompt(cls, prompt):\n state = prompt.agent_binding.configuration.editing()\n return cls(state, cls.field_family.of(state), prompt)\n\n def dismiss_control(self):\n \"\"\"An inline selection leaves its form open.\"\"\"\n\n" + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/model_switcher.py", + "target_qualname": "SessionForm.values_changed", + "replacements": [ + { + "old_source": "self.publish_core(setting.selection_event(field.value))", + "new_source": "self.prompt.publish_core(setting.selection_event(field.value))" + } + ] + }, + { + "operation": "insert_before_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/model_switcher.py", + "target_qualname": "ModelSwitcher", + "source": "class PopupSessionForm(SessionForm):\n \"\"\"The popup adds dismissal to the same advertised settings form.\"\"\"\n\n def dismiss_control(self):\n self.query_ancestor(ModelSwitcher).dismiss_control()\n\n\n" + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/model_switcher.py", + "target_qualname": "ModelSwitcher.compose_content", + "replacements": [ + { + "old_source": "SessionForm.whole(self.prompt.agent_binding.configuration.editing())", + "new_source": "PopupSessionForm.for_prompt(self.prompt)" + } + ] + }, + { + "operation": "ensure_import", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "import_source": "from toad.widgets.model_switcher import SessionForm" + }, + { + "operation": "ensure_import", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "import_source": "from toad.core_event_carrier import CoreEventReceiver" + }, + { + "operation": "ensure_import", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "import_source": "from toad.core.events import ConfigurationChanged" + }, + { + "operation": "ensure_import", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "import_source": "from metaclass_registry.mro_dispatch import handles" + }, + { + "operation": "ensure_import", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "import_source": "from textual import work" + }, + { + "operation": "insert_before_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "target_qualname": "ObjectConfigurationDialog", + "source": "class AgentProperties(CoreEventReceiver, FillHeight, SidebarPanelWidget):\n \"\"\"The selected native session's advertisements, using its original writer.\"\"\"\n\n def __init__(self, view):\n super().__init__()\n self.view = view\n self.listen(events.Mount, self.follow_binding)\n\n @classmethod\n def panel(cls, view):\n return SideBar.Panel(\"Agent settings\", cls(view), id=\"agent-settings\", flex=True)\n\n def compose(self):\n self.form = SessionForm.for_prompt(self.view.conversation.prompt)\n yield self.form\n\n def follow_binding(self):\n self.watch(self.view.conversation, \"agent_binding\", self.binding_changed)\n\n def binding_changed(self, old, new):\n self.retire_core_observations(old.events)\n self.observe_core(new.events)\n self.configuration_changed()\n\n @handles(ConfigurationChanged)\n def configuration_changed(self):\n if self.is_mounted:\n self.rebuild_form()\n\n @work(exclusive=True, group=\"agent-settings\")\n async def rebuild_form(self):\n await self.recompose()\n\n def finish(self):\n self.form.finish()\n\n\n" + }, + { + "operation": "ensure_import", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/agent_binding.py", + "import_source": "from toad.core.events import CoreEventStream" + }, + { + "operation": "insert_before_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/agent_binding.py", + "target_qualname": "NoAgentConnection.presentation", + "source": " @cached_property\n def events(self):\n return CoreEventStream(self)\n\n" + }, + { + "operation": "insert_before_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/screens/main.py", + "target_qualname": "MainScreen.keyboard_scopes", + "source": " @property\n def configuration_panels(self):\n yield from self.operational_sources.configuration_panels(self)\n yield from super().configuration_panels\n\n" + }, + { + "operation": "ensure_import", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/session_presentation.py", + "import_source": "from toad.configured_view import AgentProperties" + }, + { + "operation": "insert_before_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/session_presentation.py", + "target_qualname": "OperationalSessionSources.prompt", + "source": " def configuration_panels(self, view):\n yield AgentProperties.panel(view)\n\n" + }, + { + "operation": "insert_before_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/session_presentation.py", + "target_qualname": "MailboxSessionSources.prompt", + "source": " def configuration_panels(self, view):\n \"\"\"A mailbox has no native session configuration to advertise.\"\"\"\n return ()\n\n" + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/forms/object_configuration.py", + "target_qualname": "ObjectFieldView", + "replacements": [ + { + "old_source": " @classmethod\n def admits(cls, field):\n return True", + "new_source": "" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.verification/object-settings-sources/agent_author.py b/.verification/object-settings-sources/agent_author.py new file mode 100644 index 000000000..29219ed5c --- /dev/null +++ b/.verification/object-settings-sources/agent_author.py @@ -0,0 +1,109 @@ +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)) +def imp(f,s):ops.append(dict(operation='ensure_import',file_path=str(r/f),import_source=s)) +f='src/toad/widgets/model_switcher.py' +patch(f,'SessionModelEditor.history_scope','self.query_ancestor(ModelSwitcher).prompt.model_history_scope','self.query_ancestor(SessionForm).prompt.model_history_scope') +patch(f,'SessionModelEditor.opened',''' if self.query_ancestor(ModelSwitcher).is_open: + self.focus_content(False)''',''' self.focus_content(False)''') +patch(f,'SessionModelEditor.connect_provider','self.publish_core(input_events.ProviderLogin())','self.query_ancestor(SessionForm).prompt.publish_core(input_events.ProviderLogin())') +patch(f,'SessionModelEditor.dismiss_control','self.query_ancestor(ModelSwitcher).dismiss_control()','self.query_ancestor(SessionForm).dismiss_control()') +insert(f,'SessionForm.represents',''' def __init__(self, state, parts, prompt): + super().__init__(state, parts) + self.prompt = prompt + + @classmethod + def for_prompt(cls, prompt): + state = prompt.agent_binding.configuration.editing() + return cls(state, cls.field_family.of(state), prompt) + + def dismiss_control(self): + """An inline selection leaves its form open.""" + +''') +patch(f,'SessionForm.values_changed','self.publish_core(setting.selection_event(field.value))','self.prompt.publish_core(setting.selection_event(field.value))') +insert(f,'ModelSwitcher','''class PopupSessionForm(SessionForm): + """The popup adds dismissal to the same advertised settings form.""" + + def dismiss_control(self): + self.query_ancestor(ModelSwitcher).dismiss_control() + + +''') +patch(f,'ModelSwitcher.compose_content','SessionForm.whole(self.prompt.agent_binding.configuration.editing())','PopupSessionForm.for_prompt(self.prompt)') +f='src/toad/configured_view.py' +imp(f,'from toad.widgets.model_switcher import SessionForm') +imp(f,'from toad.core_event_carrier import CoreEventReceiver') +imp(f,'from toad.core.events import ConfigurationChanged') +imp(f,'from metaclass_registry.mro_dispatch import handles') +imp(f,'from textual import work') +insert(f,'ObjectConfigurationDialog','''class AgentProperties(CoreEventReceiver, FillHeight, SidebarPanelWidget): + """The selected native session's advertisements, using its original writer.""" + + def __init__(self, view): + super().__init__() + self.view = view + self.listen(events.Mount, self.follow_binding) + + @classmethod + def panel(cls, view): + return SideBar.Panel("Agent settings", cls(view), id="agent-settings", flex=True) + + def compose(self): + self.form = SessionForm.for_prompt(self.view.conversation.prompt) + yield self.form + + def follow_binding(self): + self.watch(self.view.conversation, "agent_binding", self.binding_changed) + + def binding_changed(self, old, new): + self.retire_core_observations(old.events) + self.observe_core(new.events) + self.configuration_changed() + + @handles(ConfigurationChanged) + def configuration_changed(self): + if self.is_mounted: + self.rebuild_form() + + @work(exclusive=True, group="agent-settings") + async def rebuild_form(self): + await self.recompose() + + def finish(self): + self.form.finish() + + +''') +f='src/toad/agent_binding.py' +imp(f,'from toad.core.events import CoreEventStream') +insert(f,'NoAgentConnection.presentation',''' @cached_property + def events(self): + return CoreEventStream(self) + +''') +f='src/toad/screens/main.py' +insert(f,'MainScreen.keyboard_scopes',''' @property + def configuration_panels(self): + yield from self.operational_sources.configuration_panels(self) + yield from super().configuration_panels + +''') +f='src/toad/session_presentation.py' +imp(f,'from toad.configured_view import AgentProperties') +insert(f,'OperationalSessionSources.prompt',''' def configuration_panels(self, view): + yield AgentProperties.panel(view) + +''') +insert(f,'MailboxSessionSources.prompt',''' def configuration_panels(self, view): + """A mailbox has no native session configuration to advertise.""" + return () + +''') +f='src/toad/forms/object_configuration.py' +patch(f,'ObjectFieldView',''' @classmethod + def admits(cls, field): + return True''','') +Path('.verification/object-settings-sources/agent.plan.json').write_text(json.dumps({'recipes':[{'recipe_id':'share-the-advertised-form-with-view-properties','operations':ops}]},indent=2)) diff --git a/.verification/object-settings-sources/agent_control_author.py b/.verification/object-settings-sources/agent_control_author.py new file mode 100644 index 000000000..5b9e9f3c8 --- /dev/null +++ b/.verification/object-settings-sources/agent_control_author.py @@ -0,0 +1,33 @@ +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)])) +patch('tests/textual/test_sidebar_projection.py','test_view_menu_discovers_settings_and_keeps_each_objects_state', ''' native_menu = native_bar.query_one(PanelMenu) + relationships =''',''' native_menu = native_bar.query_one(PanelMenu) + from acp.schema import SetSessionConfigOptionResponse + from support.agents import AppAgents + from toad.configured_view import AgentProperties + from toad.widgets.model_switcher import SessionForm + agent = AppAgents(app).detached_agent('sidebar-settings') + native.conversation.bind_agent(agent) + agent.configuration.receive(SetSessionConfigOptionResponse.model_validate({'configOptions': [ + {'id': 'sample-budget', 'name': 'Budget', 'type': 'boolean', 'currentValue': True}, + ]}).config_options) + agent_entry = next(entry for entry in native_menu.entries if isinstance(entry.widget, AgentProperties)) + native_menu.picker.value = agent_entry + await ui.until(lambda: agent_entry.widget.is_mounted and bool(agent_entry.widget.query(SessionForm)), + why='the actual advertised session form') + await ui.until(lambda: agent_entry.widget.form.represents(agent.configuration), + why='new advertisements replace the original fields') + assert agent_entry.widget.form.prompt is native.conversation.prompt + assert 'Budget' in {row.field.label for row in agent_entry.widget.form.values.values()} + agent.configuration.receive(SetSessionConfigOptionResponse.model_validate({'configOptions': [ + {'id': 'sample-budget', 'name': 'Budget', 'type': 'boolean', 'currentValue': False}, + {'id': 'new-setting', 'name': 'New setting', 'type': 'boolean', 'currentValue': True}, + ]}).config_options) + await ui.until(lambda: agent_entry.widget.form.represents(agent.configuration), + why='the sidebar follows the source update') + assert 'New setting' in {row.field.label for row in agent_entry.widget.form.values.values()} + relationships =''') +# FieldView already admits every visible declaration; retain its inherited answer. +Path('.verification/object-settings-sources/agent-control.plan.json').write_text(json.dumps({'recipes':[{'recipe_id':'selected-session-advertisement-control','operations':ops}]},indent=2)) diff --git a/.verification/object-settings-sources/author.py b/.verification/object-settings-sources/author.py new file mode 100644 index 000000000..eb87c3f39 --- /dev/null +++ b/.verification/object-settings-sources/author.py @@ -0,0 +1,58 @@ +import json +from pathlib import Path + +root = Path.cwd() +ops = [] +def patch(file, target, old, new): + ops.append(dict(operation='patch_target', file_path=str(root / file), target_qualname=target, + replacements=[dict(old_source=old, new_source=new)])) + +patch('src/toad/configured_view.py', 'ConfiguredView.configuration_panels', ''' for path in state.direct_parameter_paths(): + if state.shows(path.value): + field = state.field(path.value) + yield SideBar.Panel(field.label, ObjectProperties(field), + id=f"settings-{path.value}", flex=True)''', ''' for path, parameter in ParameterFormService().parameters_for_state(state): + field = state.field(path.value) + yield SideBar.Panel(field.label, ObjectProperties(field), + id=f"settings-{path.value}", flex=True)''') +ops.append(dict(operation='ensure_import', file_path=str(root/'src/toad/configured_view.py'), + import_source='from formbridge.forms.parameter_form_service import ParameterFormService')) +patch('src/toad/object_configuration.py', 'ObjectSettingField', ''' @property + def owner_type(self): + return DeclaredField(self.state.defaults, self.path).owner_type''', ''' @property + def owner_type(self): + return DeclaredField(self.state.defaults, self.path).owner_type + + @property + def allows_override(self) -> bool: + return self.state.admits(self.path.value) + + def edit(self, value): + if self.allows_override: + return super().edit(value) + return DeclaredField(self.state.defaults, self.path).edit(value)''') +patch('src/toad/forms/object_configuration.py', 'ObjectFieldView.admits', ''' return field.state.shows(field.path.value)''', ''' return True''') +patch('src/toad/forms/object_configuration.py', 'ObjectValueView.mark', ''' explicit = self.field.state.parameters[self.field.path.value] is not None + super().mark(explicit) + self.inherit.label = "Use global default" if explicit else "Override here" + if explicit: + self.source.update("Override for this object") + else: + scope, owner = self.field.state.get_provenance(self.field.path.value) + self.source.update("Inherited from global defaults") + self.source.tooltip = f"{owner.__module__}.{owner.__qualname__} · {scope or 'global'}"''', ''' local = self.field.allows_override + explicit = local and self.field.state.parameters[self.field.path.value] is not None + super().mark(explicit) + self.inherit.display = local + self.inherit.label = "Use global default" if explicit else "Override here" + if explicit: + self.source.update("Override for this object") + else: + scope, owner = self.field.state.get_provenance(self.field.path.value) + self.source.update("Inherited from global defaults" if local else "Global setting · applies to every view") + self.source.tooltip = f"{owner.__module__}.{owner.__qualname__} · {scope or 'global'}"''') +patch('src/toad/object_configuration.py', 'ObjectConfiguration', ''' def shows(self, path: str) -> bool: + return any(name == path or name.startswith(path + ".") for name in self.editable_paths) + +''', '') +Path('.verification/object-settings-sources/fix.plan.json').write_text(json.dumps({'recipes':[{'recipe_id':'discover-settings-at-their-original-scopes','operations':ops}]},indent=2)) diff --git a/.verification/object-settings-sources/contract.plan.json b/.verification/object-settings-sources/contract.plan.json new file mode 100644 index 000000000..b7768a3ec --- /dev/null +++ b/.verification/object-settings-sources/contract.plan.json @@ -0,0 +1,20 @@ +{ + "recipes": [ + { + "recipe_id": "declare-the-original-agent-event-source", + "operations": [ + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/agent_binding.py", + "target_qualname": "AgentConnection", + "replacements": [ + { + "old_source": " configuration: AgentConfiguration", + "new_source": " events: CoreEventStream\n configuration: AgentConfiguration" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.verification/object-settings-sources/delivery.md b/.verification/object-settings-sources/delivery.md new file mode 100644 index 000000000..3c7ff26a0 --- /dev/null +++ b/.verification/object-settings-sources/delivery.md @@ -0,0 +1,23 @@ +# Viewed settings: discovery and original writers + +The sidebar used local-override eligibility to decide which settings existed. Files therefore exposed four UI fields and hid every other settings group. Native session advertisements were also confined to the model popup. + +The dropdown now uses FormBridge's visible parameter discovery. Each field keeps its original declaration. Local fields still use the same ObjectState parent and explicit override storage. Global fields write and save the original SavedSettings object; their controls say they apply to every view and do not offer a false local override button. Hidden declarations remain hidden through FormBridge. + +Native session sources supply AgentProperties. Mailbox sources supply no ACP configuration; channel and file views do not borrow the last agent's state. AgentProperties renders SessionForm over the selected session's actual advertisements and listens to its original event stream, including binding changes. The existing model popup inherits the same form, adding only popup dismissal. All edits and provider-login requests return through the original prompt and its guarded backend writer. + +Applied through retained NRA plans. No manual product edits or new agent. + +## Executed controls + +- Existing selected-view control extended for all discovered groups, global save, local inheritance/reset, source identity, selected view retention, sidebar and context editor: passed before the session extension. +- Final same UI control plus original agent-configuration model file: **3 passed in 21.75s**, exit 0. +- The UI control receives an arbitrary Boolean advertisement and then a newly added advertisement; both appear without UI field declarations. +- Cold app/import/MRO passed. +- No broad suite or profiler. + +Actual physical inspection and installation are recorded below after execution; these checks do not establish them. + +## Limits + +This exposes the existing visible Toad settings declarations and the session's actual ACP advertisements. It does not invent advertisements for backend settings that the backend has not declared, or make app-owned operations into per-file overrides. diff --git a/.verification/object-settings-sources/finish.plan.json b/.verification/object-settings-sources/finish.plan.json new file mode 100644 index 000000000..986677e43 --- /dev/null +++ b/.verification/object-settings-sources/finish.plan.json @@ -0,0 +1,58 @@ +{ + "recipes": [ + { + "recipe_id": "persist-global-edits-at-the-original-owner", + "operations": [ + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "target_qualname": "ConfiguredView.configuration_state", + "replacements": [ + { + "old_source": "ObjectConfiguration(identity, self.app.saved_settings.state,", + "new_source": "ObjectConfiguration(identity, self.app.saved_settings," + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/object_configuration.py", + "target_qualname": "ObjectConfiguration.__init__", + "replacements": [ + { + "old_source": " def __init__(self, identity: TabIdentity, defaults: ObjectState,\n directory: Path, owner: SettingsScope) -> None:\n self.identity, self.defaults, self.owner = identity, defaults, owner", + "new_source": " def __init__(self, identity: TabIdentity, global_settings: SavedSettings,\n directory: Path, owner: SettingsScope) -> None:\n self.global_settings = global_settings\n defaults = global_settings.state\n self.identity, self.defaults, self.owner = identity, defaults, owner" + } + ] + }, + { + "operation": "ensure_import", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/object_configuration.py", + "import_source": "from toad.preferences import SavedSettings" + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/object_configuration.py", + "target_qualname": "ObjectSettingField.edit", + "replacements": [ + { + "old_source": " return DeclaredField(self.state.defaults, self.path).edit(value)", + "new_source": " result = DeclaredField(self.state.defaults, self.path).edit(value)\n self.state.global_settings.save_sync()\n return result" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/tests/textual/test_sidebar_projection.py", + "target_qualname": "test_view_menu_discovers_settings_and_keeps_each_objects_state", + "replacements": [ + { + "old_source": " assert len(file_menu.entries) < len(menu.entries)", + "new_source": " assert tuple(entry.widget.field.path for entry in file_menu.entries) == tuple(\n entry.widget.field.path for entry in menu.entries)\n # The same declarations remain discoverable; the declaring scope owns writes.\n global_field = file_view.configuration_state.field('notifications.enable_sounds')\n assert not global_field.allows_override\n before = app.saved_settings.state.fields.notifications.enable_sounds\n global_field.edit(not before)\n assert app.saved_settings.state.fields.notifications.enable_sounds is not before\n assert state.fields.notifications.enable_sounds is not before\n assert file_view.configuration_state.fields.notifications.enable_sounds is not before\n assert file_view.configuration_state.parameters['notifications.enable_sounds'] is None\n assert app.saved_settings.path().is_file()\n assert len(file_menu.entries) > 3\n notifications = next(entry for entry in file_menu.entries\n if entry.widget.field.path.value == 'notifications')\n file_menu.picker.value = notifications\n await ui.until(lambda: notifications.widget.is_mounted, why='discovered notifications form')\n await ui.until(lambda: bool(notifications.widget.query(ObjectValueView)), why='global field controls')\n field_view = next(part for part in notifications.widget.query(ObjectValueView)\n if part.field.path.value == 'notifications.enable_sounds')\n assert not field_view.inherit.display\n assert 'Global setting' in str(field_view.source.render())" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.verification/object-settings-sources/finish_author.py b/.verification/object-settings-sources/finish_author.py new file mode 100644 index 000000000..ec3dfd2b7 --- /dev/null +++ b/.verification/object-settings-sources/finish_author.py @@ -0,0 +1,40 @@ +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)])) +patch('src/toad/configured_view.py','ConfiguredView.configuration_state','ObjectConfiguration(identity, self.app.saved_settings.state,','ObjectConfiguration(identity, self.app.saved_settings,') +patch('src/toad/object_configuration.py','ObjectConfiguration.__init__',''' def __init__(self, identity: TabIdentity, defaults: ObjectState, + directory: Path, owner: SettingsScope) -> None: + self.identity, self.defaults, self.owner = identity, defaults, owner''',''' def __init__(self, identity: TabIdentity, global_settings: SavedSettings, + directory: Path, owner: SettingsScope) -> None: + self.global_settings = global_settings + defaults = global_settings.state + self.identity, self.defaults, self.owner = identity, defaults, owner''') +ops.append(dict(operation='ensure_import',file_path=str(r/'src/toad/object_configuration.py'),import_source='from toad.preferences import SavedSettings')) +patch('src/toad/object_configuration.py','ObjectSettingField.edit',''' return DeclaredField(self.state.defaults, self.path).edit(value)''',''' result = DeclaredField(self.state.defaults, self.path).edit(value) + self.state.global_settings.save_sync() + return result''') +patch('tests/textual/test_sidebar_projection.py','test_view_menu_discovers_settings_and_keeps_each_objects_state', ''' assert len(file_menu.entries) < len(menu.entries)''',''' assert tuple(entry.widget.field.path for entry in file_menu.entries) == tuple( + entry.widget.field.path for entry in menu.entries) + # The same declarations remain discoverable; the declaring scope owns writes. + global_field = file_view.configuration_state.field('notifications.enable_sounds') + assert not global_field.allows_override + before = app.saved_settings.state.fields.notifications.enable_sounds + global_field.edit(not before) + assert app.saved_settings.state.fields.notifications.enable_sounds is not before + assert state.fields.notifications.enable_sounds is not before + assert file_view.configuration_state.fields.notifications.enable_sounds is not before + assert file_view.configuration_state.parameters['notifications.enable_sounds'] is None + assert app.saved_settings.path().is_file() + assert len(file_menu.entries) > 3 + notifications = next(entry for entry in file_menu.entries + if entry.widget.field.path.value == 'notifications') + file_menu.picker.value = notifications + await ui.until(lambda: notifications.widget.is_mounted, why='discovered notifications form') + await ui.until(lambda: bool(notifications.widget.query(ObjectValueView)), why='global field controls') + field_view = next(part for part in notifications.widget.query(ObjectValueView) + if part.field.path.value == 'notifications.enable_sounds') + assert not field_view.inherit.display + assert 'Global setting' in str(field_view.source.render())''') +Path('.verification/object-settings-sources/finish.plan.json').write_text(json.dumps({'recipes':[{'recipe_id':'persist-global-edits-at-the-original-owner','operations':ops}]},indent=2)) diff --git a/.verification/object-settings-sources/fix.plan.json b/.verification/object-settings-sources/fix.plan.json new file mode 100644 index 000000000..3023cafa9 --- /dev/null +++ b/.verification/object-settings-sources/fix.plan.json @@ -0,0 +1,69 @@ +{ + "recipes": [ + { + "recipe_id": "discover-settings-at-their-original-scopes", + "operations": [ + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "target_qualname": "ConfiguredView.configuration_panels", + "replacements": [ + { + "old_source": " for path in state.direct_parameter_paths():\n if state.shows(path.value):\n field = state.field(path.value)\n yield SideBar.Panel(field.label, ObjectProperties(field),\n id=f\"settings-{path.value}\", flex=True)", + "new_source": " for path, parameter in ParameterFormService().parameters_for_state(state):\n field = state.field(path.value)\n yield SideBar.Panel(field.label, ObjectProperties(field),\n id=f\"settings-{path.value}\", flex=True)" + } + ] + }, + { + "operation": "ensure_import", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "import_source": "from formbridge.forms.parameter_form_service import ParameterFormService" + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/object_configuration.py", + "target_qualname": "ObjectSettingField", + "replacements": [ + { + "old_source": " @property\n def owner_type(self):\n return DeclaredField(self.state.defaults, self.path).owner_type", + "new_source": " @property\n def owner_type(self):\n return DeclaredField(self.state.defaults, self.path).owner_type\n\n @property\n def allows_override(self) -> bool:\n return self.state.admits(self.path.value)\n\n def edit(self, value):\n if self.allows_override:\n return super().edit(value)\n return DeclaredField(self.state.defaults, self.path).edit(value)" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/forms/object_configuration.py", + "target_qualname": "ObjectFieldView.admits", + "replacements": [ + { + "old_source": " return field.state.shows(field.path.value)", + "new_source": " return True" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/forms/object_configuration.py", + "target_qualname": "ObjectValueView.mark", + "replacements": [ + { + "old_source": " explicit = self.field.state.parameters[self.field.path.value] is not None\n super().mark(explicit)\n self.inherit.label = \"Use global default\" if explicit else \"Override here\"\n if explicit:\n self.source.update(\"Override for this object\")\n else:\n scope, owner = self.field.state.get_provenance(self.field.path.value)\n self.source.update(\"Inherited from global defaults\")\n self.source.tooltip = f\"{owner.__module__}.{owner.__qualname__} \u00b7 {scope or 'global'}\"", + "new_source": " local = self.field.allows_override\n explicit = local and self.field.state.parameters[self.field.path.value] is not None\n super().mark(explicit)\n self.inherit.display = local\n self.inherit.label = \"Use global default\" if explicit else \"Override here\"\n if explicit:\n self.source.update(\"Override for this object\")\n else:\n scope, owner = self.field.state.get_provenance(self.field.path.value)\n self.source.update(\"Inherited from global defaults\" if local else \"Global setting \u00b7 applies to every view\")\n self.source.tooltip = f\"{owner.__module__}.{owner.__qualname__} \u00b7 {scope or 'global'}\"" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/object_configuration.py", + "target_qualname": "ObjectConfiguration", + "replacements": [ + { + "old_source": " def shows(self, path: str) -> bool:\n return any(name == path or name.startswith(path + \".\") for name in self.editable_paths)\n\n", + "new_source": "" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/toad/agent_binding.py b/src/toad/agent_binding.py index 477b87f56..d9c6a7122 100644 --- a/src/toad/agent_binding.py +++ b/src/toad/agent_binding.py @@ -15,6 +15,7 @@ from toad.acp.agent_configuration import AgentConfiguration from toad.acp.agent_session import SessionDisposition, UnopenedSessionDisposition from toad.agent_presentation import AgentPresentation, LocalAgentPresentation +from toad.core.events import CoreEventStream from toad.input_decision import InputAccepted, InputDecision, NoLiveAgent from toad.commit_claims import CommitSource from toad.conversation_turn import TurnOwner @@ -43,6 +44,7 @@ class AgentConnection(ABC): + events: CoreEventStream configuration: AgentConfiguration presentation: AgentPresentation ready: bool @@ -134,6 +136,10 @@ class NoAgentConnection(AgentConnection): current_mode = None session_disposition = UnopenedSessionDisposition() + @cached_property + def events(self): + return CoreEventStream(self) + @cached_property def presentation(self): return LocalAgentPresentation(self) diff --git a/src/toad/configured_view.py b/src/toad/configured_view.py index 816b88111..be13378d9 100644 --- a/src/toad/configured_view.py +++ b/src/toad/configured_view.py @@ -1,7 +1,11 @@ """A viewed object's state and its two presentations, inherited by files and conversations.""" from functools import cached_property -from textual import events, on +from formbridge.forms.parameter_form_service import ParameterFormService +from metaclass_registry.mro_dispatch import handles +from textual import events, on, work from textual.geometry import Region +from toad.core.events import ConfigurationChanged +from toad.core_event_carrier import CoreEventReceiver from toad.forms.object_configuration import ObjectFieldView, ObjectFormView from toad.native_style import FillHeight from toad.object_configuration import ObjectConfiguration @@ -11,6 +15,7 @@ from toad.settings_scope import ViewSettingsScope from toad.widgets.acting_button import DismissButton from toad.widgets.comms_menu import ActionMenu +from toad.widgets.model_switcher import SessionForm from toad.widgets.side_bar import ( PanelMenu, SideBar, @@ -41,6 +46,43 @@ def finish(self): self.form.finish() +class AgentProperties(CoreEventReceiver, FillHeight, SidebarPanelWidget): + """The selected native session's advertisements, using its original writer.""" + + def __init__(self, view): + super().__init__() + self.view = view + self.listen(events.Mount, self.follow_binding) + + @classmethod + def panel(cls, view): + return SideBar.Panel("Agent settings", cls(view), id="agent-settings", flex=True) + + def compose(self): + self.form = SessionForm.for_prompt(self.view.conversation.prompt) + yield self.form + + def follow_binding(self): + self.watch(self.view.conversation, "agent_binding", self.binding_changed) + + def binding_changed(self, old, new): + self.retire_core_observations(old.events) + self.observe_core(new.events) + self.configuration_changed() + + @handles(ConfigurationChanged) + def configuration_changed(self): + if self.is_mounted: + self.rebuild_form() + + @work(exclusive=True, group="agent-settings") + async def rebuild_form(self): + await self.recompose() + + def finish(self): + self.form.finish() + + class ObjectConfigurationDialog(FormModal[None], DismissibleScreen[None]): def __init__(self, panels): super().__init__() @@ -60,17 +102,16 @@ class ConfiguredView(ViewSettingsScope): @cached_property def configuration_state(self) -> ObjectConfiguration: identity = self.app.session_navigation.get(self.id).identity - return ObjectConfiguration(identity, self.app.saved_settings.state, + return ObjectConfiguration(identity, self.app.saved_settings, self.app.saved_settings.path().parent, self) @property def configuration_panels(self): state = self.configuration_state - for path in state.direct_parameter_paths(): - if state.shows(path.value): - field = state.field(path.value) - yield SideBar.Panel(field.label, ObjectProperties(field), - id=f"settings-{path.value}", flex=True) + for path, parameter in ParameterFormService().parameters_for_state(state): + field = state.field(path.value) + yield SideBar.Panel(field.label, ObjectProperties(field), + id=f"settings-{path.value}", flex=True) @cached_property def _thread_sidebar_state(self): diff --git a/src/toad/forms/object_configuration.py b/src/toad/forms/object_configuration.py index a18ef8dfe..95615debd 100644 --- a/src/toad/forms/object_configuration.py +++ b/src/toad/forms/object_configuration.py @@ -14,9 +14,7 @@ class ObjectFieldView(FieldView): def family(cls): return ObjectFieldView - @classmethod - def admits(cls, field): - return field.state.shows(field.path.value) + class InheritSettingButton(ActingButton): @@ -44,14 +42,16 @@ def compose(self): yield self.inherit def mark(self, changed): - explicit = self.field.state.parameters[self.field.path.value] is not None + local = self.field.allows_override + explicit = local and self.field.state.parameters[self.field.path.value] is not None super().mark(explicit) + self.inherit.display = local self.inherit.label = "Use global default" if explicit else "Override here" if explicit: self.source.update("Override for this object") else: scope, owner = self.field.state.get_provenance(self.field.path.value) - self.source.update("Inherited from global defaults") + self.source.update("Inherited from global defaults" if local else "Global setting · applies to every view") self.source.tooltip = f"{owner.__module__}.{owner.__qualname__} · {scope or 'global'}" diff --git a/src/toad/object_configuration.py b/src/toad/object_configuration.py index 7355e8636..223eb2f67 100644 --- a/src/toad/object_configuration.py +++ b/src/toad/object_configuration.py @@ -11,6 +11,7 @@ from pydantic import JsonValue from toad import atomic +from toad.preferences import SavedSettings from toad.setting_declarations import Effect, SettingsDeclaration from toad.settings_scope import SettingsScope from toad.tab_identity import TabIdentity @@ -33,12 +34,25 @@ def declaration(self): def owner_type(self): return DeclaredField(self.state.defaults, self.path).owner_type + @property + def allows_override(self) -> bool: + return self.state.admits(self.path.value) + + def edit(self, value): + if self.allows_override: + return super().edit(value) + result = DeclaredField(self.state.defaults, self.path).edit(value) + self.state.global_settings.save_sync() + return result + class ObjectConfiguration(ObjectState): """One view's explicit values; unassigned fields follow its live defaults.""" - def __init__(self, identity: TabIdentity, defaults: ObjectState, + def __init__(self, identity: TabIdentity, global_settings: SavedSettings, directory: Path, owner: SettingsScope) -> None: + self.global_settings = global_settings + defaults = global_settings.state self.identity, self.defaults, self.owner = identity, defaults, owner self.path = directory / "objects" / f"{identity.mode_name}.json" scope = str(ScopeKey.of("view", identity.mode_name)) @@ -74,9 +88,6 @@ def admits(self, path: str) -> bool: return (issubclass(owner, SettingsDeclaration) and owner.admits_field(field.path.field_name, self.owner)) - def shows(self, path: str) -> bool: - return any(name == path or name.startswith(path + ".") for name in self.editable_paths) - def update_parameter(self, param_name: str, value: object) -> set[str]: if param_name not in self.editable_paths: raise ValueError(f"{param_name} cannot be overridden for {self.identity}") diff --git a/src/toad/screens/main.py b/src/toad/screens/main.py index 0d3183169..be987ef54 100644 --- a/src/toad/screens/main.py +++ b/src/toad/screens/main.py @@ -340,6 +340,11 @@ def initial_editing_mode(self) -> EditingMode: busy_count = var(0) conversation = getters.query_one(Conversation) + @property + def configuration_panels(self): + yield from self.operational_sources.configuration_panels(self) + yield from super().configuration_panels + def keyboard_scopes(self) -> tuple[DeclaredWidgetActions, ...]: return self.content.keyboard_scopes(self) diff --git a/src/toad/session_presentation.py b/src/toad/session_presentation.py index 1daf84880..dff733edd 100644 --- a/src/toad/session_presentation.py +++ b/src/toad/session_presentation.py @@ -21,6 +21,7 @@ from toad.agent_binding import NoAgentConnection from toad.agent_surface import AttachedSurfaceBinding +from toad.configured_view import AgentProperties from toad.transcript_scope import MailboxSent, TranscriptView from toad.input_history import InputHistories from toad.input_decision import InputDecision @@ -184,6 +185,9 @@ def external_transcript_changed(self, conversation: Conversation) -> None: conversation.transcript.source_requests.request(CanonicalSourcePublication) + def configuration_panels(self, view): + yield AgentProperties.panel(view) + def prompt(self, conversation: Conversation): return Prompt(project=conversation.project_path, submissions=conversation.submissions) @@ -273,6 +277,10 @@ def activity_row(self, conversation: Conversation): def agent_info(self, conversation: Conversation) -> str: return conversation.thread_activity.row.presentation.title + def configuration_panels(self, view): + """A mailbox has no native session configuration to advertise.""" + return () + def prompt(self, conversation: Conversation): return MailboxPrompt(project=conversation.project_path, submissions=conversation.submissions) diff --git a/src/toad/widgets/model_switcher.py b/src/toad/widgets/model_switcher.py index e3d0e7289..f66914b1e 100644 --- a/src/toad/widgets/model_switcher.py +++ b/src/toad/widgets/model_switcher.py @@ -30,14 +30,13 @@ class SessionModelEditor(ValueEditor, ModelSelection, shows=(ModelConfigurationS @property def history_scope(self) -> str: - return self.query_ancestor(ModelSwitcher).prompt.model_history_scope + return self.query_ancestor(SessionForm).prompt.model_history_scope def bind_control(self) -> None: self.listen(events.Mount, self.opened) def opened(self, event: events.Mount) -> None: - if self.query_ancestor(ModelSwitcher).is_open: - self.focus_content(False) + self.focus_content(False) def compose(self) -> ComposeResult: yield from self.compose_content() @@ -61,14 +60,26 @@ def paint(self) -> None: self.configuration_changed() def connect_provider(self) -> None: - self.publish_core(input_events.ProviderLogin()) + self.query_ancestor(SessionForm).prompt.publish_core(input_events.ProviderLogin()) def dismiss_control(self) -> None: - self.query_ancestor(ModelSwitcher).dismiss_control() + self.query_ancestor(SessionForm).dismiss_control() class SessionForm(CoreEventReceiver, InlineFormView): + def __init__(self, state, parts, prompt): + super().__init__(state, parts) + self.prompt = prompt + + @classmethod + def for_prompt(cls, prompt): + state = prompt.agent_binding.configuration.editing() + return cls(state, cls.field_family.of(state), prompt) + + 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 @@ -79,7 +90,14 @@ def values_changed(self, paths: set[str]) -> None: field = self.values[path].field setting = field.metadata(AdvertisedSetting).setting if field.value != setting.current: - self.publish_core(setting.selection_event(field.value)) + self.prompt.publish_core(setting.selection_event(field.value)) + + +class PopupSessionForm(SessionForm): + """The popup adds dismissal to the same advertised settings form.""" + + def dismiss_control(self): + self.query_ancestor(ModelSwitcher).dismiss_control() class ModelSwitcher(SelectionBounds, ScreenOverlay, InfoPopup): @@ -90,7 +108,7 @@ class ModelSwitcher(SelectionBounds, ScreenOverlay, InfoPopup): overflow_y = "auto" def compose_content(self) -> ComposeResult: - self.form = SessionForm.whole(self.prompt.agent_binding.configuration.editing()) + self.form = PopupSessionForm.for_prompt(self.prompt) yield self.form def configuration_changed(self) -> None: diff --git a/tests/textual/test_sidebar_projection.py b/tests/textual/test_sidebar_projection.py index a195a6023..746f5cf27 100644 --- a/tests/textual/test_sidebar_projection.py +++ b/tests/textual/test_sidebar_projection.py @@ -54,7 +54,28 @@ async def test_view_menu_discovers_settings_and_keeps_each_objects_state(workspa assert file_view.configuration_state.identity == app.session_navigation.get(file_mode).identity assert file_view.configuration_state is not state assert not file_bar.query(ThreadCommsSidebar) - assert len(file_menu.entries) < len(menu.entries) + assert tuple(entry.widget.field.path for entry in file_menu.entries) == tuple( + entry.widget.field.path for entry in menu.entries) + # The same declarations remain discoverable; the declaring scope owns writes. + global_field = file_view.configuration_state.field('notifications.enable_sounds') + assert not global_field.allows_override + before = app.saved_settings.state.fields.notifications.enable_sounds + global_field.edit(not before) + assert app.saved_settings.state.fields.notifications.enable_sounds is not before + assert state.fields.notifications.enable_sounds is not before + assert file_view.configuration_state.fields.notifications.enable_sounds is not before + assert file_view.configuration_state.parameters['notifications.enable_sounds'] is None + assert app.saved_settings.path().is_file() + assert len(file_menu.entries) > 3 + notifications = next(entry for entry in file_menu.entries + if entry.widget.field.path.value == 'notifications') + file_menu.picker.value = notifications + await ui.until(lambda: notifications.widget.is_mounted, why='discovered notifications form') + await ui.until(lambda: bool(notifications.widget.query(ObjectValueView)), why='global field controls') + field_view = next(part for part in notifications.widget.query(ObjectValueView) + if part.field.path.value == 'notifications.enable_sounds') + assert not field_view.inherit.display + assert 'Global setting' in str(field_view.source.render()) await ui.select_tab(channel) assert view.thread_sidebar is bar and bar.query_one(PanelMenu) is menu assert menu.picker.value is ui_entry @@ -71,6 +92,30 @@ async def test_view_menu_discovers_settings_and_keeps_each_objects_state(workspa await workspace.bind(native, 'panel-owner') native_bar = await ui.revealed(native.thread_sidebar) native_menu = native_bar.query_one(PanelMenu) + from acp.schema import SetSessionConfigOptionResponse + from support.agents import AppAgents + from toad.configured_view import AgentProperties + from toad.widgets.model_switcher import SessionForm + agent = AppAgents(app).detached_agent('sidebar-settings') + native.conversation.bind_agent(agent) + agent.configuration.receive(SetSessionConfigOptionResponse.model_validate({'configOptions': [ + {'id': 'sample-budget', 'name': 'Budget', 'type': 'boolean', 'currentValue': True}, + ]}).config_options) + agent_entry = next(entry for entry in native_menu.entries if isinstance(entry.widget, AgentProperties)) + native_menu.picker.value = agent_entry + await ui.until(lambda: agent_entry.widget.is_mounted and bool(agent_entry.widget.query(SessionForm)), + why='the actual advertised session form') + await ui.until(lambda: agent_entry.widget.form.represents(agent.configuration), + why='new advertisements replace the original fields') + assert agent_entry.widget.form.prompt is native.conversation.prompt + assert 'Budget' in {row.field.label for row in agent_entry.widget.form.values.values()} + agent.configuration.receive(SetSessionConfigOptionResponse.model_validate({'configOptions': [ + {'id': 'sample-budget', 'name': 'Budget', 'type': 'boolean', 'currentValue': False}, + {'id': 'new-setting', 'name': 'New setting', 'type': 'boolean', 'currentValue': True}, + ]}).config_options) + await ui.until(lambda: agent_entry.widget.form.represents(agent.configuration), + why='the sidebar follows the source update') + assert 'New setting' in {row.field.label for row in agent_entry.widget.form.values.values()} relationships = next(entry for entry in native_menu.entries if isinstance(entry.widget, ThreadCommsSidebar)) native_menu.picker.value = relationships