diff --git a/src/toad/frame_presentation.py b/src/toad/frame_presentation.py index 7945ee6fd..8398e4eb2 100644 --- a/src/toad/frame_presentation.py +++ b/src/toad/frame_presentation.py @@ -2,60 +2,15 @@ from __future__ import annotations import asyncio -import sys -from abc import abstractmethod from collections.abc import Callable from functools import partial from weakref import ref from agent_comms.declared_family import DeclaredFamily -from textual.driver import Driver from textual.widget import Widget from toad.screens.workspace import WorkspaceScreen -class FrameFlush(DeclaredFamily, affix="Flush"): - driver_type: type[Driver] - - def __init__(self, driver: Driver): - self.driver = driver - - @classmethod - def for_driver(cls, driver: Driver) -> FrameFlush: - declarations = {kind.driver_type: kind for kind in cls.members_with(cls)} - return next(declarations[ancestor](driver) for ancestor in type(driver).__mro__ if ancestor in declarations) - - @abstractmethod - def submit(self, callback: Callable[[], None]) -> None: ... - - -class SynchronousFrameFlush(FrameFlush): - driver_type = Driver - - def submit(self, callback: Callable[[], None]) -> None: - callback() - - -if sys.platform != "win32": - # The framework's POSIX driver imports termios; its declaration exists only - # at that external platform boundary, like Textual's own driver selection. - from textual.drivers.linux_driver import LinuxDriver - - class TerminalFrameFlush(FrameFlush): - driver_type = LinuxDriver - - def submit(self, callback: Callable[[], None]) -> None: - loop = asyncio.get_running_loop() - - def written(): - try: - loop.call_soon_threadsafe(callback) - except RuntimeError: - pass # The application loop closed after its terminal write. - - self.driver.call_after_flush(written) - - class FrameState(DeclaredFamily, affix="Frame"): ready = False @@ -66,7 +21,7 @@ def scene(self): def displayed(self, frame, deferred): receipt = WritingFrame(self.scene, deferred) frame.state = receipt - FrameFlush.for_driver(frame.screen.app._driver).submit(partial(frame.written, receipt)) + frame.screen.app._driver.call_after_flush(partial(frame.written, receipt)) def defer(self, frame, owner, callback): frame.callbacks[owner, callback] = None @@ -178,7 +133,7 @@ def defer(self, owner: Widget, callback: Callable[[], object]) -> None: def flush_owner(self, owner: Widget, callback: Callable[[], object]) -> None: """Native sender admission precedes the original terminal writer join.""" if (owner, callback) in self.callbacks: - FrameFlush.for_driver(self.screen.app._driver).submit( + self.screen.app._driver.call_after_flush( partial(self.release, owner, callback, self.state.scene)) def release(self, owner: Widget, callback: Callable[[], object], scene: PendingFrame) -> None: diff --git a/tests/native_session_retention_pilot.py b/tests/native_session_retention_pilot.py index 082a627d4..12df2a28b 100644 --- a/tests/native_session_retention_pilot.py +++ b/tests/native_session_retention_pilot.py @@ -57,8 +57,7 @@ def _display(self, screen, renderable): if all(marker in paint for marker in self.observed_expected): self.observed_frames.append((time.monotonic() - self.observed_started) * 1000) from functools import partial - from toad.frame_presentation import FrameFlush - FrameFlush.for_driver(self._driver).submit(partial( + self._driver.call_after_flush(partial( self.record_written_destination, self.observed_started, self.observed_destination)) diff --git a/tools/performance/capture_state.py b/tools/performance/capture_state.py index 3b2e5491d..ea37b9890 100644 --- a/tools/performance/capture_state.py +++ b/tools/performance/capture_state.py @@ -81,8 +81,6 @@ async def acquire_and_capture(): write_json(prefix + ".json", {"pid": expected_pid, "scope": "driver frame trace only"}) return if wait_history_seconds: - from toad.frame_presentation import FrameFlush - if not wait_history_thread: raise ValueError("Visible-history waiting requires the intended thread") @@ -118,7 +116,7 @@ def acknowledge(): written.set_result(None) app.call_after_refresh( - lambda: FrameFlush.for_driver(app._driver).submit(acknowledge)) + lambda: app._driver.call_after_flush(acknowledge)) await written if visible_history_ready(): write_json(prefix + "-wait.json", { diff --git a/tools/performance/sidebar_validation_driver.py b/tools/performance/sidebar_validation_driver.py index 09cf1bf25..bbebcdcad 100644 --- a/tools/performance/sidebar_validation_driver.py +++ b/tools/performance/sidebar_validation_driver.py @@ -194,6 +194,8 @@ def observed_exception(self, error): (Conversation, Conversation.on_agent_ready), ] if os.environ.get("TOAD_VALIDATION_OPEN_STAGES"): + from toad.acp.agent_process import AgentProcess + from toad.acp.agent_session import AgentSession from textual.widget import Widget from toad.sidebar_observation import SidebarObservation from toad.sidebar_projection import SidebarProjection @@ -206,6 +208,11 @@ def observed_exception(self, error): (SidebarProjection, SidebarProjection.rebuild), (SessionsTabs, SessionsTabs._sync_tabs), (NavigationReader, NavigationReader.read), + (AgentProcess, AgentProcess.start), + (AgentSession, AgentSession.initialize), + (AgentSession, AgentSession.load), + (AgentSession, AgentSession.touch), + (AgentSession, AgentSession.run), )) constructor = Widget.__init__ preprocess = Widget._pre_process @@ -238,6 +245,7 @@ async def measured_navigation(self, *args, _function=original_method, _name=meth return await _function(self, *args, **kwargs) finally: record("navigation_stage", stage=_name, begin_ns=begin, + function=_function.__qualname__, object_id=id(self), duration_ms=(time.monotonic_ns()-begin)/1e6, mode=args[0] if _name == "_switch_mode_ready" and args else self.app.current_mode if hasattr(self, "app") else None, @@ -373,7 +381,6 @@ def observe_useful_output(cls, app, screen, update, begin, displayed, cpu): Scalars alone survive the callback; no widget/render-cache graph is retained by the observer. Headless output remains source evidence. """ - from toad.frame_presentation import FrameFlush from toad.widgets.conversation import Conversation, Window from toad.widgets.transcript_history import TranscriptHistory from toad.widgets.viewport_body import MeasuredViewportBody @@ -422,7 +429,7 @@ def observe_useful_output(cls, app, screen, update, begin, displayed, cpu): record("body_output", **values) if not app.is_headless: mode = app.current_mode - FrameFlush.for_driver(app._driver).submit( + app._driver.call_after_flush( lambda: record("body_output_written", display_begin_ns=begin, mode=mode, current_mode=app.current_mode))