"""Platform-neutral lifecycle for the FCC desktop shell.""" from __future__ import annotations import threading from collections.abc import Callable from typing import Protocol from free_claude_code.cli.commands import ( ServerStatus, ServerSupervisor, load_server_settings, open_admin_when_ready, schedule_open_admin_browser, ) from free_claude_code.cli.launchers.common import preflight_proxy from free_claude_code.config.paths import config_dir_path from free_claude_code.config.server_urls import local_proxy_root_url from free_claude_code.config.settings import get_settings from free_claude_code.core.interprocess_lock import InterprocessFileLock class DesktopTray(Protocol): """UI loop owned by the platform tray adapter.""" def run(self) -> None: ... def stop(self) -> None: ... class DesktopTrayFactory(Protocol): """Construct a tray adapter around a desktop controller.""" def __call__(self, controller: DesktopController) -> DesktopTray: ... class ServerOwner(Protocol): """Server lifecycle used by the desktop controller.""" @property def status(self) -> ServerStatus: ... def schedule_run(self) -> bool: ... def run(self, *, open_admin_browser: bool | None = None) -> None: ... def request_restart(self) -> bool: ... def request_stop(self) -> None: ... class DesktopController: """Coordinate one tray loop with one in-process FCC server owner.""" def __init__( self, supervisor: ServerOwner, tray_factory: DesktopTrayFactory, open_admin: Callable[[], None], ) -> None: self._supervisor = supervisor self._open_admin = open_admin self._thread_lock = threading.Lock() self._server_thread: threading.Thread | None = None self._tray = tray_factory(self) @property def status(self) -> ServerStatus: return self._supervisor.status def run(self) -> None: """Run the tray on this thread and the FCC server on its owned worker.""" self._start_server() try: self._tray.run() finally: self._supervisor.request_stop() self._tray.stop() with self._thread_lock: thread = self._server_thread if thread is not None: thread.join() def open_admin(self) -> None: self._open_admin() def restart_server(self) -> None: """Restart an active server or relaunch one that exited unexpectedly.""" with self._thread_lock: thread = self._server_thread if thread is not None and thread.is_alive(): self._supervisor.request_restart() return self._start_server() def quit(self) -> None: """Close the server gracefully and end the platform tray loop.""" self._supervisor.request_stop() self._tray.stop() def _start_server(self) -> None: with self._thread_lock: if self._server_thread is not None and self._server_thread.is_alive(): return if not self._supervisor.schedule_run(): return self._server_thread = threading.Thread( target=self._run_server, name="fcc-desktop-server", ) self._server_thread.start() def _run_server(self) -> None: self._supervisor.run(open_admin_browser=False) def launch_desktop(tray_factory: DesktopTrayFactory) -> None: """Start the singleton desktop host or focus the already running FCC UI.""" settings = load_server_settings() instance_lock = InterprocessFileLock(config_dir_path() / "desktop.lock") if not instance_lock.acquire(): open_admin_when_ready(settings) return try: if preflight_proxy(local_proxy_root_url(settings)) is None: open_admin_when_ready(settings) return supervisor = ServerSupervisor(console_logging=False) def open_current_admin() -> None: schedule_open_admin_browser(get_settings()) DesktopController(supervisor, tray_factory, open_current_admin).run() finally: instance_lock.release()