Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .verification/p192-viewed-object-configuration/delivery.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
P192 — viewed-object properties

Toad PR: https://github.com/OpenHCSDev/toad/pull/1750
ObjectState PR: https://github.com/OpenHCSDev/ObjectState/pull/66
ObjectState merged: 226fcd8918e16f6d1b594cac0bdcf8057aab832a

The same ObjectConfiguration edits the properties sidebar and context form.
The settings declarations own field scope; ordinary files inherit view settings,
conversations inherit those and their composer/agent display fields. The actual
TabIdentity is saved beside raw explicit overrides. None means inherit; choosing
Override here pins even an equal value, and Use global default resumes inheritance.
The original global ObjectState supplies values and source information. A duplicate
open identity or a forbidden override refuses rather than introducing another answer.
Local display configuration stays local; remote agent and rule writes remain Core's.

ObjectState previously stopped at direct dataclass children. The shared extraction
now traverses each declared stored object once, retains shared instances and refuses
conflicting instances of one nominal configuration type. No parallel resolver.

Closed property sidebars inherit LazyThreadSidebar; the native session sidebar uses
that same lifecycle instead of a copy. Editors paint after native mount. Global and
object field-view families remain separate through their declared family owner.
All contextual action menus carry actual operations; the mirrored string/action
maps and show_target_menu are removed. Link menus keep their original admission.

Evidence:
- p192-object-control.txt: passed live nested defaults, explicit-equal override,
same-state sidebar/context edits, reset, source, persistence, and scope filtering.
- p192-objectstate-controls.txt: 14 passed, exit 0, including nested/shared identity
and collision controls.
- p192-view-lifecycle.txt: original thread/channel/file navigation and close case,
1 passed, exit 0. Initial launch failed before behavior because the inherited
descriptor limit was 1024 while the existing helper requests descriptors >=1024;
the successful launch used soft limit 8192, below hard limit 524288.
- App, MainScreen, FilePreviewScreen and the shared forms import on the new sources.

