From 511ad0542bb71610b45fe259b361b428a09b6613 Mon Sep 17 00:00:00 2001 From: Tomasz Wyderka Date: Sat, 27 Dec 2025 20:16:57 +0100 Subject: [PATCH 1/5] Add open_dashboard MCP tool for on-demand dashboard opening This provides an alternative for users who want to disable automatic browser opening (web_dashboard_open_on_launch: False) but still have easy access to the dashboard through AI assistance. Changes: - Add OpenDashboardTool in config_tools.py - Add open_dashboard() and get_dashboard_url() methods to SerenaAgent - Store dashboard URL in _dashboard_url field - Update documentation with new tool and configuration option - Update config template comment to mention the tool Closes #875 --- docs/01-about/035_tools.md | 1 + docs/02-usage/060_dashboard.md | 9 ++++++ src/serena/agent.py | 29 +++++++++++++++++++ .../resources/serena_config.template.yml | 3 +- src/serena/tools/config_tools.py | 13 +++++++++ 5 files changed, 54 insertions(+), 1 deletion(-) diff --git a/docs/01-about/035_tools.md b/docs/01-about/035_tools.md index 55ff8bf2..23662750 100644 --- a/docs/01-about/035_tools.md +++ b/docs/01-about/035_tools.md @@ -28,6 +28,7 @@ Note that in most configurations, only a subset of these tools will be enabled s * `list_dir`: Lists files and directories in the given directory (optionally with recursion). * `list_memories`: Lists memories in Serena's project-specific memory store. * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +* `open_dashboard`: Opens the Serena web dashboard in the default web browser. * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with th e necessary context). * `read_file`: Reads a file within the project directory. diff --git a/docs/02-usage/060_dashboard.md b/docs/02-usage/060_dashboard.md index 73376926..0a7a462a 100644 --- a/docs/02-usage/060_dashboard.md +++ b/docs/02-usage/060_dashboard.md @@ -20,3 +20,12 @@ Serena comes with built-in tools for monitoring and managing the current session 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. + +## Disabling Automatic Browser Opening + +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`. + +When automatic opening is disabled, you can still access the dashboard by: +* Navigating directly to `http://localhost:24282/dashboard/index.html` in your browser +* Asking the AI to use the `open_dashboard` tool, which will open the dashboard in your default browser diff --git a/src/serena/agent.py b/src/serena/agent.py index 02d44079..7f0c0e9c 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -189,6 +189,9 @@ class SerenaAgent: # project-specific instances, which will be initialized upon project activation self._active_project: Project | None = None + # dashboard URL (set when dashboard is started) + self._dashboard_url: str | None = None + # adjust log level serena_log_level = self.serena_config.log_level if Logger.root.level != serena_log_level: @@ -292,6 +295,7 @@ class SerenaAgent: if dashboard_host == "0.0.0.0": dashboard_host = "localhost" 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: if not system_has_usable_display(): @@ -405,6 +409,31 @@ class SerenaAgent: # open the dashboard URL in the default web browser webbrowser.open(url) + def get_dashboard_url(self) -> str | None: + """ + :return: the URL of the web dashboard, or None if the dashboard is not running + """ + return self._dashboard_url + + def open_dashboard(self) -> str: + """ + Opens the Serena web dashboard in the default web browser. + + :return: a message indicating success or failure + """ + if self._dashboard_url is None: + return "Error: The web dashboard is not running. Enable it in the Serena configuration." + + if not system_has_usable_display(): + return f"Error: No usable display detected. You can open the dashboard manually at: {self._dashboard_url}" + + # open the dashboard URL in the default web browser (using a separate process to control + # output redirection) + process = multiprocessing.Process(target=self._open_dashboard, args=(self._dashboard_url,)) + process.start() + process.join(timeout=1) + return f"Dashboard opened in the default web browser: {self._dashboard_url}" + def get_project_root(self) -> str: """ :return: the root directory of the active project (if any); raises a ValueError if there is no active project diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index d197991d..9465961c 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -35,7 +35,8 @@ web_dashboard_listen_address: 127.0.0.1 web_dashboard_open_on_launch: True # whether to open a browser window with the web dashboard when Serena starts (provided that web_dashboard # is enabled). If set to False, you can still open the dashboard manually by navigating to -# http://localhost:24282/dashboard/ in your web browser (24282 = 0x5EDA, SErena DAshboard). +# http://localhost:24282/dashboard/ in your web browser (24282 = 0x5EDA, SErena DAshboard), +# or by asking the AI to use the open_dashboard tool. # If you have multiple instances running, a higher port will be used; try port 24283, 24284, etc. log_level: 20 diff --git a/src/serena/tools/config_tools.py b/src/serena/tools/config_tools.py index 45ca01c7..265bb2f3 100644 --- a/src/serena/tools/config_tools.py +++ b/src/serena/tools/config_tools.py @@ -2,6 +2,19 @@ from serena.config.context_mode import SerenaAgentMode from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional +class OpenDashboardTool(Tool, ToolMarkerDoesNotRequireActiveProject): + """ + Opens the Serena web dashboard in the default web browser. + The dashboard provides logs, session information, and tool usage statistics. + """ + + def apply(self) -> str: + """ + Opens the Serena web dashboard in the default web browser. + """ + return self.agent.open_dashboard() + + class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject): """ Activates a project based on the project name or path. From 0f5d86aadfb09076fd7eb659dd7fa8c79c72a155 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 28 Dec 2025 19:58:38 +0100 Subject: [PATCH 2/5] Improve handling of OpenDashboardTool * Mark the tool as optional (disabled by default) * Enable the tool if the dashboard is enabled, not opened and the GUI is off * Improve factorisation and return value/error handling --- docs/02-usage/060_dashboard.md | 8 ++++++-- src/serena/agent.py | 33 ++++++++++++++++++-------------- src/serena/tools/config_tools.py | 7 +++++-- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/docs/02-usage/060_dashboard.md b/docs/02-usage/060_dashboard.md index 0a7a462a..eeeab1ca 100644 --- a/docs/02-usage/060_dashboard.md +++ b/docs/02-usage/060_dashboard.md @@ -15,11 +15,13 @@ Serena comes with built-in tools for monitoring and managing the current session * the **GUI tool** (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). 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 @@ -27,5 +29,7 @@ If you prefer not to have the dashboard open automatically (e.g., to avoid focus by setting `web_dashboard_open_on_launch: False` in your `serena_config.yml`. When automatic opening is disabled, you can still access the dashboard by: -* Navigating directly to `http://localhost:24282/dashboard/index.html` in your browser -* Asking the AI to use the `open_dashboard` tool, which will open the dashboard in your default browser +* 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) diff --git a/src/serena/agent.py b/src/serena/agent.py index 7f0c0e9c..a3fe8750 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -24,7 +24,7 @@ from serena.ls_manager import LanguageServerManager from serena.project import Project from serena.prompt_factory import SerenaPromptFactory from serena.task_executor import TaskExecutor -from serena.tools import ActivateProjectTool, GetCurrentConfigTool, ReplaceContentTool, Tool, ToolMarker, ToolRegistry +from serena.tools import ActivateProjectTool, GetCurrentConfigTool, OpenDashboardTool, ReplaceContentTool, Tool, ToolMarker, ToolRegistry from serena.util.gui import system_has_usable_display from serena.util.inspection import iter_subclasses from serena.util.logging import MemoryLogHandler @@ -250,8 +250,19 @@ class SerenaAgent: self._check_shell_settings() # determine the base toolset defining the set of exposed tools (which e.g. the MCP shall see), - # limited by the Serena config, the context (which is fixed for the session) and JetBrains mode + # determined by the + # * Serena config, + # * the context (which is fixed for the session) + # * dashboard availability/opening on launch + # * single-project mode reductions (if applicable) + # * JetBrains mode tool_inclusion_definitions: list[ToolInclusionDefinition] = [self.serena_config, self._context] + if ( + self.serena_config.web_dashboard + and not self.serena_config.web_dashboard_open_on_launch + and not self.serena_config.gui_log_window_enabled + ): + tool_inclusion_definitions.append(ToolInclusionDefinition(included_optional_tools=[OpenDashboardTool.get_name_from_cls()])) if self._context.single_project: tool_inclusion_definitions.extend(self._single_project_context_tool_inclusion_definitions(project)) if self.serena_config.language_backend == LanguageBackend.JETBRAINS: @@ -298,14 +309,7 @@ class SerenaAgent: self._dashboard_url = dashboard_url log.info("Serena web dashboard started at %s", dashboard_url) if self.serena_config.web_dashboard_open_on_launch: - if not system_has_usable_display(): - log.warning("Not opening the Serena web dashboard automatically because no usable display was detected.") - else: - # open the dashboard URL in the default web browser (using a separate process to control - # output redirection) - process = multiprocessing.Process(target=self._open_dashboard, args=(dashboard_url,)) - process.start() - process.join(timeout=1) + self.open_dashboard() # inform the GUI window (if any) if self._gui_log_viewer is not None: self._gui_log_viewer.set_dashboard_url(dashboard_url) @@ -415,24 +419,25 @@ class SerenaAgent: """ return self._dashboard_url - def open_dashboard(self) -> str: + def open_dashboard(self) -> bool: """ Opens the Serena web dashboard in the default web browser. :return: a message indicating success or failure """ if self._dashboard_url is None: - return "Error: The web dashboard is not running. Enable it in the Serena configuration." + raise Exception("Dashboard is not running.") if not system_has_usable_display(): - return f"Error: No usable display detected. You can open the dashboard manually at: {self._dashboard_url}" + log.warning("Not opening the Serena web dashboard because no usable display was detected.") + return False # open the dashboard URL in the default web browser (using a separate process to control # output redirection) process = multiprocessing.Process(target=self._open_dashboard, args=(self._dashboard_url,)) process.start() process.join(timeout=1) - return f"Dashboard opened in the default web browser: {self._dashboard_url}" + return True def get_project_root(self) -> str: """ diff --git a/src/serena/tools/config_tools.py b/src/serena/tools/config_tools.py index 265bb2f3..7de51e59 100644 --- a/src/serena/tools/config_tools.py +++ b/src/serena/tools/config_tools.py @@ -2,7 +2,7 @@ from serena.config.context_mode import SerenaAgentMode from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional -class OpenDashboardTool(Tool, ToolMarkerDoesNotRequireActiveProject): +class OpenDashboardTool(Tool, ToolMarkerOptional, ToolMarkerDoesNotRequireActiveProject): """ Opens the Serena web dashboard in the default web browser. The dashboard provides logs, session information, and tool usage statistics. @@ -12,7 +12,10 @@ class OpenDashboardTool(Tool, ToolMarkerDoesNotRequireActiveProject): """ Opens the Serena web dashboard in the default web browser. """ - return self.agent.open_dashboard() + if self.agent.open_dashboard(): + return f"Serena web dashboard has been opened in the user's default web browser: {self.agent.get_dashboard_url()}" + else: + return f"Serena web dashboard could not be opened automatically; tell the user to open it via {self.agent.get_dashboard_url()}" class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject): From 2138adf4d4ec2d04b8eac961efa34c3b523ed489 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 28 Dec 2025 20:40:53 +0100 Subject: [PATCH 3/5] Use subprocess instead of multiprocessing to open the dashboard (output redirection with multiprocessing worked only when called from the main thread) --- src/serena/agent.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index a3fe8750..2ec82317 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -2,11 +2,10 @@ The Serena Model Context Protocol (MCP) Server """ -import multiprocessing import os import platform +import subprocess import sys -import webbrowser from collections.abc import Callable from logging import Logger from typing import TYPE_CHECKING, Optional, TypeVar @@ -401,18 +400,6 @@ class SerenaAgent: log.debug(f"Recording tool usage for tool '{tool_name}'") self._tool_usage_stats.record_tool_usage(tool_name, input_str, output_str) - @staticmethod - def _open_dashboard(url: str) -> None: - # Redirect stdout and stderr file descriptors to /dev/null, - # making sure that nothing can be written to stdout/stderr, even by subprocesses - null_fd = os.open(os.devnull, os.O_WRONLY) - os.dup2(null_fd, sys.stdout.fileno()) - os.dup2(null_fd, sys.stderr.fileno()) - os.close(null_fd) - - # open the dashboard URL in the default web browser - webbrowser.open(url) - def get_dashboard_url(self) -> str | None: """ :return: the URL of the web dashboard, or None if the dashboard is not running @@ -432,11 +419,14 @@ class SerenaAgent: log.warning("Not opening the Serena web dashboard because no usable display was detected.") return False - # open the dashboard URL in the default web browser (using a separate process to control - # output redirection) - process = multiprocessing.Process(target=self._open_dashboard, args=(self._dashboard_url,)) - process.start() - process.join(timeout=1) + # 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})"], + 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 05b93fd4690eded05964b2d15446ab395a5ff6fc Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 28 Dec 2025 20:52:44 +0100 Subject: [PATCH 4/5] Improve comments --- .../resources/serena_config.template.yml | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index 9465961c..26158083 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -10,9 +10,8 @@ language_backend: LSP gui_log_window: False # whether to open a graphical window with Serena's logs. # This is mainly supported on Windows and (partly) on Linux; not available on macOS. -# If you want to see the logs in a web browser, use the `web_dashboard` option instead. -# Limitations: doesn't seem to work with the community version of Claude Desktop for Linux -# Might also cause problems with some MCP clients - if you have any issues, try disabling this +# If you prefer a browser-based tool, use the `web_dashboard` option instead. +# Further information: https://oraios.github.io/serena/02-usage/060_dashboard.html # # Being able to inspect logs is useful both for troubleshooting and for monitoring the tool calls, # especially when using the agno playground, since the tool calls are not always shown, @@ -26,18 +25,21 @@ gui_log_window: False web_dashboard: True # whether to open the Serena web dashboard (which will be accessible through your web browser) that -# shows Serena's current session logs - as an alternative to the GUI log window which -# is supported on all platforms. +# provides access to Serena's configuration and state as well as the current session logs. +# The web dashboard is supported on all platforms. +# Further information: https://oraios.github.io/serena/02-usage/060_dashboard.html web_dashboard_listen_address: 127.0.0.1 # The address where the web dashboard will listen on web_dashboard_open_on_launch: True # whether to open a browser window with the web dashboard when Serena starts (provided that web_dashboard -# is enabled). If set to False, you can still open the dashboard manually by navigating to -# http://localhost:24282/dashboard/ in your web browser (24282 = 0x5EDA, SErena DAshboard), -# or by asking the AI to use the open_dashboard tool. -# If you have multiple instances running, a higher port will be used; try port 24283, 24284, etc. +# is enabled). +# If set to false, you can still open the dashboard manually 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 log_level: 20 # the minimum log level for the GUI log window and the dashboard (10 = debug, 20 = info, 30 = warning, 40 = error) From a8c720e3b47470d9334cefd9c83f63888ba5308b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 28 Dec 2025 21:10:44 +0100 Subject: [PATCH 5/5] Reorder tool inclusion definitions for improved flexibility (allowing, in particular, open_dashboard to be disabled by SerenaConfig) --- src/serena/agent.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 2ec82317..ae39e8cd 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -250,18 +250,20 @@ class SerenaAgent: # determine the base toolset defining the set of exposed tools (which e.g. the MCP shall see), # determined by the + # * dashboard availability/opening on launch # * Serena config, # * the context (which is fixed for the session) - # * dashboard availability/opening on launch # * single-project mode reductions (if applicable) # * JetBrains mode - tool_inclusion_definitions: list[ToolInclusionDefinition] = [self.serena_config, self._context] + tool_inclusion_definitions: list[ToolInclusionDefinition] = [] if ( self.serena_config.web_dashboard and not self.serena_config.web_dashboard_open_on_launch and not self.serena_config.gui_log_window_enabled ): tool_inclusion_definitions.append(ToolInclusionDefinition(included_optional_tools=[OpenDashboardTool.get_name_from_cls()])) + tool_inclusion_definitions.append(self.serena_config) + tool_inclusion_definitions.append(self._context) if self._context.single_project: tool_inclusion_definitions.extend(self._single_project_context_tool_inclusion_definitions(project)) if self.serena_config.language_backend == LanguageBackend.JETBRAINS: