From f7f5a8e7e6905ba89e2bb9e9c83c3f0f088d4194 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 2 Mar 2026 21:12:23 +0100 Subject: [PATCH 01/17] Introduce SerenaDashboardViewer based on pywebview Shows the dashboard in a separate application window we can control, replacing the use of the webbrowser module to open the dashboard in a browser tab --- pyproject.toml | 2 + src/serena/agent.py | 20 ++-- src/serena/cli.py | 16 ++++ src/serena/dashboard.py | 92 ++++++++++++++++++ uv.lock | 202 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 326 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 587bbf2c..55265316 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,8 @@ dependencies = [ "tiktoken>=0.9.0", "anthropic>=0.54.0", "beautifulsoup4>=4.14.2", + "pywebview>=6.1", + "pystray>=0.19.5", ] [[tool.uv.index]] diff --git a/src/serena/agent.py b/src/serena/agent.py index 83f9694b..d19ff241 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -2,6 +2,7 @@ The Serena Model Context Protocol (MCP) Server """ +import multiprocessing import os import platform import subprocess @@ -27,7 +28,7 @@ from serena.config.serena_config import ( SerenaPaths, ToolInclusionDefinition, ) -from serena.dashboard import SerenaDashboardAPI +from serena.dashboard import SerenaDashboardAPI, SerenaDashboardViewer from serena.ls_manager import LanguageServerManager, LanguageServerManagerInitialisationError from serena.project import Project from serena.prompt_factory import SerenaPromptFactory @@ -375,8 +376,7 @@ class SerenaAgent: dashboard_url = f"http://{dashboard_host}:{port}/dashboard/index.html" self._dashboard_url = dashboard_url log.info("Serena web dashboard started at %s", dashboard_url) - if self.serena_config.web_dashboard_open_on_launch: - self.open_dashboard() + self._start_dashboard_viewer(minimized=not self.serena_config.web_dashboard_open_on_launch) # inform the GUI window (if any) if self._gui_log_viewer is not None: self._gui_log_viewer.set_dashboard_url(dashboard_url) @@ -528,11 +528,19 @@ class SerenaAgent: """ return self._dashboard_url + @staticmethod + def _start_dashboard_viewer_process_function(url: str, minimized: bool) -> None: + SerenaDashboardViewer(url, start_minimized=minimized).run() + + def _start_dashboard_viewer(self, minimized: bool) -> None: + url = self.get_dashboard_url() + assert url is not None + process = multiprocessing.Process(target=self._start_dashboard_viewer_process_function, args=(url, minimized), daemon=True) + process.start() + def open_dashboard(self) -> bool: """ - Opens the Serena web dashboard in the default web browser. - - :return: a message indicating success or failure + Opens the viewer for the Serena dashboard """ if self._dashboard_url is None: raise Exception("Dashboard is not running.") diff --git a/src/serena/cli.py b/src/serena/cli.py index 711e8e9a..61546b1d 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -350,6 +350,22 @@ class TopLevelCommands(AutoRegisteringGroup): else: print(f"{prefix}\n{instr}\n{postfix}") + @staticmethod + @click.command( + "dashboard-viewer", + help="Open the Serena dashboard viewer for a given URL.", + context_settings={"max_content_width": _MAX_CONTENT_WIDTH}, + ) + @click.argument("url", type=str) + @click.option("--width", type=int, default=1400, show_default=True, help="Window width.") + @click.option("--height", type=int, default=900, show_default=True, help="Window height.") + @click.option("--minimized", is_flag=True, default=False, help="Whether to start minimized/in tray.") + def dashboard_viewer(url: str, width: int, height: int, minimized: bool) -> None: + from serena.dashboard import SerenaDashboardViewer + + viewer = SerenaDashboardViewer(url, start_minimized=minimized, width=width, height=height) + viewer.run() + class ModeCommands(AutoRegisteringGroup): """Group for 'mode' subcommands.""" diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index a5c91a04..b2b23df7 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -1,12 +1,17 @@ import os import socket +import sys import threading from collections.abc import Callable from pathlib import Path from typing import TYPE_CHECKING, Any, Self +import pystray +import webview from flask import Flask, Response, request, send_from_directory +from PIL import Image from pydantic import BaseModel +from pystray import MenuItem as Item from sensai.util import logging from serena.analytics import ToolUsageStats @@ -643,3 +648,90 @@ class SerenaDashboardAPI: thread = threading.Thread(target=lambda: self.run(host=host, port=port), daemon=True) thread.start() return thread, port + + +class SerenaDashboardViewer: + """ + Minimal pywebview wrapper with optional system tray. + """ + + def __init__( + self, + url: str, + *, + start_minimized: bool = False, + width: int = 1400, + height: int = 900, + ): + self.url = url + self.tray = True + self.title = "Serena Dashboard" + self.width = width + self.height = height + self.start_minimized = start_minimized + + self.window: webview.Window | None = None + self._tray_icon = None + + def run(self) -> None: + # Set app id (avoid app being lumped together with other Python-based apps in Windows taskbar) + if sys.platform == "win32": + import ctypes + + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("oraios.serena") + + dashboard_path = Path(SERENA_DASHBOARD_DIR) + icon_path = str(dashboard_path / "serena-icon-48.png") + + # Create hidden to avoid flash; show/restore/minimize in start callback. + self.window = webview.create_window( + self.title, + self.url, + width=self.width, + height=self.height, + hidden=self.start_minimized, + confirm_close=False, + ) + + def _start_callback(): + if self.start_minimized: + self.window.minimize() + else: + self.window.show() + self.window.restore() + + if self.tray: + threading.Thread(target=self._run_tray, daemon=True).start() + + webview.start(_start_callback, icon=icon_path) + + def _run_tray(self) -> None: + dashboard_path = Path(SERENA_DASHBOARD_DIR) + + icon_path = dashboard_path / "serena-icon-48.png" + icon_img = Image.open(icon_path) + + def show(_icon, _item): + if self.window: + self.window.show() + self.window.restore() + + def hide(_icon, _item): + if self.window: + self.window.hide() + + def quit_app(_icon, _item): + try: + _icon.stop() + finally: + if self.window: + self.window.destroy() + + menu = pystray.Menu( + Item("Open", show, default=True), + Item("Hide", hide), + Item("Quit", quit_app), + ) + + self._tray_icon = pystray.Icon("dashboard_viewer", icon_img, self.title, menu) + self._tray_icon.run() diff --git a/uv.lock b/uv.lock index 1b96f636..2e111743 100644 --- a/uv.lock +++ b/uv.lock @@ -229,6 +229,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, ] +[[package]] +name = "bottle" +version = "0.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/71/cca6167c06d00c81375fd668719df245864076d284f7cb46a694cbeb5454/bottle-0.13.4.tar.gz", hash = "sha256:787e78327e12b227938de02248333d788cfe45987edca735f8f88e03472c3f47", size = 98717, upload-time = "2025-06-15T10:08:59.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/f6/b55ec74cfe68c6584163faa311503c20b0da4c09883a41e8e00d6726c954/bottle-0.13.4-py2.py3-none-any.whl", hash = "sha256:045684fbd2764eac9cdeb824861d1551d113e8b683d8d26e296898d3dd99a12e", size = 103807, upload-time = "2025-06-15T10:08:57.691Z" }, +] + [[package]] name = "cachecontrol" version = "0.14.3" @@ -323,6 +332,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] +[[package]] +name = "clr-loader" +version = "0.2.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/61/cf819f8e8bb4d4c74661acf2498ba8d4a296714be3478d21eaabf64f5b9b/clr_loader-0.2.10-py3-none-any.whl", hash = "sha256:ebbbf9d511a7fe95fa28a95a4e04cd195b097881dfe66158dc2c281d3536f282", size = 56483, upload-time = "2026-01-03T23:13:05.439Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -1350,6 +1371,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, +] + [[package]] name = "platformdirs" version = "4.3.8" @@ -1393,6 +1440,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, ] +[[package]] +name = "proxy-tools" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/cf/77d3e19b7fabd03895caca7857ef51e4c409e0ca6b37ee6e9f7daa50b642/proxy_tools-0.1.0.tar.gz", hash = "sha256:ccb3751f529c047e2d8a58440d86b205303cf0fe8146f784d1cbcd94f0a28010", size = 2978, upload-time = "2014-05-05T21:02:24.606Z" } + [[package]] name = "psutil" version = "7.0.0" @@ -1603,6 +1656,79 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pyobjc-core" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263, upload-time = "2025-11-14T09:31:35.231Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812, upload-time = "2025-11-14T09:40:53.169Z" }, +] + +[[package]] +name = "pyobjc-framework-quartz" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ef/dcd22b743e38b3c430fce4788176c2c5afa8bfb01085b8143b02d1e75201/pyobjc_framework_quartz-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19f99ac49a0b15dd892e155644fe80242d741411a9ed9c119b18b7466048625a", size = 217795, upload-time = "2025-11-14T09:59:46.922Z" }, +] + +[[package]] +name = "pyobjc-framework-security" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/aa/796e09a3e3d5cee32ebeebb7dcf421b48ea86e28c387924608a05e3f668b/pyobjc_framework_security-12.1.tar.gz", hash = "sha256:7fecb982bd2f7c4354513faf90ba4c53c190b7e88167984c2d0da99741de6da9", size = 168044, upload-time = "2025-11-14T10:22:06.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/3d/8d3a39cd292d7c76ab76233498189bc7170fc80f573b415308464f68c7ee/pyobjc_framework_security-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b2d8819f0fb7b619ec7627a0d8c1cac1a57c5143579ce8ac21548165680684b", size = 41287, upload-time = "2025-11-14T10:02:54.491Z" }, +] + +[[package]] +name = "pyobjc-framework-uniformtypeidentifiers" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/b8/dd9d2a94509a6c16d965a7b0155e78edf520056313a80f0cd352413f0d0b/pyobjc_framework_uniformtypeidentifiers-12.1.tar.gz", hash = "sha256:64510a6df78336579e9c39b873cfcd03371c4b4be2cec8af75a8a3d07dff607d", size = 17030, upload-time = "2025-11-14T10:23:02.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/5f/1f10f5275b06d213c9897850f1fca9c881c741c1f9190cea6db982b71824/pyobjc_framework_uniformtypeidentifiers-12.1-py2.py3-none-any.whl", hash = "sha256:ec5411e39152304d2a7e0e426c3058fa37a00860af64e164794e0bcffee813f2", size = 4901, upload-time = "2025-11-14T10:05:51.532Z" }, +] + +[[package]] +name = "pyobjc-framework-webkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/10/110a50e8e6670765d25190ca7f7bfeecc47ec4a8c018cb928f4f82c56e04/pyobjc_framework_webkit-12.1.tar.gz", hash = "sha256:97a54dd05ab5266bd4f614e41add517ae62cdd5a30328eabb06792474b37d82a", size = 284531, upload-time = "2025-11-14T10:23:40.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/37/5082a0bbe12e48d4ffa53b0c0f09c77a4a6ffcfa119e26fa8dd77c08dc1c/pyobjc_framework_webkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3db734877025614eaef4504fadc0fbbe1279f68686a6f106f2e614e89e0d1a9d", size = 49970, upload-time = "2025-11-14T10:07:01.413Z" }, +] + [[package]] name = "pyright" version = "1.1.403" @@ -1616,6 +1742,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504, upload-time = "2025-07-09T07:15:50.958Z" }, ] +[[package]] +name = "pystray" +version = "0.19.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, + { name = "python-xlib", marker = "sys_platform == 'linux'" }, + { name = "six" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/64/927a4b9024196a4799eba0180e0ca31568426f258a4a5c90f87a97f51d28/pystray-0.19.5-py2.py3-none-any.whl", hash = "sha256:a0c2229d02cf87207297c22d86ffc57c86c227517b038c0d3c59df79295ac617", size = 49068, upload-time = "2023-09-17T13:44:26.872Z" }, +] + [[package]] name = "pytest" version = "8.4.1" @@ -1687,6 +1827,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] +[[package]] +name = "python-xlib" +version = "0.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068, upload-time = "2022-12-25T18:53:00.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185, upload-time = "2022-12-25T18:52:58.662Z" }, +] + +[[package]] +name = "pythonnet" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "clr-loader" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/f1/bfb6811df4745f92f14c47a29e50e89a36b1533130fcc56452d4660bd2d6/pythonnet-3.0.5-py3-none-any.whl", hash = "sha256:f6702d694d5d5b163c9f3f5cc34e0bed8d6857150237fae411fefb883a656d20", size = 297506, upload-time = "2024-12-13T08:30:40.661Z" }, +] + +[[package]] +name = "pywebview" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bottle" }, + { name = "proxy-tools" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-uniformtypeidentifiers", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-webkit", marker = "sys_platform == 'darwin'" }, + { name = "pythonnet", marker = "sys_platform == 'win32'" }, + { name = "qtpy", marker = "sys_platform == 'openbsd6'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/09/75cf92c4db19350ddb084d841e352ce30e89668815d0494d1dd595c85469/pywebview-6.1.tar.gz", hash = "sha256:f0b95047860caf3d921581f9e16b4edb1a125b23e2ce691c4da2968f8b160ff2", size = 496270, upload-time = "2025-10-21T18:02:44.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/12/ce9006cdcc1834378826b5c0d4b554d899cd25ac9c665569d060f220b0e1/pywebview-6.1-py3-none-any.whl", hash = "sha256:2b552a340557e76a740b045a25937f72dae751e0985d5f5d9556f697ba622ec5", size = 511255, upload-time = "2025-10-21T18:02:42.377Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -1750,6 +1936,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] +[[package]] +name = "qtpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/01/392eba83c8e47b946b929d7c46e0f04b35e9671f8bb6fc36b6f7945b4de8/qtpy-2.4.3.tar.gz", hash = "sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb", size = 66982, upload-time = "2025-02-11T15:09:25.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045, upload-time = "2025-02-11T15:09:24.162Z" }, +] + [[package]] name = "referencing" version = "0.36.2" @@ -1945,7 +2143,9 @@ dependencies = [ { name = "psutil" }, { name = "pydantic" }, { name = "pyright" }, + { name = "pystray" }, { name = "python-dotenv" }, + { name = "pywebview" }, { name = "pyyaml" }, { name = "requests" }, { name = "ruamel-yaml" }, @@ -2009,10 +2209,12 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.10.6" }, { name = "pyinstrument", marker = "extra == 'dev'" }, { name = "pyright", specifier = ">=1.1.396,<2" }, + { name = "pystray", specifier = ">=0.19.5" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.2" }, { name = "pytest-timeout", marker = "extra == 'dev'", specifier = ">=2.4.0" }, { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.5.0" }, { name = "python-dotenv", specifier = ">=1.0.0,<2" }, + { name = "pywebview", specifier = ">=6.1" }, { name = "pyyaml", specifier = ">=6.0.2" }, { name = "requests", specifier = ">=2.32.3,<3" }, { name = "ruamel-yaml", specifier = "==0.18.14" }, From 12b63721619b5b1c01a183975c70a140b1bf2dff Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 7 Mar 2026 12:18:18 +0100 Subject: [PATCH 02/17] Depend on dev version of pywebview for improved icon support --- pyproject.toml | 5 ++++- uv.lock | 10 +++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 55265316..f4ad1be8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,10 +36,13 @@ dependencies = [ "tiktoken>=0.9.0", "anthropic>=0.54.0", "beautifulsoup4>=4.14.2", - "pywebview>=6.1", + "pywebview", "pystray>=0.19.5", ] +[tool.uv.sources] +pywebview = { git = "https://github.com/r0x0r/pywebview", rev = "345596c1f4fd59fd26c7aa02d4176c5362bcbabe" } + [[tool.uv.index]] name = "testpypi" url = "https://test.pypi.org/simple/" diff --git a/uv.lock b/uv.lock index 2e111743..7a750069 100644 --- a/uv.lock +++ b/uv.lock @@ -1853,8 +1853,8 @@ wheels = [ [[package]] name = "pywebview" -version = "6.1" -source = { registry = "https://pypi.org/simple" } +version = "6.2.dev50+g345596c1f" +source = { git = "https://github.com/r0x0r/pywebview?rev=345596c1f4fd59fd26c7aa02d4176c5362bcbabe#345596c1f4fd59fd26c7aa02d4176c5362bcbabe" } dependencies = [ { name = "bottle" }, { name = "proxy-tools" }, @@ -1868,10 +1868,6 @@ dependencies = [ { name = "qtpy", marker = "sys_platform == 'openbsd6'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/09/75cf92c4db19350ddb084d841e352ce30e89668815d0494d1dd595c85469/pywebview-6.1.tar.gz", hash = "sha256:f0b95047860caf3d921581f9e16b4edb1a125b23e2ce691c4da2968f8b160ff2", size = 496270, upload-time = "2025-10-21T18:02:44.323Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/12/ce9006cdcc1834378826b5c0d4b554d899cd25ac9c665569d060f220b0e1/pywebview-6.1-py3-none-any.whl", hash = "sha256:2b552a340557e76a740b045a25937f72dae751e0985d5f5d9556f697ba622ec5", size = 511255, upload-time = "2025-10-21T18:02:42.377Z" }, -] [[package]] name = "pywin32" @@ -2214,7 +2210,7 @@ requires-dist = [ { name = "pytest-timeout", marker = "extra == 'dev'", specifier = ">=2.4.0" }, { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.5.0" }, { name = "python-dotenv", specifier = ">=1.0.0,<2" }, - { name = "pywebview", specifier = ">=6.1" }, + { name = "pywebview", git = "https://github.com/r0x0r/pywebview?rev=345596c1f4fd59fd26c7aa02d4176c5362bcbabe" }, { name = "pyyaml", specifier = ">=6.0.2" }, { name = "requests", specifier = ">=2.32.3,<3" }, { name = "ruamel-yaml", specifier = "==0.18.14" }, From 658bf29c8d141b898ec9c58c8b5279c41b6fabc9 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 2 Mar 2026 21:52:40 +0100 Subject: [PATCH 03/17] Update Serena icons, adding .ico file --- src/serena/dashboard.py | 2 +- .../resources/dashboard/serena-icon-48.png | Bin 6262 -> 5203 bytes .../resources/dashboard/serena-icon-64.png | Bin 0 -> 5286 bytes src/serena/resources/dashboard/serena.ico | Bin 0 -> 6102 bytes 4 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 src/serena/resources/dashboard/serena-icon-64.png create mode 100644 src/serena/resources/dashboard/serena.ico diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index b2b23df7..f54ebbdb 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -681,7 +681,7 @@ class SerenaDashboardViewer: ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("oraios.serena") dashboard_path = Path(SERENA_DASHBOARD_DIR) - icon_path = str(dashboard_path / "serena-icon-48.png") + icon_path = str(dashboard_path / "serena.ico") # Create hidden to avoid flash; show/restore/minimize in start callback. self.window = webview.create_window( diff --git a/src/serena/resources/dashboard/serena-icon-48.png b/src/serena/resources/dashboard/serena-icon-48.png index acb6e08c671ecdee9546fccde58bdab00b0e35af..6aadd7b689af2b823d654e0916320a02fd2abf82 100644 GIT binary patch literal 5203 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 literal 6262 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 diff --git a/src/serena/resources/dashboard/serena-icon-64.png b/src/serena/resources/dashboard/serena-icon-64.png new file mode 100644 index 0000000000000000000000000000000000000000..28dc7d042e5957bd9106335a0d4790f1abefe524 GIT binary patch literal 5286 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 H?_~cMTAWj= literal 0 HcmV?d00001 From 3880b75ec6b48ae1c24a13a94d1fc9fc05597c90 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 7 Mar 2026 12:48:53 +0100 Subject: [PATCH 04/17] In tray mode, intercept close button to minimise to tray instead --- src/serena/dashboard.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index f54ebbdb..f5675da3 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -665,6 +665,9 @@ class SerenaDashboardViewer: ): self.url = url self.tray = True + """ + whether we use the system tray to allow minimizing to tray and intercepting the close event to hide to tray instead of quitting. + """ self.title = "Serena Dashboard" self.width = width self.height = height @@ -672,6 +675,7 @@ class SerenaDashboardViewer: self.window: webview.Window | None = None self._tray_icon = None + self._quitting = False def run(self) -> None: # Set app id (avoid app being lumped together with other Python-based apps in Windows taskbar) @@ -684,6 +688,7 @@ class SerenaDashboardViewer: icon_path = str(dashboard_path / "serena.ico") # Create hidden to avoid flash; show/restore/minimize in start callback. + # When tray is enabled, confirm_close allows us to intercept the X button. self.window = webview.create_window( self.title, self.url, @@ -693,6 +698,9 @@ class SerenaDashboardViewer: confirm_close=False, ) + if self.tray: + self.window.events.closing += self._on_closing + def _start_callback(): if self.start_minimized: self.window.minimize() @@ -705,6 +713,14 @@ class SerenaDashboardViewer: webview.start(_start_callback, icon=icon_path) + def _on_closing(self) -> bool: + """Intercept window close: hide to tray instead of quitting.""" + if self._quitting: + return True + if self.window: + self.window.hide() + return False # Prevent the window from actually closing + def _run_tray(self) -> None: dashboard_path = Path(SERENA_DASHBOARD_DIR) @@ -721,6 +737,7 @@ class SerenaDashboardViewer: self.window.hide() def quit_app(_icon, _item): + self._quitting = True try: _icon.stop() finally: From 161c8209094e4ad697530a910317a2e225c2c864 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 18 Mar 2026 14:52:47 +0100 Subject: [PATCH 05/17] _start_dashboard_viewer: Open in browser as a fallback (upon exception from pywebview) --- src/serena/agent.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index d19ff241..73202d35 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -11,6 +11,7 @@ from collections.abc import Callable, Sequence from logging import Logger from typing import TYPE_CHECKING, Optional, TypeVar +import webview from sensai.util import logging from sensai.util.logging import LogTime from sensai.util.string import TextBuilder @@ -530,7 +531,12 @@ class SerenaAgent: @staticmethod def _start_dashboard_viewer_process_function(url: str, minimized: bool) -> None: - SerenaDashboardViewer(url, start_minimized=minimized).run() + try: + SerenaDashboardViewer(url, start_minimized=minimized).run() + except webview.errors.WebViewException as e: + # This can happen on Linux if neither GTK or QT are available. Open in browser as a fallback + log.warning(f"Could not open Serena Dashboard viewer; falling back to opening browser. Cause:\n{e}") + SerenaAgent._open_dashboard_in_browser(url) def _start_dashboard_viewer(self, minimized: bool) -> None: url = self.get_dashboard_url() @@ -549,15 +555,19 @@ class SerenaAgent: log.warning("Not opening the Serena web dashboard because no usable display was detected.") return False + self._open_dashboard_in_browser(self._dashboard_url) + return True + + @staticmethod + def _open_dashboard_in_browser(url) -> None: # Use a subprocess to avoid any output from webbrowser.open being written to stdout subprocess.Popen( - [sys.executable, "-c", f"import webbrowser; webbrowser.open({self._dashboard_url!r})"], + [sys.executable, "-c", f"import webbrowser; webbrowser.open({url!r})"], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, # Detach from parent process ) - return True def get_project_root(self) -> str: """ From 1a4e57eec711eb71e00af4aa44a1327236556597 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 28 Mar 2026 11:36:02 +0100 Subject: [PATCH 06/17] SerenaDashboardViewer: Attempt to fix tray on macOS (cannot run in background thread) --- src/serena/dashboard.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 9143bcd5..58d17d5a 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -709,7 +709,7 @@ class SerenaDashboardViewer: self._quitting = False def run(self) -> None: - # Set app id (avoid app being lumped together with other Python-based apps in Windows taskbar) + # set app id (avoid app being lumped together with other Python-based apps in Windows taskbar) if sys.platform == "win32": import ctypes @@ -732,6 +732,9 @@ class SerenaDashboardViewer: if self.tray: self.window.events.closing += self._on_closing + if self.tray: + self._start_tray() + def _start_callback(): if self.start_minimized: self.window.minimize() @@ -739,9 +742,6 @@ class SerenaDashboardViewer: self.window.show() self.window.restore() - if self.tray: - threading.Thread(target=self._run_tray, daemon=True).start() - webview.start(_start_callback, icon=icon_path) def _on_closing(self) -> bool: @@ -752,7 +752,7 @@ class SerenaDashboardViewer: self.window.hide() return False # Prevent the window from actually closing - def _run_tray(self) -> None: + def _start_tray(self) -> None: dashboard_path = Path(SERENA_DASHBOARD_DIR) icon_path = dashboard_path / "serena-icon-48.png" @@ -781,5 +781,19 @@ class SerenaDashboardViewer: Item("Quit", quit_app), ) - self._tray_icon = pystray.Icon("dashboard_viewer", icon_img, self.title, menu) - self._tray_icon.run() + kwargs = {} + if sys.platform == "darwin": + # Passing darwin_nsapplication integrates pystray with the NSApplication + # run loop that webview.start() is about to enter. sharedApplication() + # is idempotent; pywebview will reuse the same singleton. + from AppKit import NSApplication + + kwargs["darwin_nsapplication"] = NSApplication.sharedApplication() + + self._tray_icon = pystray.Icon("dashboard_viewer", icon_img, self.title, menu, **kwargs) + + # On Windows/Linux, run_detached spawns pystray's own internal thread and + # returns immediately. On macOS it hooks into the NSApplication run loop + # that webview.start() is about to enter (run_detached is always called + # before webview.start() on macOS — see run()). + self._tray_icon.run_detached() From 38565950803d46b80193408922cd413b40866ab2 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 28 Mar 2026 11:43:37 +0100 Subject: [PATCH 07/17] SerenaDashboardViewer: Use 64px png icon on non-Windows platforms --- src/serena/dashboard.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 58d17d5a..844b80e3 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -716,7 +716,9 @@ class SerenaDashboardViewer: ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("oraios.serena") dashboard_path = Path(SERENA_DASHBOARD_DIR) - icon_path = str(dashboard_path / "serena.ico") + # .ico is Windows-only; macOS expects a PNG for the window/dock icon. + icon_filename = "serena.ico" if sys.platform == "win32" else "serena-icon-64.png" + icon_path = str(dashboard_path / icon_filename) # Create hidden to avoid flash; show/restore/minimize in start callback. # When tray is enabled, confirm_close allows us to intercept the X button. @@ -755,8 +757,10 @@ class SerenaDashboardViewer: def _start_tray(self) -> None: dashboard_path = Path(SERENA_DASHBOARD_DIR) - icon_path = dashboard_path / "serena-icon-48.png" - icon_img = Image.open(icon_path) + # macOS menu bar icons are displayed at 16pt; 32px covers Retina (@2x). + # Windows/Linux tray icons are larger, so 48px is the better fit there. + icon_filename = "serena-icon-32.png" if sys.platform == "darwin" else "serena-icon-48.png" + icon_img = Image.open(dashboard_path / icon_filename) def show(_icon, _item): if self.window: From 341b00577b422a1594aa7a3c0facf8a638f12438 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 28 Mar 2026 12:33:38 +0100 Subject: [PATCH 08/17] SerenaDashboardViewer: macOS: Try to hide the main window icon from the dock when minimised to tray --- src/serena/dashboard.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 844b80e3..b52aa809 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -736,6 +736,7 @@ class SerenaDashboardViewer: if self.tray: self._start_tray() + self._set_dock_icon_visible(not self.start_minimized) def _start_callback(): if self.start_minimized: @@ -752,7 +753,36 @@ class SerenaDashboardViewer: return True if self.window: self.window.hide() - return False # Prevent the window from actually closing + if sys.platform == "darwin": + # setActivationPolicy_ must run on the main thread; _on_closing + # may be called from a pywebview background thread. + from PyObjCTools.AppHelper import callAfter + + callAfter(self._set_dock_icon_visible, False) + return False # prevent the window from actually closing + + def _set_dock_icon_visible(self, visible: bool) -> None: + """macOS-only; toggles the dock icon. + + Sets the NSApplication activation policy to Regular (shows in dock) or + Accessory (hides from dock). No-op on other platforms. + """ + if sys.platform != "darwin": + return + from AppKit import ( + NSApplication, + NSApplicationActivationPolicyAccessory, + NSApplicationActivationPolicyRegular, + ) + + ns_app = NSApplication.sharedApplication() + policy = NSApplicationActivationPolicyRegular if visible else NSApplicationActivationPolicyAccessory + ns_app.setActivationPolicy_(policy) + if visible: + # setActivationPolicy_ alone is not enough at runtime; unhide_ fires + # the full applicationWillUnhide:/applicationDidUnhide: cycle, which + # is what actually restores the dock icon and brings windows forward. + ns_app.unhide_(None) def _start_tray(self) -> None: dashboard_path = Path(SERENA_DASHBOARD_DIR) @@ -764,12 +794,14 @@ class SerenaDashboardViewer: def show(_icon, _item): if self.window: + self._set_dock_icon_visible(True) self.window.show() self.window.restore() def hide(_icon, _item): if self.window: self.window.hide() + self._set_dock_icon_visible(False) def quit_app(_icon, _item): self._quitting = True From fa8cef0fe88d5d5741e1865004094cb7e3d5516c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 28 Mar 2026 12:52:07 +0100 Subject: [PATCH 09/17] SerenaDashboardViewer: Fix typing issues --- src/serena/dashboard.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index b52aa809..357764a9 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -12,6 +12,7 @@ from flask import Flask, Response, redirect, request, send_from_directory from PIL import Image from pydantic import BaseModel from pystray import MenuItem as Item +from pystray._base import Icon as TrayIcon from sensai.util import logging from serena.analytics import ToolUsageStats @@ -704,8 +705,8 @@ class SerenaDashboardViewer: self.height = height self.start_minimized = start_minimized - self.window: webview.Window | None = None - self._tray_icon = None + self.window: webview.Window + self._tray_icon: TrayIcon self._quitting = False def run(self) -> None: @@ -722,7 +723,7 @@ class SerenaDashboardViewer: # Create hidden to avoid flash; show/restore/minimize in start callback. # When tray is enabled, confirm_close allows us to intercept the X button. - self.window = webview.create_window( + window = webview.create_window( self.title, self.url, width=self.width, @@ -730,6 +731,8 @@ class SerenaDashboardViewer: hidden=self.start_minimized, confirm_close=False, ) + assert window is not None + self.window = window if self.tray: self.window.events.closing += self._on_closing @@ -738,7 +741,7 @@ class SerenaDashboardViewer: self._start_tray() self._set_dock_icon_visible(not self.start_minimized) - def _start_callback(): + def _start_callback() -> None: if self.start_minimized: self.window.minimize() else: @@ -792,18 +795,18 @@ class SerenaDashboardViewer: icon_filename = "serena-icon-32.png" if sys.platform == "darwin" else "serena-icon-48.png" icon_img = Image.open(dashboard_path / icon_filename) - def show(_icon, _item): + def show(_icon: TrayIcon, _item: Item) -> None: if self.window: self._set_dock_icon_visible(True) self.window.show() self.window.restore() - def hide(_icon, _item): + def hide(_icon: TrayIcon, _item: Item) -> None: if self.window: self.window.hide() self._set_dock_icon_visible(False) - def quit_app(_icon, _item): + def quit_app(_icon: TrayIcon, _item: Item) -> None: self._quitting = True try: _icon.stop() @@ -817,7 +820,7 @@ class SerenaDashboardViewer: Item("Quit", quit_app), ) - kwargs = {} + kwargs: dict[str, Any] = {} if sys.platform == "darwin": # Passing darwin_nsapplication integrates pystray with the NSApplication # run loop that webview.start() is about to enter. sharedApplication() From 07dbf5ed22724cba2b20ba30d0bf5ce1352d3553 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 28 Mar 2026 13:07:51 +0100 Subject: [PATCH 10/17] Implement proper fallback behaviour for dashboard viewer Open browser if the viewer was supposed to be shown directly --- src/serena/agent.py | 31 ++++++++++++++++++++++++------- src/serena/dashboard.py | 9 +++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index b495fa59..76d42224 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -522,22 +522,39 @@ class SerenaAgent: @staticmethod def _start_dashboard_viewer_process_function(url: str, minimized: bool) -> None: + """ + Main function of the subprocess for starting the dashboard viewer + """ try: SerenaDashboardViewer(url, start_minimized=minimized).run() except webview.errors.WebViewException as e: - # This can happen on Linux if neither GTK or QT are available. Open in browser as a fallback - log.warning(f"Could not open Serena Dashboard viewer; falling back to opening browser. Cause:\n{e}") - SerenaAgent._open_dashboard_in_browser(url) + log.warning(f"Could not open Serena Dashboard viewer. Cause:\n{e}") + # Fall back to opening the browser window if the window was supposed to be shown directly + if not minimized: + SerenaAgent._open_dashboard_in_browser(url) def _start_dashboard_viewer(self, minimized: bool) -> None: + """ + Starts the dashboard viewer (in a separate process) or, if the current platform does not support it, + opens the dashboard in the default web browser. + + :param minimized: whether the dashboard viewer should be started minimized (if supported on the current platform). + If the viewer is not supported on the current platform, then this controls whether to open the browser window. + """ url = self.get_dashboard_url() assert url is not None - process = multiprocessing.Process(target=self._start_dashboard_viewer_process_function, args=(url, minimized), daemon=True) - process.start() + if SerenaDashboardViewer.is_current_platform_supported(): + process = multiprocessing.Process(target=self._start_dashboard_viewer_process_function, args=(url, minimized), daemon=True) + process.start() + else: + if not minimized: + self._open_dashboard_in_browser(url) def open_dashboard(self) -> bool: """ - Opens the viewer for the Serena dashboard + Opens the Serena dashboard (for on-demand usage as triggered by the user, e.g. via a tool) + + :return: True if the dashboard was opened, False if it could not be opened """ if self._dashboard_url is None: raise Exception("Dashboard is not running.") @@ -550,7 +567,7 @@ class SerenaAgent: return True @staticmethod - def _open_dashboard_in_browser(url) -> None: + def _open_dashboard_in_browser(url: str) -> None: # Use a subprocess to avoid any output from webbrowser.open being written to stdout subprocess.Popen( [sys.executable, "-c", f"import webbrowser; webbrowser.open({url!r})"], diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 357764a9..65cbb2d3 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -709,6 +709,15 @@ class SerenaDashboardViewer: self._tray_icon: TrayIcon self._quitting = False + @staticmethod + def is_current_platform_supported() -> bool: + """ + :return: whether the current platform supports the dashboard viewer + """ + # We fully support the dashboard viewer (with system tray) only on Windows and macOS. + # Linux support is problematic; see https://github.com/oraios/serena/pull/1117#issuecomment-4128753943 + return sys.platform in ("win32", "darwin") + def run(self) -> None: # set app id (avoid app being lumped together with other Python-based apps in Windows taskbar) if sys.platform == "win32": From af1de59d2b852d98e38cebee84fd17e9c257140d Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 29 Mar 2026 20:35:50 +0200 Subject: [PATCH 11/17] Adapt Dashboard documention in light of pywebview changes --- docs/02-usage/060_dashboard.md | 78 ++++++++++++++----- .../resources/serena_config.template.yml | 26 +++---- 2 files changed, 71 insertions(+), 33 deletions(-) diff --git a/docs/02-usage/060_dashboard.md b/docs/02-usage/060_dashboard.md index 9bb6406e..3e4e3a5a 100644 --- a/docs/02-usage/060_dashboard.md +++ b/docs/02-usage/060_dashboard.md @@ -2,38 +2,80 @@ Serena comes with built-in tools for monitoring and managing the current session: -* the **web-based dashboard** (enabled by default) +* the **Serena Dashboard** (enabled by default) The dashboard provides detailed information on your Serena session, the current configuration and provides access to logs. Some settings (e.g. the current set of active programming languages) can also be directly modified through the dashboard. The dashboard is supported on all platforms. - - By default, it will be accessible at `http://localhost:24282/dashboard/index.html`, - but a higher port may be used if the default port is unavailable/multiple instances are running. - **We recommend always enabling the dashboard**. If you don't want the browser to open automatically, - you can disable it while still keeping the dashboard running in the background (see below). - -* the **GUI tool** (disabled by default) +* the **GUI Log Viewer** (disabled by default) - The GUI tool is a native application window which displays logs. - It furthermore allows you to shut down the agent and to access the dashboard's URL (if it is running). + The GUI tool is a legacy native application which displays Serena's live logs. This is mainly supported on Windows, but it may also work on Linux; macOS is unsupported. Both can be configured in Serena's [configuration](050_configuration) file (`serena_config.yml`). -If enabled, they will automatically be opened as soon as the Serena agent/MCP server is started. -For the dashboard, this can be disabled if desired (see below). -## Disabling Automatic Browser Opening +## The Serena Dashboard -If you prefer not to have the dashboard open automatically (e.g., to avoid focus stealing), you can disable it -by setting `web_dashboard_open_on_launch: False` in your `serena_config.yml` or by passing `--open-web-dashboard False` -to `start-mcp-server` CLI command. +The dashboard is a web application, which is opened via a native application wrapper on Windows and macOS, +where it will appear in the system tray. **Click on the tray icon** to open the application window. -When automatic opening is disabled, you can still access the dashboard by: +On Linux, native app support is difficult to provide, so the dashboard will be opened in your default web browser instead. + +By default, the dashboard can be accessed at `http://localhost:24282/dashboard/index.html`, +but a higher port may be used if the default port is unavailable/multiple instances are running. + +### Features + +The dashboard provides ... + * a detailed overview of + * the current Serena status and configuration (e.g. active tools, active programming languages, enabled modes and contexts, etc.) + * ongoing and past tool calls (and statistics on tool usage) + * access to Serena's live logs + * the ability to modify settings (e.g. the set of active programming languages) on the fly + * the ability to edit + * configuration files (global `serena_config.yml` and project-specific `project.yml`) + * memories of the current project + * the ability to shut down the Serena MCP server. + +### Configuring the Dashboard + +You can configure settings for the dashboard in the [global configuration file](global-config). + +In particular, you can configure + * whether the dashboard is enabled at all. + We highly recommend keeping it enabled, as it provides valuable insights into Serena's operation and allows you to adjust configuration conveniently. + * whether the dashboard window is opened automatically when Serena is started (see below). + * network settings (for cases where you might want to access the Dashboard non-locally). + +#### Dashboard Opening Behaviour + +When Serena is started, the Dashboard window is opened by default in order to make users aware of its existence. + +If you prefer not to have this happen (e.g., to avoid focus stealing), you can disable prevent it from opening +by setting `web_dashboard_open_on_launch: False` in the [global configuration file](global-config) or by passing `--open-web-dashboard False` +to the `start-mcp-server` CLI command. + +On Windows and macOS, you can conveniently open the dashboard at any time by clicking on the tray icon, so automatic +opening is not a requirement to be able to access the dashboard on these platforms. + +On Linux, access to the dashboard is more cumbersome without native app support. +Yet you may still access it by * asking the LLM to "open the Serena dashboard", which will open the dashboard in your default browser (the tool `open_dashboard` is enabled for this purpose, provided that the dashboard is active, not opened by default and the GUI tool, which can provide the URL, is not enabled) -* navigating directly to the URL (see above) +* navigating directly to the URL (see above). + +## The GUI Log Viewer + +The Serena GUI Log Viewer is a legacy application which provides access to Serena's live logs. + +Via its menu, it furthermore allows you to + * shut down the agent + * access the dashboard's URL (if it is running). + +The tool is mainly supported on Windows, but it may work on some Linux systems as well (depending on your desktop environment). + +To enable the tool, set `gui_log_window` to `true` in Serena's [global configuration file](global-config). \ No newline at end of file diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index 7ec556db..a823c743 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -29,30 +29,26 @@ line_ending: native # we have to live with this limitation for now. gui_log_window: False -# whether to open the Serena web dashboard (which will be accessible through your web browser) that -# provides access to Serena's configuration and state as well as the current session logs. -# The web dashboard is supported on all platforms. -# We strongly recommend to always enable this option, since the dashboard provides important information -# about the current state of Serena, including the configuration, the logs and tool usage statistics. -# If you don't want the browser window to pop up automatically, set the `web_dashboard_open_on_launch` to False -# (either here or through the corresponding flag in the `start-mcp-server` CLI command). -# You can then open the dashboard by asking your agent to do so (e.g., by saying "open the dashboard"), Serena provides -# a tool for this. +# whether to start the Serena Dashboard, which provides detailed information on your Serena session, +# the current configuration and furthermore allows some settings to be conveniently modified on the fly. +# We strongly recommend to always enable this option! +# If you want to prevent the Dashboard window from being opened on launch, +# set `web_dashboard_open_on_launch` to false (see below). # Further information: https://oraios.github.io/serena/02-usage/060_dashboard.html web_dashboard: True -# the address the web dashboard will listen on (bind address). -web_dashboard_listen_address: 127.0.0.1 - -# whether to open a browser window with the web dashboard when Serena starts (provided that web_dashboard -# is enabled). See also the web_dashboard option. -# If set to false, you can still open the dashboard manually by +# whether to open the Dashboard window/browser tab when Serena starts (provided that web_dashboard is enabled). +# If set to false, you can still open the dashboard manually by clicking on the Serena icon in your system +# tray on Windows and macOS. On Linux, there is no system tray support, so you can only open the dashboard by # a) telling the LLM to "open the dashboard" (provided that the open_dashboard tool is enabled) or by # b) manually navigating to http://localhost:24282/dashboard/ in your web browser (actual port # may be higher if you have multiple instances running; try ports 24283, 24284, etc.) # See also: https://oraios.github.io/serena/02-usage/060_dashboard.html web_dashboard_open_on_launch: True +# the address the web dashboard will listen on (bind address). +web_dashboard_listen_address: 127.0.0.1 + # address where JetBrains plugin servers are running (only relevant when using the JetBrains language backend) jetbrains_plugin_server_address: 127.0.0.1 From 01e9582cacd26ef82d5dba2695f6f8f4d86fbab7 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 31 Mar 2026 11:26:01 +0200 Subject: [PATCH 12/17] Nix flake: add proxy-tools (needed to build pywebview) --- flake.nix | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flake.nix b/flake.nix index e8d9ccfa..2454b5fc 100644 --- a/flake.nix +++ b/flake.nix @@ -57,6 +57,11 @@ final.setuptools ]; }); + proxy-tools = prev.proxy-tools.overrideAttrs (old: { + nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ + final.setuptools + ]; + }); }; python = pkgs.python311; From e53e487cae26b296797832201c4d98f216fdf2bc Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 31 Mar 2026 11:27:11 +0200 Subject: [PATCH 13/17] Pinned pystray exactly --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ca115350..fa434238 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "anthropic==0.59.0", "beautifulsoup4==4.14.2", "pywebview", - "pystray>=0.19.5", + "pystray==0.19.5", # Transitive deps pinned for security (dependabot alerts). # Exact pins because uvx installs from git, ignoring the lock file. "urllib3==2.6.3", diff --git a/uv.lock b/uv.lock index d7684133..e4ec874a 100644 --- a/uv.lock +++ b/uv.lock @@ -3009,7 +3009,7 @@ requires-dist = [ { name = "pyinstrument", marker = "extra == 'dev'", specifier = "==5.1.1" }, { name = "pyjwt", marker = "extra == 'dev'", specifier = "==2.10.1" }, { name = "pyright", specifier = "==1.1.403" }, - { name = "pystray", specifier = ">=0.19.5" }, + { name = "pystray", specifier = "==0.19.5" }, { name = "pytest", marker = "extra == 'dev'", specifier = "==8.4.1" }, { name = "pytest-timeout", marker = "extra == 'dev'", specifier = "==2.4.0" }, { name = "pytest-xdist", marker = "extra == 'dev'", specifier = "==3.8.0" }, From 568f9a06af2373f86afb6c092f67f0f8ecd66145 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 31 Mar 2026 14:29:08 +0200 Subject: [PATCH 14/17] Nix flake: added setuptools as build input for pywebview --- flake.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/flake.nix b/flake.nix index 2454b5fc..e172a186 100644 --- a/flake.nix +++ b/flake.nix @@ -62,6 +62,12 @@ final.setuptools ]; }); + pywebview = prev.pywebview.overrideAttrs (old: { + nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ + final.setuptools + final.setuptools-scm + ]; + }); }; python = pkgs.python311; From 92301c058becb9ed214e25bc828a688f23576937 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 31 Mar 2026 15:27:42 +0200 Subject: [PATCH 15/17] Import pystray locally, because import crashes if there is no display --- src/serena/dashboard.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index dfb5a165..640bee51 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -6,13 +6,10 @@ from collections.abc import Callable from pathlib import Path from typing import TYPE_CHECKING, Any, Self -import pystray import webview from flask import Flask, Response, redirect, request, send_from_directory from PIL import Image from pydantic import BaseModel -from pystray import MenuItem as Item -from pystray._base import Icon as TrayIcon from sensai.util import logging from serena.analytics import ToolUsageStats @@ -708,7 +705,7 @@ class SerenaDashboardViewer: self.start_minimized = start_minimized self.window: webview.Window - self._tray_icon: TrayIcon + self._tray_icon: Any self._quitting = False @staticmethod @@ -799,6 +796,11 @@ class SerenaDashboardViewer: ns_app.unhide_(None) def _start_tray(self) -> None: + # import pystray locally, because the import fails when there is no display! + import pystray + from pystray import MenuItem as Item + from pystray._base import Icon as TrayIcon + dashboard_path = Path(SERENA_DASHBOARD_DIR) # macOS menu bar icons are displayed at 16pt; 32px covers Retina (@2x). From 0bab0125d72f167c4ae853f166800504bfe36896 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 31 Mar 2026 15:30:11 +0200 Subject: [PATCH 16/17] _start_dashboard_viewer: Do nothing if no display detected, improve logging --- src/serena/agent.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/serena/agent.py b/src/serena/agent.py index 76d42224..91431192 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -541,12 +541,17 @@ class SerenaAgent: :param minimized: whether the dashboard viewer should be started minimized (if supported on the current platform). If the viewer is not supported on the current platform, then this controls whether to open the browser window. """ + if not system_has_usable_display(): + log.info("Not starting the Serena dashboard viewer because no usable display was detected.") + return + url = self.get_dashboard_url() assert url is not None if SerenaDashboardViewer.is_current_platform_supported(): process = multiprocessing.Process(target=self._start_dashboard_viewer_process_function, args=(url, minimized), daemon=True) process.start() else: + log.info("Not starting Serena dashboard viewer because the current platform does not support it; using browser-based fallback") if not minimized: self._open_dashboard_in_browser(url) From 0cf16d1eefac3c6b1b434111b00e372e352438f5 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 31 Mar 2026 15:36:04 +0200 Subject: [PATCH 17/17] Circumvent mypy unreachable code error --- src/serena/dashboard.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 640bee51..34927596 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -778,22 +778,21 @@ class SerenaDashboardViewer: Sets the NSApplication activation policy to Regular (shows in dock) or Accessory (hides from dock). No-op on other platforms. """ - if sys.platform != "darwin": - return - from AppKit import ( - NSApplication, - NSApplicationActivationPolicyAccessory, - NSApplicationActivationPolicyRegular, - ) + if sys.platform == "darwin": + from AppKit import ( + NSApplication, + NSApplicationActivationPolicyAccessory, + NSApplicationActivationPolicyRegular, + ) - ns_app = NSApplication.sharedApplication() - policy = NSApplicationActivationPolicyRegular if visible else NSApplicationActivationPolicyAccessory - ns_app.setActivationPolicy_(policy) - if visible: - # setActivationPolicy_ alone is not enough at runtime; unhide_ fires - # the full applicationWillUnhide:/applicationDidUnhide: cycle, which - # is what actually restores the dock icon and brings windows forward. - ns_app.unhide_(None) + ns_app = NSApplication.sharedApplication() + policy = NSApplicationActivationPolicyRegular if visible else NSApplicationActivationPolicyAccessory + ns_app.setActivationPolicy_(policy) + if visible: + # setActivationPolicy_ alone is not enough at runtime; unhide_ fires + # the full applicationWillUnhide:/applicationDidUnhide: cycle, which + # is what actually restores the dock icon and brings windows forward. + ns_app.unhide_(None) def _start_tray(self) -> None: # import pystray locally, because the import fails when there is no display!