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..eeeab1ca 100644 --- a/docs/02-usage/060_dashboard.md +++ b/docs/02-usage/060_dashboard.md @@ -15,8 +15,21 @@ 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 + +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: +* 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 02d44079..ae39e8cd 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 @@ -24,7 +23,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 @@ -189,6 +188,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: @@ -247,8 +249,21 @@ 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 - tool_inclusion_definitions: list[ToolInclusionDefinition] = [self.serena_config, self._context] + # determined by the + # * dashboard availability/opening on launch + # * Serena config, + # * the context (which is fixed for the session) + # * single-project mode reductions (if applicable) + # * JetBrains mode + 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: @@ -292,16 +307,10 @@ 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(): - 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) @@ -393,17 +402,34 @@ 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) + 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 - # open the dashboard URL in the default web browser - webbrowser.open(url) + 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: + raise Exception("Dashboard is not running.") + + if not system_has_usable_display(): + log.warning("Not opening the Serena web dashboard because no usable display was detected.") + return False + + # 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: """ diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index d197991d..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,17 +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). -# 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) diff --git a/src/serena/tools/config_tools.py b/src/serena/tools/config_tools.py index 45ca01c7..7de51e59 100644 --- a/src/serena/tools/config_tools.py +++ b/src/serena/tools/config_tools.py @@ -2,6 +2,22 @@ from serena.config.context_mode import SerenaAgentMode from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional +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. + """ + + def apply(self) -> str: + """ + Opens the Serena web dashboard in the default web browser. + """ + 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): """ Activates a project based on the project name or path.