From 065df5ea310b6dc59095e0d68482e712e38d9592 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 21 May 2026 15:17:45 +0200 Subject: [PATCH 1/9] Introduce events for agent config/status updates decoupling dashboard config computation from the task queue Resolves #1064 --- CHANGELOG.md | 4 ++++ src/serena/agent.py | 45 +++++++++++++++++++++++++++++------ src/serena/dashboard.py | 18 ++++++++++---- src/serena/task_executor.py | 15 +++++++++++- test/serena/test_dashboard.py | 4 ++++ 5 files changed, 74 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5c99726..9c4b584a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* Dashboard: + - Decouple configuration computation from the agent's task queue by introducing events for agent config/status updates. + This allows the dashboard to display the configuration while the project provided at startup is still initialising. #1064 + # v1.5.1 (2026-05-18) * General: diff --git a/src/serena/agent.py b/src/serena/agent.py index c8e0d1e5..7b174f87 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -544,6 +544,7 @@ class SerenaAgent: self._project_prompt_status = ProjectPromptProvisionStatus() self._session_mode_selection_definition = modes self.version = serena_version() + self._config_changed_callbacks: list[Callable[[], None]] = [] # obtain serena configuration using the decoupled factory function self.serena_config = serena_config or SerenaConfig.from_config_file() @@ -633,7 +634,7 @@ class SerenaAgent: # create executor for starting the language server and running tools in another thread # This executor is used to achieve linear task execution - self._task_executor = TaskExecutor("SerenaAgentTaskExecutor") + self._task_executor = TaskExecutor("SerenaAgentTaskExecutor", self._task_completion_callback) # Initialize the prompt factory self.prompt_factory = SerenaPromptFactory() @@ -659,13 +660,22 @@ class SerenaAgent: self._active_tools: AvailableTools self._update_active_tools() - # start the dashboard (web frontend), registering its log handler - # should be the last thing to happen in the initialization since the dashboard - # may access various parts of the agent + # create the dashboard backend (if enabled), which will register callback. + dashboard_api: SerenaDashboardAPI | None = None if self.serena_config.web_dashboard: - self._dashboard_thread, port = SerenaDashboardAPI( - get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats - ).run_in_thread(host=self.serena_config.web_dashboard_listen_address) + dashboard_api = SerenaDashboardAPI(get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats) + + # propagate the initial config/state to listeners, particularly to the dashboard API. + # Note: The only ongoing task can be the LS initialisation, which does not change the config + # This is executed before starting the dashboard thread to ensure that the dashboard + # has the correct initial state before can requests come in. + self._on_config_changed() + + # start the dashboard backend thread and the dashboard frontend manager (if enabled). + # This should be the last thing to happen in the initialization since the dashboard + # may access various parts of the agent + if dashboard_api: + dashboard_thread, port = dashboard_api.run_in_thread(host=self.serena_config.web_dashboard_listen_address) self._dashboard_manager = DashboardManager( port, self.serena_config.web_dashboard_listen_address, @@ -1072,6 +1082,27 @@ class SerenaAgent: """ return self._task_executor.execute_task(task, name=name, logged=logged, timeout=timeout) + def _task_completion_callback(self) -> None: + """ + Called after every task completion. Tasks potentially change the agent's state/configuration. + """ + self._on_config_changed() + + def _on_config_changed(self) -> None: + for callback in self._config_changed_callbacks: + try: + callback() + except Exception as e: + log.error(f"Error in config changed callback {callback}: {e}", exc_info=e) + + def register_config_changed_callback(self, callback: Callable[[], None]) -> None: + """ + Registers a callback to be called when the agent configuration has (potentially) changed. + + :param callback: the callback function to register + """ + self._config_changed_callbacks.append(callback) + def is_using_language_server(self) -> bool: """ :return: whether this agent uses language server-based code analysis diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 66c93827..234245d3 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -207,7 +207,12 @@ class SerenaDashboardAPI: self._news_ready = threading.Event() self._setup_routes() self._read_news = ReadNews.load() - # Fetch remote news in background on startup (non-blocking) + + # register callback for config changes + self._current_config_overview: dict[str, Any] | None = None + self._agent.register_config_changed_callback(self._on_agent_config_changed) + + # fetch remote news in background on startup (non-blocking) threading.Thread(target=self._fetch_news, daemon=True).start() @property @@ -272,8 +277,10 @@ class SerenaDashboardAPI: @self._app.route("/get_config_overview", methods=["GET"]) def get_config_overview() -> dict[str, Any]: - result = self._agent.execute_task(self._get_config_overview, logged=False) - return result.model_dump() + result = self._current_config_overview + if result is None: + raise ValueError("Config overview not yet available") + return result @self._app.route("/shutdown", methods=["PUT"]) def shutdown() -> dict[str, str]: @@ -468,7 +475,7 @@ class SerenaDashboardAPI: if self._tool_usage_stats is not None: self._tool_usage_stats.clear() - def _get_config_overview(self) -> ResponseConfigOverview: + def _compute_config_overview(self) -> ResponseConfigOverview: from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode from serena.tools.tools_base import Tool @@ -596,6 +603,9 @@ class SerenaDashboardAPI: serena_version=self._agent.version, ) + def _on_agent_config_changed(self) -> None: + self._current_config_overview = self._compute_config_overview().model_dump() + def _get_available_languages(self) -> ResponseAvailableLanguages: from solidlsp.ls_config import Language diff --git a/src/serena/task_executor.py b/src/serena/task_executor.py index 5e624296..1c72f068 100644 --- a/src/serena/task_executor.py +++ b/src/serena/task_executor.py @@ -16,7 +16,12 @@ T = TypeVar("T") class TaskExecutor: - def __init__(self, name: str): + def __init__(self, name: str, task_completion_callback: Callable[[], None] | None = None): + """ + :param name: the name of the task executor, used for logging purposes + :param task_completion_callback: an optional callback function that will be called after each task completion + (regardless of success, failure, or cancellation) + """ self._task_executor_lock = threading.Lock() self._task_executor_queue: list[TaskExecutor.Task] = [] self._task_executor_thread = Thread(target=self._process_task_queue, name=name, daemon=True) @@ -24,6 +29,7 @@ class TaskExecutor: self._task_executor_task_index = 1 self._task_executor_current_task: TaskExecutor.Task | None = None self._task_executor_last_executed_task_info: TaskExecutor.TaskInfo | None = None + self._task_completion_callback = task_completion_callback class Task(ToStringMixin, Generic[T]): def __init__(self, function: Callable[[], T], name: str, logged: bool = True, timeout: float | None = None): @@ -130,6 +136,13 @@ class TaskExecutor: if task.logged: self._task_executor_last_executed_task_info = self.TaskInfo.from_task(task, is_running=False) + # call the task completion callback if provided + if self._task_completion_callback is not None: + try: + self._task_completion_callback() + except Exception as e: + log.error(f"Error in task completion callback after executing {task.name}: {e}", exc_info=e) + @dataclass class TaskInfo: name: str diff --git a/test/serena/test_dashboard.py b/test/serena/test_dashboard.py index 37e68e01..a4355e87 100644 --- a/test/serena/test_dashboard.py +++ b/test/serena/test_dashboard.py @@ -1,3 +1,4 @@ +from collections.abc import Callable from types import SimpleNamespace from serena.dashboard import SerenaDashboardAPI @@ -16,6 +17,9 @@ class _DummyAgent: def __init__(self, project: SimpleNamespace | None) -> None: self._project = project + def register_config_changed_callback(self, callback: Callable[[], None]) -> None: + pass + def execute_task(self, func, *, logged: bool | None = None, name: str | None = None): del logged, name return func() From 24c16b6fecc1ff38d341e2267c99b9c7692c405c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 21 May 2026 17:22:38 +0200 Subject: [PATCH 2/9] Fix empty executions queue displaying as 'Loading ...' --- CHANGELOG.md | 1 + src/serena/resources/dashboard/dashboard.css | 5 +++++ src/serena/resources/dashboard/dashboard.js | 1 + 3 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c4b584a..e56d4f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Status of the `main` branch. Changes prior to the next official version change w * Dashboard: - Decouple configuration computation from the agent's task queue by introducing events for agent config/status updates. This allows the dashboard to display the configuration while the project provided at startup is still initialising. #1064 + - Fix empty executions queue displaying "Loading..." # v1.5.1 (2026-05-18) diff --git a/src/serena/resources/dashboard/dashboard.css b/src/serena/resources/dashboard/dashboard.css index ad658a97..fb2d70da 100644 --- a/src/serena/resources/dashboard/dashboard.css +++ b/src/serena/resources/dashboard/dashboard.css @@ -1012,6 +1012,11 @@ code, pre, kbd, samp, font-style: italic; } +.loaded { + text-align: center; + color: var(--text-muted); +} + .error-message { color: var(--log-error); text-align: center; diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index afa71865..43987b58 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -944,6 +944,7 @@ class Dashboard { displayActiveExecutionsQueue(executions) { if (!executions || executions.length === 0) { + this.$activeExecutionQueueDisplay.html('
No executions queued.
'); return; } From d5033c404dc565fb7e0f9edffb98745610df52d7 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 28 May 2026 14:31:41 +0200 Subject: [PATCH 3/9] Make dashboard trusted hosts configurable --- CHANGELOG.md | 6 +++--- src/serena/agent.py | 1 + src/serena/config/serena_config.py | 1 + src/serena/dashboard.py | 6 +++--- src/serena/resources/serena_config.template.yml | 8 ++++++++ 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2872553c..7230a56f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,9 @@ Status of the `main` branch. Changes prior to the next official version change w and `workspace/configuration` pulls. Enables Yarn PnP setups with `typescript.tsdk` pointing at the Yarn-generated SDK. -* Dashboard: - - Fix: Host validation required a local host regardless of the listen address (regression introduced in v1.5.2), - preventing remote connections +* Dashboard: + - Make list of trusted hosts configurable, fixing host validation introduced in v1.5.2 allowing only + default local hostnames, effectively preventing remote connections # v1.5.3 (2026-05-26) diff --git a/src/serena/agent.py b/src/serena/agent.py index ce4c1191..9542d37f 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -669,6 +669,7 @@ class SerenaAgent: agent=self, tool_usage_stats=self._tool_usage_stats, host=self.serena_config.web_dashboard_listen_address, + trusted_hosts=self.serena_config.web_dashboard_trusted_hosts, ).run_in_thread() self._dashboard_manager = DashboardManager( port, diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index ff63f523..7207a9b1 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -720,6 +720,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): web_dashboard_open_on_launch: bool = True web_dashboard_interface: str | None = None web_dashboard_listen_address: str = "127.0.0.1" + web_dashboard_trusted_hosts: list[str] = field(default_factory=lambda: ["127.0.0.1", "localhost"]) jetbrains_plugin_server_address: str = "127.0.0.1" tool_timeout: float = DEFAULT_TOOL_TIMEOUT diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index f7c5c3cc..8b15166b 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -198,15 +198,15 @@ class SerenaDashboardAPI: agent: "SerenaAgent", tool_usage_stats: ToolUsageStats | None = None, host: str = "127.0.0.1", + trusted_hosts: list[str] | None = None, ) -> None: self._memory_log_handler = memory_log_handler self._tool_names = tool_names self._agent = agent self._host = host self._app = Flask(__name__) - local_hosts = ["127.0.0.1", "localhost"] - if self._host in local_hosts: - self._app.config["TRUSTED_HOSTS"] = local_hosts + if trusted_hosts: + self._app.config["TRUSTED_HOSTS"] = trusted_hosts self._tool_usage_stats = tool_usage_stats self._loaded_news: dict[str, str] = {} self._news_ready = threading.Event() diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index a24ed313..8664fea3 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -67,6 +67,14 @@ web_dashboard_interface: # the address the web dashboard will listen on (bind address). web_dashboard_listen_address: 127.0.0.1 +# trusted hosts used to access the web dashboard. +# By default, only allow access via local addresses for security reasons. +# If you want to allow access from remote machines, add the hostname used to access the dashboard to this list. +# If the list is empty/undefined, all hosts are trusted. +web_dashboard_trusted_hosts: + - 127.0.0.1 + - localhost + # address where JetBrains plugin servers are running (only relevant when using the JetBrains language backend) jetbrains_plugin_server_address: 127.0.0.1 From 7b30f9328efe7f7543209d4204907dd094d1578c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 1 Jun 2026 12:46:44 +0200 Subject: [PATCH 4/9] SvelteLanguageServer: Fix diagnostics requests for TypeScript/JavaScript files incorrectly being processed by the Svelte LS instead of the TypeScript LS --- CHANGELOG.md | 2 ++ .../language_servers/svelte_language_server.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7230a56f..071cbcd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ Status of the `main` branch. Changes prior to the next official version change w The dict is forwarded to vtsls via `initializationOptions`, `workspace/didChangeConfiguration`, and `workspace/configuration` pulls. Enables Yarn PnP setups with `typescript.tsdk` pointing at the Yarn-generated SDK. + - `SvelteLanguageServer`: Fix diagnostics requests for TypeScript/JavaScript files incorrectly being + processed by the Svelte LS instead of the TypeScript LS. * Dashboard: - Make list of trusted hosts configurable, fixing host validation introduced in v1.5.2 allowing only diff --git a/src/solidlsp/language_servers/svelte_language_server.py b/src/solidlsp/language_servers/svelte_language_server.py index 634d4dbb..c633d10f 100644 --- a/src/solidlsp/language_servers/svelte_language_server.py +++ b/src/solidlsp/language_servers/svelte_language_server.py @@ -647,6 +647,19 @@ class SvelteLanguageServer(SolidLanguageServer): return self._ts_server.request_definition(relative_file_path, line, column) return super().request_definition(relative_file_path, line, column) + @override + def request_text_document_diagnostics( + self, + relative_file_path: str, + start_line: int = 0, + end_line: int = -1, + min_severity: int = 4, + ) -> list[ls_types.Diagnostic]: + if _is_ts_file(relative_file_path) and self._ts_server is not None: + with self._ts_server.open_file(relative_file_path): + return self._ts_server.request_text_document_diagnostics(relative_file_path, start_line, end_line, min_severity) + return super().request_text_document_diagnostics(relative_file_path, start_line, end_line, min_severity) + @override def _get_language_id_for_file(self, relative_file_path: str) -> str: ext = os.path.splitext(relative_file_path)[1].lower() From 7b9fc9c5b718e1752f44460557359f19266d0d24 Mon Sep 17 00:00:00 2001 From: Felix Rath Date: Sat, 30 May 2026 16:51:44 +0200 Subject: [PATCH 5/9] fix(nix): Add Linux-support for the tray manager in AppIndicator trays Assisted-by: gpt-5.5 --- CHANGELOG.md | 1 + docs/02-usage/060_dashboard.md | 4 +- flake.nix | 148 ++++++++++++++---- .../resources/serena_config.template.yml | 2 + 4 files changed, 119 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29f774dc..0ce28a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Status of the `main` branch. Changes prior to the next official version change w - Decouple configuration computation from the agent's task queue by introducing events for agent config/status updates. This allows the dashboard to display the configuration while the project provided at startup is still initialising. #1064 - Fix empty executions queue displaying "Loading..." + - Tray manager: Add NixOS-support for AppIndicator-based trays (e.g., most Wayland-trays) to the package in flake.nix. # v1.5.3 (2026-05-26) diff --git a/docs/02-usage/060_dashboard.md b/docs/02-usage/060_dashboard.md index ae11fd4d..50a1dc16 100644 --- a/docs/02-usage/060_dashboard.md +++ b/docs/02-usage/060_dashboard.md @@ -25,7 +25,7 @@ The dashboard is a web application, which is opened in one of three ways: * via a tray manager that aggregates multiple instances (on supported platforms) * via your default web browser (supported on all platforms) -Configure your preferred interface via the setting `web_dashoard_interface` in [Serena's global configuration file](global-config) (see below). +Configure your preferred interface via the setting `web_dashboard_interface` in [Serena's global configuration file](global-config) (see below). 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. @@ -82,4 +82,4 @@ Via its menu, it furthermore allows you to 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 +To enable the tool, set `gui_log_window` to `true` in Serena's [global configuration file](global-config). diff --git a/flake.nix b/flake.nix index 9550e4ff..3cdd06f7 100644 --- a/flake.nix +++ b/flake.nix @@ -50,27 +50,79 @@ sourcePreference = "wheel"; # or sourcePreference = "sdist"; }; - pyprojectOverrides = final: prev: { - # Add setuptools for packages that need it during build - ruamel-yaml-clib = prev.ruamel-yaml-clib.overrideAttrs (old: { - nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ - final.setuptools - ]; - }); - proxy-tools = prev.proxy-tools.overrideAttrs (old: { - nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ - final.setuptools - ]; - }); - pywebview = prev.pywebview.overrideAttrs (old: { - nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ - final.setuptools - final.setuptools-scm - ]; - }); - }; - python = pkgs.python311; + pyprojectHacks = pkgs.callPackage pyproject-nix.build.hacks {}; + + pyprojectOverrides = final: prev: + { + # Add setuptools for packages that need it during build + ruamel-yaml-clib = prev.ruamel-yaml-clib.overrideAttrs (old: { + nativeBuildInputs = + (old.nativeBuildInputs or []) + ++ [ + final.setuptools + ]; + }); + proxy-tools = prev.proxy-tools.overrideAttrs (old: { + nativeBuildInputs = + (old.nativeBuildInputs or []) + ++ [ + final.setuptools + ]; + }); + pywebview = prev.pywebview.overrideAttrs (old: { + nativeBuildInputs = + (old.nativeBuildInputs or []) + ++ [ + final.setuptools + final.setuptools-scm + ]; + }); + } + // lib.optionalAttrs pkgs.stdenv.isLinux { + # pyproject-nix's virtualenv resolver follows pyproject-style + # passthru.dependencies, while these GI bindings are already packaged + # by nixpkgs' Python infrastructure. nixpkgsPrebuilt is the upstream + # adapter for that case: + # https://pyproject-nix.github.io/pyproject.nix/builders/hacks.html#using-prebuilt-packages-from-nixpkgs + pycairo = pyprojectHacks.nixpkgsPrebuilt { + from = python.pkgs.pycairo; + prev = { + passthru = { + dependencies = {}; + optional-dependencies = {}; + dependency-groups = {}; + }; + }; + }; + pygobject3 = pyprojectHacks.nixpkgsPrebuilt { + from = python.pkgs.pygobject3; + prev = { + passthru = { + dependencies = { + pycairo = []; + }; + optional-dependencies = {}; + dependency-groups = {}; + }; + }; + }; + + # pystray has an optional AppIndicator backend on Linux. Its uv lock + # metadata does not encode the GI dependency, so add the runtime edge + # here while keeping pystray itself lockfile-driven. + pystray = prev.pystray.overrideAttrs (old: { + passthru = + (old.passthru or {}) + // { + dependencies = + (old.passthru.dependencies or {}) + // { + pygobject3 = []; + }; + }; + }); + }; pythonSet = (pkgs.callPackage pyproject-nix.build.packages { @@ -88,20 +140,48 @@ packages = { serena-env = pythonSet.mkVirtualEnv "serena-env" workspace.deps.default; - serena = pkgs.runCommand "serena" { - meta = { - description = "A powerful coding agent toolkit providing semantic retrieval and editing capabilities (MCP server & Agno integration)"; - homepage = "https://oraios.github.io/serena"; - changelog = "https://github.com/oraios/serena/blob/main/CHANGELOG.md"; - mainProgram = "serena"; - license = pkgs.lib.licenses.mit; - platforms = lib.platforms.all; - }; - } '' - mkdir -p $out/bin - ln -s ${packages.serena-env}/bin/serena $out/bin/serena - ln -s ${packages.serena-env}/bin/serena-hooks $out/bin/serena-hooks - ''; + serena = pkgs.stdenv.mkDerivation { + name = "serena"; + dontUnpack = true; + dontWrapGApps = true; + nativeBuildInputs = + [pkgs.makeWrapper] + ++ lib.optionals pkgs.stdenv.isLinux [ + pkgs.wrapGAppsHook3 + pkgs.gobject-introspection + ]; + buildInputs = lib.optionals pkgs.stdenv.isLinux [ + pkgs.gtk3 + pkgs.libayatana-appindicator + ]; + installPhase = '' + runHook preInstall + + mkdir -p $out/bin + ${ + lib.optionalString (!pkgs.stdenv.isLinux) '' + ln -s ${packages.serena-env}/bin/serena $out/bin/serena + '' + } + ln -s ${packages.serena-env}/bin/serena-hooks $out/bin/serena-hooks + + runHook postInstall + ''; + # Run the GApps fixup phase so the wrapper gets GI_TYPELIB_PATH for + # Gtk and AyatanaAppIndicator3. This is the same pattern nixpkgs uses + # for pystray consumers such as plex-mpv-shim and jellyfin-mpv-shim. + preFixup = lib.optionalString pkgs.stdenv.isLinux '' + makeWrapper ${packages.serena-env}/bin/serena $out/bin/serena "''${gappsWrapperArgs[@]}" + ''; + meta = { + description = "A powerful coding agent toolkit providing semantic retrieval and editing capabilities (MCP server & Agno integration)"; + homepage = "https://oraios.github.io/serena"; + changelog = "https://github.com/oraios/serena/blob/main/CHANGELOG.md"; + mainProgram = "serena"; + license = pkgs.lib.licenses.mit; + platforms = lib.platforms.all; + }; + }; default = packages.serena; }; diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index 8664fea3..36f22bf3 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -61,6 +61,8 @@ web_dashboard_open_on_launch: True # opening the dashboard in browser tabs when selected from the tray menu. # This is EXPERIMENTAL. It is tested on Windows only. We will establish macOS support, but it is yet untested. # On Linux, this cannot be universally supported, but it may work in some desktop environments. +# On NixOS, when using the package from flake.nix, both modern AppIndicator trays as well as +# older Xorg-/XEmbed-based trays should be supported. # See https://oraios.github.io/serena/02-usage/060_dashboard.html web_dashboard_interface: From 5db677162c104b446cfd1c78a919451fced7fcdf Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 2 Jun 2026 10:30:27 +0200 Subject: [PATCH 6/9] Improve quoting of arguments for subprocess executions * Add util function subprocess_util.convert_shell_cmd * Apply function in - StdioLanguageServer (replacing use of quote_arg function) - RuntimeDependencyCollection (previously no quoting applied) --- CHANGELOG.md | 1 + src/solidlsp/language_servers/common.py | 11 +++-------- src/solidlsp/ls_process.py | 16 ++++++---------- src/solidlsp/util/subprocess_util.py | 20 ++++++++++---------- 4 files changed, 20 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce28a00..d9744c6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Status of the `main` branch. Changes prior to the next official version change w at the Yarn-generated SDK. - `SvelteLanguageServer`: Fix diagnostics requests for TypeScript/JavaScript files incorrectly being processed by the Svelte LS instead of the TypeScript LS. + - Improve quoting of arguments in shell executions * Dashboard: - Make list of trusted hosts configurable, fixing host validation introduced in v1.5.2 allowing only diff --git a/src/solidlsp/language_servers/common.py b/src/solidlsp/language_servers/common.py index 16a97411..dc38076d 100644 --- a/src/solidlsp/language_servers/common.py +++ b/src/solidlsp/language_servers/common.py @@ -10,7 +10,7 @@ from dataclasses import dataclass, replace from typing import Any, cast from solidlsp.ls_utils import FileUtils, PlatformUtils -from solidlsp.util.subprocess_util import subprocess_kwargs +from solidlsp.util import subprocess_util log = logging.getLogger(__name__) @@ -101,18 +101,13 @@ class RuntimeDependencyCollection: @staticmethod def _run_command(command: str | list[str], cwd: str) -> None: - kwargs = subprocess_kwargs() + kwargs = subprocess_util.subprocess_kwargs() if not PlatformUtils.get_platform_id().is_windows(): import pwd kwargs["user"] = pwd.getpwuid(os.getuid()).pw_name # type: ignore - is_windows = platform.system() == "Windows" - if not isinstance(command, str) and not is_windows: - # Since we are using the shell, we need to convert the command list to a single string - # on Linux/macOS - command = " ".join(command) - + command = subprocess_util.convert_shell_cmd(command) log.info("Running command %s in '%s'", f"'{command}'" if isinstance(command, str) else command, cwd) completed_process = subprocess.run( diff --git a/src/solidlsp/ls_process.py b/src/solidlsp/ls_process.py index 5ac06c0e..820947af 100644 --- a/src/solidlsp/ls_process.py +++ b/src/solidlsp/ls_process.py @@ -2,7 +2,6 @@ import asyncio import json import logging import os -import platform import socket import subprocess import threading @@ -33,7 +32,7 @@ from solidlsp.lsp_protocol_handler.server import ( make_request, make_response, ) -from solidlsp.util.subprocess_util import quote_arg, subprocess_kwargs, terminate_process_tree_with_kill_fallback +from solidlsp.util import subprocess_util log = logging.getLogger(__name__) @@ -459,14 +458,9 @@ class StdioLanguageServer(LanguageServerInterface): child_proc_env = os.environ.copy() child_proc_env.update(self.process_launch_info.env) - cmd = self.process_launch_info.cmd - is_windows = platform.system() == "Windows" - if not isinstance(cmd, str) and not is_windows: - # Since we are using the shell, we need to convert the command list to a single string - # on Linux/macOS - cmd = " ".join(map(quote_arg, cmd)) + cmd = subprocess_util.convert_shell_cmd(self.process_launch_info.cmd) log.info("Starting language server process via command: %s", self.process_launch_info.cmd) - kwargs = subprocess_kwargs() + kwargs = subprocess_util.subprocess_kwargs() kwargs["start_new_session"] = self.start_independent_lsp_process self.process = subprocess.Popen( cmd, @@ -513,7 +507,9 @@ class StdioLanguageServer(LanguageServerInterface): log.debug(f"Exception during graceful shutdown: {e}") # Ignore errors here, we are proceeding to terminate anyway. # terminate the process - terminate_process_tree_with_kill_fallback(self.process, terminate_timeout=timeout, process_name=f"LS[{self.language.value}]") + subprocess_util.terminate_process_tree_with_kill_fallback( + self.process, terminate_timeout=timeout, process_name=f"LS[{self.language.value}]" + ) finally: self.process = None diff --git a/src/solidlsp/util/subprocess_util.py b/src/solidlsp/util/subprocess_util.py index 25dc2963..9d13773a 100644 --- a/src/solidlsp/util/subprocess_util.py +++ b/src/solidlsp/util/subprocess_util.py @@ -19,19 +19,19 @@ def subprocess_kwargs() -> dict: return kwargs -def quote_arg(arg: str) -> str: +def convert_shell_cmd(cmd: str | list[str]) -> str | list[str]: """ - Quotes a shell argument to prevent interpretation of metacharacters. + Converts a command (specified as a list or string) to a format supported by subprocess calls with shell=True on the current platform. + List format must be converted to string format on POSIX systems, quoting arguments appropriately, + while it can be used as-is on Windows. - Uses :func:`shlex.quote` on POSIX systems for proper escaping of all - shell-special characters. On Windows, wraps arguments containing spaces - in double quotes (Windows shell does not interpret single-quoted strings). + :param cmd: the command to convert, specified as a list of arguments + :return: a suitable representation of the command for subprocess calls on the current platform """ - if platform.system() == "Windows": - if " " not in arg: - return arg - return f'"{arg}"' - return shlex.quote(arg) + if isinstance(cmd, list) and platform.system() != "Windows": + return " ".join(shlex.quote(arg) for arg in cmd) + else: + return cmd def _signal_process_tree(process: subprocess.Popen[bytes], terminate: bool = True) -> None: From 02c3d6bb78cbc2bf1659e76982ba36f4d760fe77 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 2 Jun 2026 11:36:05 +0200 Subject: [PATCH 7/9] Fix: Wait for the subprocess that opens the browser window, preventing zombie processes Fixes #1488 --- CHANGELOG.md | 1 + src/serena/dashboard.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce28a00..adc1788f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Status of the `main` branch. Changes prior to the next official version change w This allows the dashboard to display the configuration while the project provided at startup is still initialising. #1064 - Fix empty executions queue displaying "Loading..." - Tray manager: Add NixOS-support for AppIndicator-based trays (e.g., most Wayland-trays) to the package in flake.nix. + - Fix: Wait for the subprocess that opens the browser window, preventing zombie processes #1488 # v1.5.3 (2026-05-26) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 7b599f9c..cac54bc5 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -813,13 +813,14 @@ def open_url_in_browser(url: str, use_subprocess: bool = False) -> None: if use_subprocess: # Use a subprocess to avoid any output from webbrowser.open being written to stdout try: - subprocess.Popen( + p = subprocess.Popen( [sys.executable, "-c", f"import webbrowser; webbrowser.open({url!r})"], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=False, ) + threading.Thread(target=p.wait, daemon=True).start() except Exception as e: # Subprocess creation can fail in rare cases (e.g. on some Linux systems; possibly subprocess/glibc bug) # See #1363 From a10c3e1c5a263c4db919f212f8bd4a6af4864ff4 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 2 Jun 2026 13:46:36 +0200 Subject: [PATCH 8/9] Update additional usage pointers * Remove obsolete/outdated content * git-worktree: - Remove incorrect recommendation to copy the entire .serena folder in order to retain the cache (cache contains abs. paths) - Recommend to add the .serena folder to version control instead (which will not include the cache folder) Relates to #805 --- docs/02-usage/999_additional-usage.md | 28 ++++++--------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/docs/02-usage/999_additional-usage.md b/docs/02-usage/999_additional-usage.md index 275fd190..d4da7ce6 100644 --- a/docs/02-usage/999_additional-usage.md +++ b/docs/02-usage/999_additional-usage.md @@ -3,30 +3,14 @@ ## Prompting Strategies We found that it is often a good idea to spend some time conceptualizing and planning a task -before actually implementing it, especially for non-trivial task. This helps both in achieving -better results and in increasing the feeling of control and staying in the loop. You can -make a detailed plan in one session, where Serena may read a lot of your code to build up the context, -and then continue with the implementation in another (potentially after creating suitable memories). - -## Running Out of Context - -For long and complicated tasks, or tasks where Serena has read a lot of content, you -may come close to the limits of context tokens. In that case, it is often a good idea to continue -in a new conversation. Serena has a dedicated tool to create a summary of the current state -of the progress and all relevant info for continuing it. You can request to create this summary and -write it to a memory. Then, in a new conversation, you can just ask Serena to read the memory and -continue with the task. In our experience, this worked really well. On the up-side, since in a -single session there is no summarization involved, Serena does not usually get lost (unlike some -other agents that summarize under the hood), and it is also instructed to occasionally check whether -it's on the right track. - -Serena instructs the LLM to be economical in general, so the problem of running out of context -should not occur too often, unless the task is very large or complicated. +before actually implementing it, especially for non-trivial tasks. +For very complex tasks, you can make a detailed plan in one session, +where Serena may read a lot of your code to build up the context, +and then continue with the implementation in another, +having persisted the plan in a memory or dedicated file. ## Serena and Git Worktrees [git-worktree](https://git-scm.com/docs/git-worktree) can be an excellent way to parallelize your work. More on this in [Anthropic: Run parallel Claude Code sessions with Git worktrees](https://docs.claude.com/en/docs/claude-code/common-workflows#run-parallel-claude-code-sessions-with-git-worktrees). -When it comes to serena AND git-worktree AND larger projects (that take longer to index), -the recommended way is to COPY your `$ORIG_PROJECT/.serena/cache` to `$GIT_WORKTREE/.serena/cache`. -Perform [pre-indexing of your project](indexing) to avoid having to re-index per each worktree you create. +Be sure to add the `.serena` folder to version control, such that your project-specific settings and memories are available across worktrees. From 1a3943b9172aa43eb988a5597d47da09fb1748cc Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 2 Jun 2026 16:24:03 +0200 Subject: [PATCH 9/9] Add configuration option `jetbrains_launch_command`, allowing Serena to spawn IDE instances automatically upon project activation Serena searches for the plugin instance upon project activation, and if the instance is not found and the setting is configured, launches a new IDE instance based on the command --- CHANGELOG.md | 4 ++++ docs/02-usage/025_jetbrains_plugin.md | 8 +++++-- src/serena/agent.py | 22 +++++++++++++++++ src/serena/config/serena_config.py | 4 ++++ .../jetbrains/jetbrains_plugin_client.py | 24 ++++++++++++------- .../resources/serena_config.template.yml | 7 ++++++ 6 files changed, 58 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b15b206..75c9309c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ Status of the `main` branch. Changes prior to the next official version change w processed by the Svelte LS instead of the TypeScript LS. - Improve quoting of arguments in shell executions +* JetBrains: + - Add configuration option `jetbrains_launch_command`, allowing Serena to spawn IDE instances automatically + upon project activation + * Dashboard: - Make list of trusted hosts configurable, fixing host validation introduced in v1.5.2 allowing only default local hostnames, effectively preventing remote connections diff --git a/docs/02-usage/025_jetbrains_plugin.md b/docs/02-usage/025_jetbrains_plugin.md index 4eb08688..73638445 100644 --- a/docs/02-usage/025_jetbrains_plugin.md +++ b/docs/02-usage/025_jetbrains_plugin.md @@ -84,7 +84,7 @@ language_backend: JetBrains Note that the file might not exist yet if you never executed Serena before. -**Per-Server Instance Configuration**. +**Per-Instance Configuration**. The configuration setting in the global config file can be overridden on a per-instance basis by providing the arguments `--language-backend JetBrains` when launching the Serena MCP server. @@ -121,8 +121,12 @@ You will also notice that your client will use the JetBrains-specific tools like Having installed the plugin in your IDE and having configured Serena to use the JetBrains backend, the general workflow is simple: + 1. Open the project you want to work on in your JetBrains IDE. - Note that the project must be appropriately set up in your IDE, i.e. symbol lookups for all relevant programming languages and frameworks should work in the IDE. + Note that the project must be appropriately set up in your IDE, i.e. symbol lookups for all relevant programming languages and frameworks should work in the IDE. + + You can optionally make Serena open an IDE instance for your project root folder automatically upon project activation, allowing you to skip this step for a project that was previously set up correctly. + To enable this, configure `jetbrains_launch_command` in [Serena's global configuration file](global-config) appropriately. 2. Activate the project's root folder as a project in Serena (see [Project Creation](project-creation-indexing) and [Project Activation](project-activation)). 3. Start using Serena's tools as usual. diff --git a/src/serena/agent.py b/src/serena/agent.py index a1db940a..bef1b201 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -7,6 +7,7 @@ import multiprocessing import os import platform import signal +import subprocess import threading from collections import defaultdict from collections.abc import Callable, Iterator, Sequence @@ -40,6 +41,7 @@ from serena.config.serena_config import ( ToolInclusionDefinition, ) from serena.dashboard import SerenaDashboardAPI, SerenaDashboardTrayManager, SerenaDashboardViewer, open_url_in_browser +from serena.jetbrains import jetbrains_plugin_client from serena.ls_manager import LanguageServerManager from serena.memories.memory_manager import MemoryManager from serena.project import Project @@ -60,6 +62,7 @@ from serena.util.gui import system_has_usable_display from serena.util.inspection import iter_subclasses from serena.util.logging import MemoryLogHandler from solidlsp.ls_config import Language +from solidlsp.util import subprocess_util if TYPE_CHECKING: from serena.gui_log_viewer import GuiLogViewer @@ -1165,6 +1168,25 @@ class SerenaAgent: if self.get_language_backend().is_lsp(): self.issue_task(init_language_server_manager) + def init_jetbrains_ide() -> None: + assert self.serena_config.jetbrains_launch_command is not None + try: + client = jetbrains_plugin_client.JetBrainsPluginClient.from_project(project, log_warning=False) + log.info("Found Serena JetBrains Plugin server: %s", client) + except jetbrains_plugin_client.ServerNotFoundError: + log.info("Serena JetBrains Plugin server not found for project %s", project.project_name) + if self.serena_config.jetbrains_launch_command: + cmd = subprocess_util.convert_shell_cmd([self.serena_config.jetbrains_launch_command, project.project_root]) + log.info("Launching IDE with command: %s", cmd) + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + stdout, stderr = p.communicate() + if p.returncode != 0: + log.error(f"Failed to launch JetBrains IDE: {stderr.decode('utf-8')}") + + # for JetBrains mode, search for plugin server and spawn IDE if not found and enabled + if self.get_language_backend().is_jetbrains(): + self.issue_task(init_jetbrains_ide) + if self._project_activation_callback is not None: self._project_activation_callback() diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 7207a9b1..03a18e72 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -722,6 +722,10 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): web_dashboard_listen_address: str = "127.0.0.1" web_dashboard_trusted_hosts: list[str] = field(default_factory=lambda: ["127.0.0.1", "localhost"]) jetbrains_plugin_server_address: str = "127.0.0.1" + jetbrains_launch_command: str | None = None + """ + JetBrains IDE launch command, which can be used to auto-start an IDE instance on demand. + """ tool_timeout: float = DEFAULT_TOOL_TIMEOUT token_count_estimator: str = RegisteredTokenCountEstimator.CHAR_COUNT.name diff --git a/src/serena/jetbrains/jetbrains_plugin_client.py b/src/serena/jetbrains/jetbrains_plugin_client.py index c7b440ce..3cbd915f 100644 --- a/src/serena/jetbrains/jetbrains_plugin_client.py +++ b/src/serena/jetbrains/jetbrains_plugin_client.py @@ -105,7 +105,12 @@ class JetBrainsPluginClientManager: futures.append(future) return futures - def find_client(self, project_root: Path) -> "JetBrainsPluginClient": + def find_client(self, project_root: Path, log_warning: bool = True) -> "JetBrainsPluginClient": + """ + :param project_root: the project root path to find a plugin instance for + :param log_warning: whether to log a warning if no matching plugin instance is found + :return: the instance + """ plugin_paths_found = [] for future in self._submit_scan(): client = future.result() @@ -114,12 +119,13 @@ class JetBrainsPluginClientManager: elif client.project_root is not None: plugin_paths_found.append(client.project_root) - log.warning( - "Searched for Serena JetBrains plugin service for project at %s but found no matching service. " - "Found plugin instances for the following project paths: %s", - project_root, - plugin_paths_found, - ) + if log_warning: + log.warning( + "Searched for Serena JetBrains plugin service for project at %s but found no matching service. " + "Found plugin instances for the following project paths: %s", + project_root, + plugin_paths_found, + ) raise ServerNotFoundError( f"Found no Serena service in a JetBrains IDE instance for the project at {project_root}. " "STOP. Do not attempt any other tools or workarounds. Ask the user to open this folder as a project in a JetBrains IDE " @@ -219,7 +225,7 @@ class JetBrainsPluginClient(ToStringMixin): return ["_port", "project_root", "_plugin_version"] @classmethod - def from_project(cls, project: Project) -> Self: + def from_project(cls, project: Project, log_warning: bool = True) -> Self: resolved_path = Path(project.project_root).resolve() if cls._last_port is not None: @@ -227,7 +233,7 @@ class JetBrainsPluginClient(ToStringMixin): if client.matches(resolved_path): return client - client = JetBrainsPluginClientManager().find_client(resolved_path) + client = JetBrainsPluginClientManager().find_client(resolved_path, log_warning=log_warning) cls._last_port = client._port return client diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index 36f22bf3..fb5c1d40 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -80,6 +80,13 @@ web_dashboard_trusted_hosts: # address where JetBrains plugin servers are running (only relevant when using the JetBrains language backend) jetbrains_plugin_server_address: 127.0.0.1 +# command used to launch a JetBrains IDE on demand (only relevant when using the JetBrains language backend) +# If this is non-empty and, at project activation, the Serena JetBrains Plugin server instance corresponding +# to the project is not found, use this command to spawn a new instance. +# Specifically, if this is , then ` ` is launched. +# Provide the full path to the executable/script (e.g., "/usr/bin/idea" or "C:/Users/bob/AppData/Local/JetBrains/Toolbox/scripts/idea.cmd"). +jetbrains_launch_command: + # the minimum log level for the GUI log window and the dashboard (10 = debug, 20 = info, 30 = warning, 40 = error) log_level: 20