From abef88c4828cb330ef5dea49e1c3f5cb34682dcd Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 19 Jan 2026 18:33:43 +0100 Subject: [PATCH 1/2] SerenaConfig: Rename gui_log_window_enabled to gui_log_window This removes the discrepancy with the configuration file (where it was always gui_log_window) --- scripts/profile_tool_call.py | 2 +- src/serena/agent.py | 4 ++-- src/serena/cli.py | 2 +- src/serena/config/serena_config.py | 20 +++++++------------- src/serena/mcp.py | 2 +- test/serena/test_serena_agent.py | 2 +- test/serena/test_tool_parameter_types.py | 2 +- 7 files changed, 14 insertions(+), 20 deletions(-) diff --git a/scripts/profile_tool_call.py b/scripts/profile_tool_call.py index 8fe922a5..1ee06081 100644 --- a/scripts/profile_tool_call.py +++ b/scripts/profile_tool_call.py @@ -25,7 +25,7 @@ if __name__ == "__main__": serena_config = SerenaConfig.from_config_file() serena_config.log_level = logging.INFO - serena_config.gui_log_window_enabled = False + serena_config.gui_log_window = False serena_config.web_dashboard = False agent = SerenaAgent(str(project_path), serena_config=serena_config) diff --git a/src/serena/agent.py b/src/serena/agent.py index 6a5c7feb..dbeb27e8 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -216,7 +216,7 @@ class SerenaAgent: # open GUI log window if enabled self._gui_log_viewer: Optional["GuiLogViewer"] = None - if self.serena_config.gui_log_window_enabled: + if self.serena_config.gui_log_window: log.info("Opening GUI window") if platform.system() == "Darwin": log.warning("GUI log window is not supported on macOS") @@ -269,7 +269,7 @@ class SerenaAgent: 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 + and not self.serena_config.gui_log_window ): tool_inclusion_definitions.append(ToolInclusionDefinition(included_optional_tools=[OpenDashboardTool.get_name_from_cls()])) tool_inclusion_definitions.append(self.serena_config) diff --git a/src/serena/cli.py b/src/serena/cli.py index 40d0fc92..9ba0a267 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -746,7 +746,7 @@ class ProjectCommands(AutoRegisteringGroup): try: # Create SerenaAgent with dashboard disabled log.info("Creating SerenaAgent with disabled dashboard...") - config = SerenaConfig(gui_log_window_enabled=False, web_dashboard=False) + config = SerenaConfig(gui_log_window=False, web_dashboard=False) agent = SerenaAgent(project=project_path, serena_config=config) log.info("SerenaAgent created successfully") diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index f449a64f..130f23b4 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -403,8 +403,10 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): For testing purposes, it can also be instantiated directly with the desired parameters. """ + # *** fields that are mapped directly to/from the configuration file (DO NOT RENAME) *** + projects: list[RegisteredProject] = field(default_factory=list) - gui_log_window_enabled: bool = False + gui_log_window: bool = False log_level: int = logging.INFO trace_lsp_communication: bool = False web_dashboard: bool = True @@ -446,12 +448,6 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): # *** static members *** CONFIG_FILE = "serena_config.yml" - CONFIG_FILE_RENAMED_KEYS = { - "gui_log_window_enabled": "gui_log_window", - } - """ - maps attributes to the keys used in the configuration file, in case they differ - """ # *** methods *** @@ -539,11 +535,10 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): instance.projects.append(project) def get_value_or_default(field_name: str) -> Any: - key = cls.CONFIG_FILE_RENAMED_KEYS.get(field_name, field_name) nonlocal num_migrations - if key not in loaded_commented_yaml: + if field_name not in loaded_commented_yaml: num_migrations += 1 - return loaded_commented_yaml.get(key, get_dataclass_default(SerenaConfig, field_name)) + return loaded_commented_yaml.get(field_name, get_dataclass_default(SerenaConfig, field_name)) # determine language backend language_backend = get_dataclass_default(SerenaConfig, "language_backend") @@ -560,7 +555,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): instance.language_backend = language_backend # set other configuration parameters (primitive types) - instance.gui_log_window_enabled = get_value_or_default("gui_log_window_enabled") + instance.gui_log_window = get_value_or_default("gui_log_window") instance.web_dashboard_listen_address = get_value_or_default("web_dashboard_listen_address") instance.log_level = loaded_commented_yaml.get("log_level", loaded_commented_yaml.get("gui_log_level", logging.INFO)) instance.web_dashboard = get_value_or_default("web_dashboard") @@ -690,8 +685,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): field_name = field_info.name if field_name in ["projects", "language_backend"] or field_name.startswith("_"): continue - yaml_key = SerenaConfig.CONFIG_FILE_RENAMED_KEYS.get(field_name, field_name) - loaded_original_yaml[yaml_key] = getattr(self, field_name) + loaded_original_yaml[field_name] = getattr(self, field_name) # convert project objects into list of paths loaded_original_yaml["projects"] = sorted({str(project.project_root) for project in self.projects}) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 2e885cf9..b0367717 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -305,7 +305,7 @@ class SerenaMCPFactory: if enable_web_dashboard is not None: config.web_dashboard = enable_web_dashboard if enable_gui_log_window is not None: - config.gui_log_window_enabled = enable_gui_log_window + config.gui_log_window = enable_gui_log_window if open_web_dashboard is not None: config.web_dashboard_open_on_launch = open_web_dashboard if log_level is not None: diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index a030256f..6e7a0792 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -55,7 +55,7 @@ def serena_config(): ) test_projects.append(RegisteredProject.from_project_instance(project)) - config = SerenaConfig(gui_log_window_enabled=False, web_dashboard=False, log_level=logging.ERROR) + config = SerenaConfig(gui_log_window=False, web_dashboard=False, log_level=logging.ERROR) config.projects = test_projects return config diff --git a/test/serena/test_tool_parameter_types.py b/test/serena/test_tool_parameter_types.py index 4fdf0dc8..9899a5bb 100644 --- a/test/serena/test_tool_parameter_types.py +++ b/test/serena/test_tool_parameter_types.py @@ -13,7 +13,7 @@ def test_all_tool_parameters_have_type(context): For every tool exposed by Serena, ensure that the generated Open‑AI schema contains a ``type`` entry for each parameter. """ - cfg = SerenaConfig(gui_log_window_enabled=False, web_dashboard=False, log_level=logging.ERROR) + cfg = SerenaConfig(gui_log_window=False, web_dashboard=False, log_level=logging.ERROR) registry = ToolRegistry() cfg.included_optional_tools = tuple(registry.get_tool_names_optional()) factory = SerenaMCPFactory(context=context) From 0c43e60810940283ce4ee1ba1404eabcc030a865 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 19 Jan 2026 19:23:21 +0100 Subject: [PATCH 2/2] SerenaConfig: Unify handling of file-mapped configuration entries Fixes 'fixed_tools' not being loaded --- src/serena/config/serena_config.py | 69 +++++++++++++++--------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 130f23b4..7bfbb3e8 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -5,7 +5,7 @@ The Serena Model Context Protocol (MCP) Server import dataclasses import os import shutil -from collections.abc import Sequence +from collections.abc import Iterator, Sequence from copy import deepcopy from dataclasses import dataclass, field from datetime import datetime @@ -448,6 +448,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): # *** static members *** CONFIG_FILE = "serena_config.yml" + CONFIG_FIELDS_WITH_TYPE_CONVERSION = {"projects", "language_backend"} # *** methods *** @@ -455,6 +456,15 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): def config_file_path(self) -> str | None: return self._config_file_path + def _iter_config_file_mapped_fields_without_type_conversion(self) -> Iterator[str]: + for field_info in dataclasses.fields(self): + field_name = field_info.name + if field_name.startswith("_"): + continue + if field_name in self.CONFIG_FIELDS_WITH_TYPE_CONVERSION: + continue + yield field_name + def _tostring_includes(self) -> list[str]: return ["config_file_path"] @@ -509,14 +519,23 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): # create the configuration instance instance = cls(_loaded_commented_yaml=loaded_commented_yaml, _config_file_path=config_file_path) + num_migrations = 0 + + def get_value_or_default(field_name: str) -> Any: + nonlocal num_migrations + if field_name not in loaded_commented_yaml: + num_migrations += 1 + return loaded_commented_yaml.get(field_name, get_dataclass_default(SerenaConfig, field_name)) + + # transfer regular fields that do not require type conversion + for field_name in instance._iter_config_file_mapped_fields_without_type_conversion(): + assert hasattr(instance, field_name) + setattr(instance, field_name, get_value_or_default(field_name)) # read projects if "projects" not in loaded_commented_yaml: raise SerenaConfigError("`projects` key not found in Serena configuration. Please update your `serena_config.yml` file.") - - # load list of known projects instance.projects = [] - num_migrations = 0 for path in loaded_commented_yaml["projects"]: path = Path(path).resolve() if not path.exists() or (path.is_dir() and not (path / ProjectConfig.rel_path_to_project_yml()).exists()): @@ -534,12 +553,6 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): ) instance.projects.append(project) - def get_value_or_default(field_name: str) -> Any: - nonlocal num_migrations - if field_name not in loaded_commented_yaml: - num_migrations += 1 - return loaded_commented_yaml.get(field_name, get_dataclass_default(SerenaConfig, field_name)) - # determine language backend language_backend = get_dataclass_default(SerenaConfig, "language_backend") if "language_backend" in loaded_commented_yaml: @@ -554,20 +567,12 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): del loaded_commented_yaml["jetbrains"] instance.language_backend = language_backend - # set other configuration parameters (primitive types) - instance.gui_log_window = get_value_or_default("gui_log_window") - instance.web_dashboard_listen_address = get_value_or_default("web_dashboard_listen_address") - instance.log_level = loaded_commented_yaml.get("log_level", loaded_commented_yaml.get("gui_log_level", logging.INFO)) - instance.web_dashboard = get_value_or_default("web_dashboard") - instance.web_dashboard_open_on_launch = get_value_or_default("web_dashboard_open_on_launch") - instance.jetbrains_plugin_server_address = get_value_or_default("jetbrains_plugin_server_address") - instance.tool_timeout = get_value_or_default("tool_timeout") - instance.trace_lsp_communication = get_value_or_default("trace_lsp_communication") - instance.excluded_tools = get_value_or_default("excluded_tools") - instance.included_optional_tools = get_value_or_default("included_optional_tools") - instance.token_count_estimator = get_value_or_default("token_count_estimator") - instance.default_max_tool_answer_chars = get_value_or_default("default_max_tool_answer_chars") - instance.ls_specific_settings = get_value_or_default("ls_specific_settings") + # migrate deprecated "gui_log_level" field if necessary + if "gui_log_level" in loaded_commented_yaml: + num_migrations += 1 + if "log_level" not in loaded_commented_yaml: + instance.log_level = loaded_commented_yaml["gui_log_level"] + del loaded_commented_yaml["gui_log_level"] # re-save the configuration file if any migrations were performed if num_migrations > 0: @@ -677,23 +682,19 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): assert self._loaded_commented_yaml is not None, "Cannot save configuration without loaded YAML" - loaded_original_yaml = deepcopy(self._loaded_commented_yaml) + commented_yaml = deepcopy(self._loaded_commented_yaml) # update fields with current values - # iterate over all dataclass fields - for field_info in dataclasses.fields(self): - field_name = field_info.name - if field_name in ["projects", "language_backend"] or field_name.startswith("_"): - continue - loaded_original_yaml[field_name] = getattr(self, field_name) + for field_name in self._iter_config_file_mapped_fields_without_type_conversion(): + commented_yaml[field_name] = getattr(self, field_name) # convert project objects into list of paths - loaded_original_yaml["projects"] = sorted({str(project.project_root) for project in self.projects}) + commented_yaml["projects"] = sorted({str(project.project_root) for project in self.projects}) # convert language backend to string - loaded_original_yaml["language_backend"] = self.language_backend.value + commented_yaml["language_backend"] = self.language_backend.value - save_yaml(self.config_file_path, loaded_original_yaml, preserve_comments=True) + save_yaml(self.config_file_path, commented_yaml, preserve_comments=True) def propagate_settings(self) -> None: """