The source check is exactly class ObjectConfiguration(, not the dialog's name.
The original order's created-owner citation named the new file's first line; the
actual state class is in object_configuration.py. The eliminated file-menu identity
copy is now owned by the selected ActionMenu row, not by a second callable map.

No paid provider command or profile was run. No install or physical desktop trial
is claimed by this source report. Auth recovery is independently held by codex-parallel.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import ast,json
from pathlib import Path
r=Path('/home/ts/wt/codex-live--P181--toad/src/toad');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 imp(f,s):ops.append(dict(operation='ensure_import',file_path=str(r/f),import_source=s))
s=(r/'widgets/comms_menu.py').read_text();n=next(n for n in ast.parse(s).body if isinstance(n,ast.FunctionDef) and n.name=='show_target_menu')
patch('widgets/comms_menu.py','show_target_menu',ast.get_source_segment(s,n),'''class ActionMenu(ContextMenu[Callable[[], object]]):
"""The selected row carries the actual operation; there is no second lookup table."""

@classmethod
def show(cls, screen, anchor: Region, title: str, items):
return super().show(screen, anchor, title, items, callback=cls.run_action)

@staticmethod
async def run_action(action):
if action is not None:
await invoke(action)
''')
imp('widgets/comms_menu.py','from textual._callback import invoke')
patch('target_commands.py','TargetContext.show_menu','from toad.widgets.comms_menu import show_target_menu','from toad.widgets.comms_menu import ActionMenu')
patch('target_commands.py','TargetContext.show_menu',''' show_target_menu(selected_screen, Region.from_offset(offset, (0, 0)), self.title,
[(command.command.removeprefix('/'), command.label(self)) for command in choices],
{command.command.removeprefix('/'): partial(self.execute_menu, sidebar, command)
for command in choices})''',''' ActionMenu.show(selected_screen, Region.from_offset(offset, (0, 0)), self.title,
[(partial(self.execute_menu, sidebar, command), command.label(self))
for command in choices])''')
patch('widgets/thread_comms.py','toad.widgets.thread_comms','from toad.widgets.comms_menu import show_target_menu','from toad.widgets.comms_menu import ActionMenu')
patch('widgets/thread_comms.py','ThreadCommsSidebar.show_row_menu',''' items = [("open", "Open")] if row.available else []
items.append(("copy", "Copy name"))
show_target_menu(
self.screen,
Region.from_offset(offset, (0, 0)),
target,
items,
{"open": open_selected, "copy": lambda: self.app.copy_to_clipboard(target)},
)''',''' items = [(open_selected, "Open")] if row.available else []
items.append((lambda: self.app.copy_to_clipboard(target), "Copy name"))
ActionMenu.show(self.screen, Region.from_offset(offset, (0, 0)), target, items)''')
patch('widgets/context_explorer.py','ContextCorrect.apply','from toad.widgets.comms_menu import show_target_menu','from toad.widgets.comms_menu import ActionMenu')
patch('widgets/context_explorer.py','ContextCorrect.apply',''' show_target_menu(
self.screen,
self.region,
"Correct original answer",
[(answer.declared_name, answer.public_title()) for answer in answers],
{answer.declared_name: partial(self._correct, explorer, model, answer) for answer in answers},
)''',''' ActionMenu.show(
self.screen, self.region, "Correct original answer",
[(partial(self._correct, explorer, model, answer), answer.public_title())
for answer in answers],
)''')
patch('configured_view.py','toad.configured_view','from toad.widgets.comms_menu import ContextMenu','from toad.widgets.comms_menu import ActionMenu')
patch('configured_view.py','ConfiguredView.configuration_menu',''' ContextMenu.show(self.screen, Region.from_offset(event.screen_offset, (0, 0)),
self.display_name, [(self.open_configuration, "Properties…")],
callback=lambda chosen: chosen() if chosen is not None else None)''',''' ActionMenu.show(self.screen, Region.from_offset(event.screen_offset, (0, 0)),
self.display_name, [(self.open_configuration, "Properties…")])''')
patch('project_link.py','ProjectFileLink.copy_file','from toad.widgets.comms_menu import ContextMenu','from toad.widgets.comms_menu import ActionMenu')
patch('project_link.py','ProjectFileLink.copy_file',''' ContextMenu.show(widget.screen, Region.from_offset(event.screen_offset, (0, 0)), full_path,
[(copy_path, "Copy full path")],
callback=lambda chosen: chosen() if chosen is not None else None)''',''' ActionMenu.show(widget.screen, Region.from_offset(event.screen_offset, (0, 0)), full_path,
[(copy_path, "Copy full path")])''')
Path('/home/ts/.cache/agent-scratch/codex-latency/p192-action-menus.plan.json').write_text(json.dumps({'recipes':[dict(recipe_id='menu-rows-own-their-actual-operation',operations=ops)]},indent=2))
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import ast,json
from pathlib import Path
root=Path('/home/ts/wt/codex-live--P181--toad/src/toad');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)]))
def imp(file,source):ops.append(dict(operation='ensure_import',file_path=str(root/file),import_source=source))
patch('column_layout.py','toad.column_layout','from textual.scrollable_container import ScrollableContainer','from textual.containers import ScrollableContainer')
# The common native body owns mounting for every configured source.
ops.append(dict(operation='insert_after_target',file_path=str(root/'settings_scope.py'),target_qualname='ConversationSettingsScope',source='''class ObjectSidebarHost(Widget):
"""The native body in which a viewed object's properties are placed."""
'''))
imp('settings_scope.py','from textual.widget import Widget')
imp('screens/session_view.py','from toad.settings_scope import ObjectSidebarHost')
patch('screens/session_view.py','SessionBody','class SessionBody(WidgetView, ToadNode, Center):','class SessionBody(WidgetView, ToadNode, ObjectSidebarHost, Center):')
patch('screens/session_view.py','SidebarSessionView','def sidebars(self) -> tuple[SidebarT, ...]:','def sidebars(self) -> tuple[SideBar, ...]:')
imp('screens/file_preview.py','from toad.screens.session_view import SessionBody')
patch('screens/file_preview.py','FilePreviewScreen.compose',''' with FileContents():
yield self.preview''',''' with SessionBody():
with FileContents():
yield self.preview''')
imp('configured_view.py','from toad.settings_scope import ObjectSidebarHost')
imp('configured_view.py','from toad.session_tracker import SidebarState')
patch('configured_view.py','ConfiguredView',''' return SideBar(SideBar.Panel("Properties", ObjectProperties.whole(self.configuration_state), flex=True),
placement_identity=ObjectSidebarPlacement, hide=True)''',''' return ObjectSidebar(SideBar.Panel("Properties", ObjectProperties.whole(self.configuration_state), flex=True),
state=SidebarState())''')
patch('configured_view.py','ConfiguredView',' @on(events.Unmount)',''' @on(events.Mount)
async def mount_configuration(self):
await self.query_one(ObjectSidebarHost).mount(self.configuration_sidebar)

@on(events.Unmount)''')
imp('configured_view.py','from toad.widgets.side_bar import ThreadSidebar')
ops.append(dict(operation='insert_before_target',file_path=str(root/'configured_view.py'),target_qualname='ObjectProperties',source='''class ObjectSidebar(ThreadSidebar):
placement_type = ObjectSidebarPlacement


'''))
patch('widgets/side_bar.py','ThreadSidebar','class ThreadSidebar(SideBar):','class ThreadSidebar(SideBar):\n placement_type = ThreadSidebarPlacement\n')
patch('widgets/side_bar.py','ThreadSidebar','placement_identity=ThreadSidebarPlacement','placement_identity=self.placement_type')
# Remove the holes left by declaration relocation, keeping field source otherwise exact.
s=(root/'preferences.py').read_text();c=next(n for n in ast.parse(s).body if isinstance(n,ast.ClassDef) and n.name=='UiSettings');before=ast.get_source_segment(s,c);after=before
while '\n\n\n' in after:after=after.replace('\n\n\n','\n\n')
if after!=before:patch('preferences.py','UiSettings',before,after)
Path('/home/ts/.cache/agent-scratch/codex-latency/p192-assembly.plan.json').write_text(json.dumps({'recipes':[dict(recipe_id='native-sidebar-assembly-and-shared-placement',operations=ops)]},indent=2)+'\n')
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import ast,json
from pathlib import Path
r=Path('/home/ts/wt/codex-live--P181--toad/src/toad');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 imp(f,s):ops.append(dict(operation='ensure_import',file_path=str(r/f),import_source=s))
ops.append(dict(operation='insert_after_target',file_path=str(r/'widgets/side_bar.py'),target_qualname='ThreadSidebar',source='''class LazyThreadSidebar(ThreadSidebar):
"""Create panel widgets only when this view's sidebar opens."""

def __init__(self, *panels, **kwargs):
super().__init__(*panels, defer_mount=True, **kwargs)

@property
def wants_panels(self) -> bool:
return super().wants_panels and not self.collapsed

@abstractmethod
def make_panels(self): ...

def _compose_panels(self):
if not self.panels:
self.panels.extend(self.make_panels())
yield from super()._compose_panels()
'''))
imp('widgets/side_bar.py','from abc import abstractmethod')
patch('widgets/session_thread_sidebar.py','SessionThreadSidebar','class SessionThreadSidebar(ThreadSidebar, NativePresentation):','class SessionThreadSidebar(LazyThreadSidebar, NativePresentation):')
patch('widgets/session_thread_sidebar.py','SessionThreadSidebar','super().__init__(state=screen._thread_sidebar_state, defer_mount=True)','super().__init__(state=screen._thread_sidebar_state)')
s=(r/'widgets/session_thread_sidebar.py').read_text();cls=next(n for n in ast.parse(s).body if isinstance(n,ast.ClassDef) and n.name=='SessionThreadSidebar');node=next(n for n in cls.body if isinstance(n,ast.FunctionDef) and n.name=='wants_panels');start=node.decorator_list[0].lineno;old='\n'.join(s.splitlines()[start-1:node.end_lineno]);patch('widgets/session_thread_sidebar.py','SessionThreadSidebar',old,'')
old=''' def _compose_panels(self) -> ComposeResult:
if not self.panels:
screen = self._owner()
if screen is None:
raise ReferenceError("The session sidebar owner has retired")
self.panels.extend(owner.compose_panel(screen) for owner in self._panel_owners)
yield from super()._compose_panels()'''
new=''' def make_panels(self):
screen = self._owner()
if screen is None:
raise ReferenceError("The session sidebar owner has retired")
return (owner.compose_panel(screen) for owner in self._panel_owners)'''
patch('widgets/session_thread_sidebar.py','SessionThreadSidebar',old,new)
imp('widgets/session_thread_sidebar.py','from toad.widgets.side_bar import LazyThreadSidebar')
patch('configured_view.py','ObjectSidebar','class ObjectSidebar(ThreadSidebar):\n placement_type = ObjectSidebarPlacement','''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),)''')
imp('configured_view.py','from toad.widgets.side_bar import LazyThreadSidebar')
patch('configured_view.py','ConfiguredView',''' return ObjectSidebar(SideBar.Panel("Properties", ObjectProperties.whole(self.configuration_state), flex=True),
state=SidebarState())''',''' return ObjectSidebar(self.configuration_state)''')
patch('forms/form_view.py','FormView.follow_state',''' self.state.on_state_changed(self.marks_changed)
self.marks_changed(self.state.dirty_fields)''',''' self.state.on_state_changed(self.marks_changed)
self.values_changed(set(self.values))
self.marks_changed(self.state.dirty_fields)''')
# Avoid keeping the inherited parent name as a second unused import.
for file,names in [('configured_view.py',['ThreadSidebar']),('widgets/session_thread_sidebar.py',['ThreadSidebar','ComposeResult'])]:
ops.append(dict(operation='remove_import_bindings',file_path=str(r/file),bound_names=names))
Path('/home/ts/.cache/agent-scratch/codex-latency/p192-lazy-panels.plan.json').write_text(json.dumps({'stages':[{'recipes':[dict(recipe_id='inherit-one-lazy-view-sidebar-lifecycle',operations=[op for op in ops if op['operation']!='remove_import_bindings'])]},{'recipes':[dict(recipe_id='remove-replaced-parent-imports',operations=[op for op in ops if op['operation']=='remove_import_bindings'])]}]},indent=2))
Loading
Loading