diff --git a/CHANGELOG.md b/CHANGELOG.md index 495e7a7d..915bf4a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,14 @@ Status of the `main` branch. Changes prior to the next official version change w successful Serena call. Add a `serena-hooks reset` command and a `PostToolUse` example matched to Serena's own tools to close the gap (#1852) +* Dashboard: + - Fix: On macOS, the tray manager refreshed the tray menu straight from the Flask request handlers + for `/register`, `/update_project` and `/unregister` and from the alive-check thread. That reaches + `NSStatusItem.setMenu_()` off the main thread, which AppKit forbids and which recent macOS + versions punish with SIGTRAP, so the tray-manager process died within seconds of every agent + start and the tray icon never became usable. Menu refreshes are now marshalled onto the main + thread (#2038) + * Language Servers: - Fix: Godot's GDScript parser can report a symbol's end column one column past the line-end convention every other language server follows (closing a node's range from diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 4d0574b8..e186117c 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -1030,9 +1030,28 @@ class SerenaDashboardTrayManager: log.info("Unregistered instance on port %d", port) return {"status": "unregistered"} + @staticmethod + def _run_in_ui_thread(fn: Callable[[], None]) -> None: + """ + Runs a UI mutation in the thread in which the platform's UI toolkit requires it to run. + + On macOS, AppKit demands that mutations of the status item happen on the main thread, and + recent macOS versions terminate the process with SIGTRAP when they do not. The tray manager + reaches such mutations from Flask request handlers and from the alive-check thread, so the + call has to be marshalled. On other platforms it is made directly. + + :param fn: the UI mutation to run + """ + if sys.platform == "darwin": + from PyObjCTools import AppHelper + + AppHelper.callAfter(fn) + else: + fn() + def _update_menu(self) -> None: if self._tray_icon: - self._tray_icon.update_menu() + self._run_in_ui_thread(self._tray_icon.update_menu) def _build_menu_items(self) -> tuple[Any, ...]: """