Plan: Refactor lifecycle.py to AppLifecycle
Current State
The src/tunacode/ui/lifecycle.py module contains free functions:
| Function | Responsibility |
|---|---|
on_mount(app) |
Entry point — theme, session metadata, setup-vs-repl branch |
_on_setup_complete(app, completed) |
Setup dismiss callback |
_start_repl(app) |
Logger wiring, request worker, timer, welcome |
on_unmount(app) |
Stop timer + save session |
Problems
- Lifecycle state is implicit and spread across function scope +
appfields. appis threaded through every call.- Setup callback indirection (
_on_setup_callback) adds noise. _start_replcan be reached from two paths with no explicit idempotency guard.- Mount/unmount logic is not represented as one cohesive object.
Proposed Design
Create an AppLifecycle class that owns lifecycle sequencing and mount/unmount flow.
class AppLifecycle:
"""Manage TunaCode app lifecycle stages."""
def __init__(self, app: TextualReplApp) -> None:
self._app = app
self._state_manager = app.state_manager
self._repl_started = False
def mount(self) -> None:
self._init_theme()
self._init_session_metadata()
if self._app._show_setup:
self._push_setup_screen()
return
self._start_repl()
async def unmount(self) -> None:
self._stop_slopgotchi_timer()
await self._state_manager.save_session()
def _push_setup_screen(self) -> None:
self._app.push_screen(SetupScreen(self._state_manager), self._on_setup_complete)
def _on_setup_complete(self, completed: bool | None) -> None:
if completed:
self._app._update_resource_bar()
self._start_repl()
def _start_repl(self) -> None:
if self._repl_started:
return
self._repl_started = True
...
App Integration (Critical)
TextualReplApp must persist one lifecycle instance:
self._lifecycle = AppLifecycle(self)
self._lifecycle.mount()
...
await self._lifecycle.unmount()
Do not instantiate a fresh AppLifecycle in on_unmount; that would drop mount-time state like _repl_started.
Timer Ownership
Keep timer ownership on TextualReplApp._slopgotchi_timer (existing field) to avoid split state and minimize migration risk.
AppLifecycle starts/stops that timer via dedicated helper methods.
Benefits
- Single lifecycle controller with explicit sequencing.
- Cleaner app boundary:
mount()/unmount()as the public lifecycle API. - Fewer cross-function parameters (
self._appreplaces repeatedapp). - Idempotent REPL startup via
_repl_startedguard. - Easier to unit test by mocking app/state manager boundaries.
Implementation Steps
- Replace free functions in
lifecycle.pywithAppLifecyclemethods. - Update
TextualReplAppto holdself._lifecycle: AppLifecycle | None. - In
on_mount, create + store lifecycle, then callmount(). - In
on_unmount, assert lifecycle exists, then callawait unmount(). - Remove old function imports/call sites from
app.py. - Run lint + tests.
Validation Checklist
- Mount with setup disabled starts worker, logger callback, timer, welcome exactly once.
- Mount with setup enabled starts REPL after setup dismiss callback.
_start_replremains idempotent.- Unmount stops timer and saves session.
- No remaining imports of
on_mount/on_unmountfromtunacode.ui.lifecycle.
Notes
- Keep lazy import of
SetupScreenandshow_welcometo avoid circular dependencies. - Preserve current behavior where setup dismissal still enters REPL flow (regardless of completion value).