diff --git a/.verification/settings-read-and-sidebar/delivery.md b/.verification/settings-read-and-sidebar/delivery.md new file mode 100644 index 000000000..386c8a994 --- /dev/null +++ b/.verification/settings-read-and-sidebar/delivery.md @@ -0,0 +1,7 @@ +SettingsReader now reads ObjectState.fields. FieldProxy follows the existing ParameterStructure, resolving one leaf rather than reconstructing every dataclass during paint. ObjectState #67 merged as d34d75803d3b7e65128bf43ab867bb70f37cd013 and this package declares that version. + +Properties are a panel in the existing ThreadSidebar. Session, channel and file views inherit the same bar resource; the separate ObjectSidebar and mounting marker are deleted. The right-click editor and that panel still edit the same ObjectConfiguration. Lazy inheritance, explicit overrides, reset and persistence remain owned there. + +The user's existing settings actually contain an object sidebar placement. Its value declaration is retained solely to decode that saved data; a nominal ActiveSidebarPlacement parent derives the two current roles, so the old role cannot claim screen space. No configuration file was written. + +Executed small native controls: real Toad settings follow registered global edits, retain an explicit child override, and inherit again on reset; padding resolves as a leaf. All three view classes import and are concrete. A first inheritance attempt omitted the application's registry registration and failed setup; corrected application lifecycle passed. A later control reached every assertion for active roles, inherited bar methods and current saved-layout decoding, printed its result, then exceeded the 15-second outer limit during process exit (exit 124). This is not a passing process-exit check. No suite, profile or CPU-speed claim. Physical sidebar trial and live CPU effect follow installation. diff --git a/.verification/settings-read-and-sidebar/finish.plan.json b/.verification/settings-read-and-sidebar/finish.plan.json new file mode 100644 index 000000000..443c61110 --- /dev/null +++ b/.verification/settings-read-and-sidebar/finish.plan.json @@ -0,0 +1,122 @@ +{ + "stages": [ + { + "recipes": [ + { + "recipe_id": "one-view-bar-and-preserved-stored-layouts", + "operations": [ + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/settings_scope.py", + "target_qualname": "ObjectSidebarHost", + "replacements": [ + { + "old_source": "class ObjectSidebarHost(Widget):\n \"\"\"The native body in which a viewed object's properties are placed.\"\"\"", + "new_source": "" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/screens/session_view.py", + "target_qualname": "SidebarSessionView", + "replacements": [ + { + "old_source": " @cached_property\n def thread_sidebar(self) -> SidebarT:\n return self.make_thread_sidebar()\n\n def sidebars(self) -> tuple[SideBar, ...]:\n return (self.thread_sidebar,)", + "new_source": "" + } + ] + }, + { + "operation": "insert_before_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/sidebar_placement.py", + "target_qualname": "ChannelsSidebarPlacement", + "source": "class ActiveSidebarPlacement(SidebarPlacement, declared_name=None):\n \"\"\"The sidebar roles a current view can actually own.\"\"\"" + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/sidebar_placement.py", + "target_qualname": "ChannelsSidebarPlacement", + "replacements": [ + { + "old_source": "class ChannelsSidebarPlacement(SidebarPlacement):", + "new_source": "class ChannelsSidebarPlacement(ActiveSidebarPlacement):" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/sidebar_placement.py", + "target_qualname": "ThreadSidebarPlacement", + "replacements": [ + { + "old_source": "class ThreadSidebarPlacement(SidebarPlacement):", + "new_source": "class ThreadSidebarPlacement(ActiveSidebarPlacement):" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/sidebar_placement.py", + "target_qualname": "ObjectSidebarPlacement", + "replacements": [ + { + "old_source": "class ObjectSidebarPlacement(ThreadSidebarPlacement):\n \"\"\"Properties follow the viewed object, beside its other panels.\"\"\"\n\n @classmethod\n def heading(cls, viewed: Iterable[str]) -> str:\n return f\"{super().heading(viewed)} properties\"\n", + "new_source": "class ObjectSidebarPlacement(SidebarPlacement):\n \"\"\"Read already-saved third-bar positions; no current view owns this role.\"\"\"" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/sidebar_layout.py", + "target_qualname": "SidebarLayout.placements", + "replacements": [ + { + "old_source": "SidebarPlacement.members_with(SidebarPlacement)", + "new_source": "ActiveSidebarPlacement.members_with(ActiveSidebarPlacement)" + } + ] + }, + { + "operation": "ensure_import", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/sidebar_layout.py", + "import_source": "from toad.sidebar_placement import ActiveSidebarPlacement" + } + ] + } + ] + }, + { + "recipes": [ + { + "recipe_id": "remove-third-bar-mount-imports", + "operations": [ + { + "operation": "remove_import_bindings", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "bound_names": [ + "ObjectSidebarHost", + "ObjectSidebarPlacement", + "LazyThreadSidebar" + ] + }, + { + "operation": "remove_import_bindings", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/screens/session_view.py", + "bound_names": [ + "ObjectSidebarHost" + ] + }, + { + "operation": "remove_import_bindings", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/settings_scope.py", + "bound_names": [ + "Widget" + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.verification/settings-read-and-sidebar/fix.plan.json b/.verification/settings-read-and-sidebar/fix.plan.json new file mode 100644 index 000000000..5ba031d33 --- /dev/null +++ b/.verification/settings-read-and-sidebar/fix.plan.json @@ -0,0 +1,119 @@ +{ + "recipes": [ + { + "recipe_id": "read-leaves-and-reuse-view-sidebar", + "operations": [ + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/settings_scope.py", + "target_qualname": "SettingsReader.settings", + "replacements": [ + { + "old_source": "return self.configuration_state.to_resolved_object()", + "new_source": "return self.configuration_state.fields" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/settings_scope.py", + "target_qualname": "ViewSettingsScope", + "replacements": [ + { + "old_source": " \"\"\"A file, thread or channel owns its display overrides.\"\"\"", + "new_source": " \"\"\"A file, thread or channel owns its display overrides.\"\"\"\n\n @property\n @abstractmethod\n def configuration_panel(self): ..." + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "target_qualname": "ObjectSidebar", + "replacements": [ + { + "old_source": "class ObjectSidebar(LazyThreadSidebar):\n placement_type = ObjectSidebarPlacement\n\n def __init__(self, configuration):\n self.configuration = configuration\n super().__init__(state=SidebarState())\n\n def make_panels(self):\n return (SideBar.Panel(\"Properties\", ObjectProperties.whole(self.configuration), flex=True),)", + "new_source": "" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "target_qualname": "ConfiguredView", + "replacements": [ + { + "old_source": " @cached_property\n def configuration_sidebar(self):\n return ObjectSidebar(self.configuration_state)\n\n def sidebars(self):\n return (*super().sidebars(), self.configuration_sidebar)\n\n @on(events.Mount)\n async def mount_configuration(self):\n await self.query_one(ObjectSidebarHost).mount(self.configuration_sidebar)", + "new_source": " @property\n def configuration_panel(self):\n return SideBar.Panel(\"Properties\", ObjectProperties.whole(self.configuration_state),\n flex=True, collapsed=True)\n\n def make_thread_sidebar(self):\n return ThreadSidebar(state=SidebarState())\n\n @cached_property\n def thread_sidebar(self):\n return self.make_thread_sidebar()\n\n def sidebars(self):\n return (self.thread_sidebar,)" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/side_bar.py", + "target_qualname": "SideBar._compose_panels", + "replacements": [ + { + "old_source": "for panel in self.panels:", + "new_source": "for panel in self.panel_entries():" + } + ] + }, + { + "operation": "insert_before_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/side_bar.py", + "target_qualname": "SideBar._compose_panels", + "source": " def panel_entries(self):\n return iter(self.panels)" + }, + { + "operation": "insert_before_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/side_bar.py", + "target_qualname": "ThreadSidebar.__init__", + "source": " def panel_entries(self):\n yield from super().panel_entries()\n yield self.query_ancestor(ViewSettingsScope).configuration_panel" + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/screens/session_view.py", + "target_qualname": "SidebarSessionView.sidebars", + "replacements": [ + { + "old_source": "return (self.thread_sidebar, self.configuration_sidebar)", + "new_source": "return (self.thread_sidebar,)" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/screens/session_view.py", + "target_qualname": "SessionBody", + "replacements": [ + { + "old_source": "class SessionBody(WidgetView, ToadNode, ObjectSidebarHost, Center):", + "new_source": "class SessionBody(WidgetView, ToadNode, Center):" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/screens/file_preview.py", + "target_qualname": "FilePreviewScreen.compose", + "replacements": [ + { + "old_source": "with SessionBody():\n with FileContents():", + "new_source": "with SessionBody():\n yield self.thread_sidebar\n with FileContents():" + } + ] + }, + { + "operation": "ensure_import", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/widgets/side_bar.py", + "import_source": "from toad.settings_scope import ViewSettingsScope" + }, + { + "operation": "ensure_import", + "file_path": "/home/ts/wt/codex-live--P181--toad/src/toad/configured_view.py", + "import_source": "from toad.widgets.side_bar import ThreadSidebar" + } + ] + } + ] +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4c7cc9a2d..9db41f0e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ textual = { git = "https://github.com/OpenHCSDev/textual.git", branch = "main" } formbridge = { git = "https://github.com/OpenHCSDev/formbridge.git", branch = "main" } textual-image = { git = "https://github.com/OpenHCSDev/textual-image.git", branch = "main" } textual-diff-view = { git = "https://github.com/OpenHCSDev/textual-diff-view.git", branch = "main" } -objectstate = { git = "https://github.com/OpenHCSDev/objectstate.git", rev = "226fcd8918e16f6d1b594cac0bdcf8057aab832a" } +objectstate = { git = "https://github.com/OpenHCSDev/objectstate.git", rev = "d34d75803d3b7e65128bf43ab867bb70f37cd013" } [project.scripts] toad = "toad.cli:main" diff --git a/src/toad/configured_view.py b/src/toad/configured_view.py index 65383968e..9647e3692 100644 --- a/src/toad/configured_view.py +++ b/src/toad/configured_view.py @@ -7,26 +7,17 @@ from toad.screens.modal import DismissibleScreen, ModalPanel from toad.forms.presentation import FormModal from toad.session_tracker import SidebarState -from toad.settings_scope import ObjectSidebarHost, ViewSettingsScope -from toad.sidebar_placement import ObjectSidebarPlacement +from toad.settings_scope import ViewSettingsScope from toad.widgets.acting_button import DismissButton from toad.widgets.comms_menu import ActionMenu from toad.widgets.side_bar import ( - LazyThreadSidebar, SideBar, SidebarPanelWidget, + ThreadSidebar, ) -class ObjectSidebar(LazyThreadSidebar): - placement_type = ObjectSidebarPlacement - def __init__(self, configuration): - self.configuration = configuration - super().__init__(state=SidebarState()) - - def make_panels(self): - return (SideBar.Panel("Properties", ObjectProperties.whole(self.configuration), flex=True),) class ObjectProperties(ObjectFormView, SidebarPanelWidget): @@ -55,16 +46,20 @@ def configuration_state(self) -> ObjectConfiguration: return ObjectConfiguration(identity, self.app.saved_settings.state, self.app.saved_settings.path().parent, self) + @property + def configuration_panel(self): + return SideBar.Panel("Properties", ObjectProperties.whole(self.configuration_state), + flex=True, collapsed=True) + + def make_thread_sidebar(self): + return ThreadSidebar(state=SidebarState()) + @cached_property - def configuration_sidebar(self): - return ObjectSidebar(self.configuration_state) + def thread_sidebar(self): + return self.make_thread_sidebar() def sidebars(self): - return (*super().sidebars(), self.configuration_sidebar) - - @on(events.Mount) - async def mount_configuration(self): - await self.query_one(ObjectSidebarHost).mount(self.configuration_sidebar) + return (self.thread_sidebar,) @on(events.Unmount) def close_configuration(self): diff --git a/src/toad/screens/file_preview.py b/src/toad/screens/file_preview.py index 53fdd0669..4594184e4 100644 --- a/src/toad/screens/file_preview.py +++ b/src/toad/screens/file_preview.py @@ -36,5 +36,6 @@ def project_root(self) -> Path: def compose(self) -> ComposeResult: with SessionBody(): + yield self.thread_sidebar with FileContents(): yield self.preview diff --git a/src/toad/screens/session_view.py b/src/toad/screens/session_view.py index a39a73bc4..a68261a6d 100644 --- a/src/toad/screens/session_view.py +++ b/src/toad/screens/session_view.py @@ -7,7 +7,7 @@ from textual.color import Color from textual.layout import Layout from toad.configured_view import ConfiguredView -from toad.settings_scope import ConversationSettingsScope, ObjectSidebarHost +from toad.settings_scope import ConversationSettingsScope from ui_theme.scheme import BackgroundColour from textual.reactive import reactive from contextlib import contextmanager @@ -175,7 +175,7 @@ def retained_native_presentations(self): return () -class SessionBody(WidgetView, ToadNode, ObjectSidebarHost, Center): +class SessionBody(WidgetView, ToadNode, Center): """The shared sidebar/content layout reads its owner directly.""" width = height = FILL @@ -223,9 +223,4 @@ def _thread_sidebar_state(self) -> SidebarState: @abstractmethod def make_thread_sidebar(self) -> SidebarT: ... - @cached_property - def thread_sidebar(self) -> SidebarT: - return self.make_thread_sidebar() - def sidebars(self) -> tuple[SideBar, ...]: - return (self.thread_sidebar, self.configuration_sidebar) diff --git a/src/toad/settings_scope.py b/src/toad/settings_scope.py index 0a93f2ef1..be954c933 100644 --- a/src/toad/settings_scope.py +++ b/src/toad/settings_scope.py @@ -2,7 +2,6 @@ from abc import abstractmethod from objectstate import ObjectState from textual.dom import DOMNode -from textual.widget import Widget class SettingsReader(DOMNode): @@ -17,7 +16,7 @@ def configuration_state(self) -> ObjectState: @property def settings(self): - return self.configuration_state.to_resolved_object() + return self.configuration_state.fields class SettingsScope(SettingsReader): @@ -32,10 +31,13 @@ def settings_widgets(self, widget_type): class ViewSettingsScope(SettingsScope): """A file, thread or channel owns its display overrides.""" + @property + @abstractmethod + def configuration_panel(self): ... + class ConversationSettingsScope(ViewSettingsScope): """A thread or channel additionally owns conversation and composer settings.""" -class ObjectSidebarHost(Widget): - """The native body in which a viewed object's properties are placed.""" + diff --git a/src/toad/sidebar_layout.py b/src/toad/sidebar_layout.py index af4da4848..9182ef855 100644 --- a/src/toad/sidebar_layout.py +++ b/src/toad/sidebar_layout.py @@ -11,7 +11,14 @@ from textual.widgets import Static from toad.preferences import SETTINGS, SavedSettings -from toad.sidebar_placement import BandSplit, SidebarEdge, SidebarFold, SidebarGeometry, SidebarPlacement +from toad.sidebar_placement import ( + ActiveSidebarPlacement, + BandSplit, + SidebarEdge, + SidebarFold, + SidebarGeometry, + SidebarPlacement, +) from toad.terminal_origin import TerminalLayout, TerminalOrigin @@ -146,7 +153,7 @@ def terminal(self) -> TerminalLayout: @property def placements(self) -> dict[type[SidebarPlacement], SidebarPlacement]: saved = {type(placement): placement for placement in self.terminal.placements} - return {role: saved.get(role, role.initial()) for role in SidebarPlacement.members_with(SidebarPlacement)} + return {role: saved.get(role, role.initial()) for role in ActiveSidebarPlacement.members_with(ActiveSidebarPlacement)} def split(self, side: type[SidebarEdge]) -> int: """The first bar's share of the band on this edge.""" diff --git a/src/toad/sidebar_placement.py b/src/toad/sidebar_placement.py index 540ac98f9..761122055 100644 --- a/src/toad/sidebar_placement.py +++ b/src/toad/sidebar_placement.py @@ -338,13 +338,17 @@ def heading(cls, viewed: Iterable[str]) -> str: return cls.scope -class ChannelsSidebarPlacement(SidebarPlacement): +class ActiveSidebarPlacement(SidebarPlacement, declared_name=None): + """The sidebar roles a current view can actually own.""" + + +class ChannelsSidebarPlacement(ActiveSidebarPlacement): default_side = LeftSidebarEdge default_extent_percent = 40 scope = "all" -class ThreadSidebarPlacement(SidebarPlacement): +class ThreadSidebarPlacement(ActiveSidebarPlacement): default_side = RightSidebarEdge default_extent_percent = 34 scope = "this" @@ -356,12 +360,8 @@ def heading(cls, viewed: Iterable[str]) -> str: return next(iter(viewed), cls.scope) -class ObjectSidebarPlacement(ThreadSidebarPlacement): - """Properties follow the viewed object, beside its other panels.""" - - @classmethod - def heading(cls, viewed: Iterable[str]) -> str: - return f"{super().heading(viewed)} properties" +class ObjectSidebarPlacement(SidebarPlacement): + """Read already-saved third-bar positions; no current view owns this role.""" @dataclass diff --git a/src/toad/widgets/side_bar.py b/src/toad/widgets/side_bar.py index 6fe6121b2..3091b734e 100644 --- a/src/toad/widgets/side_bar.py +++ b/src/toad/widgets/side_bar.py @@ -7,6 +7,7 @@ from metaclass_registry.mro_dispatch import handles from toad.core_event_carrier import CoreEventReceiver +from toad.settings_scope import ViewSettingsScope from toad.worker_group import WorkerGroup from toad.core import events as core_events from textual.content import Content @@ -1078,9 +1079,12 @@ def search_changed(self, event: widgets.Input.Changed) -> None: event.stop() self.search = event.value + def panel_entries(self): + return iter(self.panels) + def _compose_panels(self) -> ComposeResult: navigation = self.navigation - for panel in self.panels: + for panel in self.panel_entries(): frame = FlexSideBarCollapsible if panel.flex else SideBarCollapsible yield frame( panel.widget, @@ -1253,6 +1257,10 @@ def dismiss_control(self) -> None: class ThreadSidebar(SideBar): placement_type = ThreadSidebarPlacement + def panel_entries(self): + yield from super().panel_entries() + yield self.query_ancestor(ViewSettingsScope).configuration_panel + def __init__(self, *panels, state: SidebarState, **kwargs): self.state = state super().__init__(*panels, placement_identity=self.placement_type, hide=True, **kwargs)