From 3912a07fcbfa24794c684d144eacd9877b6ec5fb Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 25 Mar 2026 17:01:23 +0100 Subject: [PATCH 001/190] Conftest: added get_pytest_markers --- test/conftest.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/conftest.py b/test/conftest.py index f8981251..d02c22be 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import Any import pytest +from _pytest.mark import Mark, MarkDecorator from sensai.util.logging import configure from serena.config.serena_config import SerenaConfig, SerenaPaths @@ -243,6 +244,39 @@ Flag indicating whether the tests are running in the GitHub CI environment. is_windows = platform.system() == "Windows" +_LANGUAGE_PYTEST_MARKERS: dict[Language, list[MarkDecorator | Mark]] = { + Language.CLOJURE: [ + pytest.mark.clojure, + pytest.mark.skipif(not is_clojure_cli_available(), reason="clojure CLI is not installed"), + ], + Language.CPP: [pytest.mark.cpp], + Language.CPP_CCLS: [pytest.mark.cpp], + Language.CSHARP: [pytest.mark.csharp], + Language.FSHARP: [pytest.mark.fsharp], + Language.GO: [pytest.mark.go], + Language.JAVA: [pytest.mark.java], + Language.KOTLIN: [pytest.mark.kotlin, pytest.mark.skipif(is_ci, reason="Kotlin LSP JVM crashes on restart in CI")], + Language.LEAN4: [pytest.mark.lean4, pytest.mark.skipif(_sh.which("lean") is None, reason="Lean is not installed")], + Language.PHP: [pytest.mark.php], + Language.PHP_PHPACTOR: [pytest.mark.php], + Language.POWERSHELL: [pytest.mark.powershell], + Language.PYTHON: [pytest.mark.python], + Language.PYTHON_JEDI: [pytest.mark.python], + Language.PYTHON_TY: [pytest.mark.python], + Language.RUST: [pytest.mark.rust], + Language.TYPESCRIPT: [pytest.mark.typescript], +} + + +def get_pytest_markers(language: Language) -> list[MarkDecorator | Mark]: + """Pytest markers for a language. + + The returned list contains the primary language marker and any + environment-dependent skip markers shared across the test suite. + """ + return _LANGUAGE_PYTEST_MARKERS[language] + + def _determine_disabled_languages() -> list[Language]: """ Determine which language tests should be disabled (based on the environment) From 20e14fa158be6ef4d1cd180beecf8e52f291590c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 5 Mar 2026 16:35:27 +0100 Subject: [PATCH 002/190] Feature: move symbol WIP --- .../jetbrains/jetbrains_plugin_client.py | 15 +++++++++ src/serena/tools/jetbrains_tools.py | 32 ++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/serena/jetbrains/jetbrains_plugin_client.py b/src/serena/jetbrains/jetbrains_plugin_client.py index f237a079..56b08758 100644 --- a/src/serena/jetbrains/jetbrains_plugin_client.py +++ b/src/serena/jetbrains/jetbrains_plugin_client.py @@ -425,6 +425,21 @@ class JetBrainsPluginClient(ToStringMixin): self._postprocess_symbol_collection_response(symbol_collection) return symbol_collection + def move_symbol( + self, + name_path: str | None, + relative_path: str | None, + target_parent_name_path: str | None, + target_relative_path: str | None, + ) -> dict[str, Any]: + request_data = { + "namePath": name_path, + "relativePath": relative_path, + "targetParentNamePath": target_parent_name_path, + "targetRelativePath": target_relative_path, + } + return self._make_request("POST", "/moveSymbol", request_data) + def find_references(self, name_path: str, relative_path: str, include_quick_info: bool) -> jb.SymbolCollectionResponse: """ Finds references to a symbol. diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index bc0b99bc..68323872 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -6,7 +6,7 @@ import serena.jetbrains.jetbrains_types as jb from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient from serena.jetbrains.jetbrains_types import SymbolDTO from serena.symbol import JetBrainsSymbolDictGrouper -from serena.tools import Tool, ToolMarkerOptional, ToolMarkerSymbolicRead +from serena.tools import Tool, ToolMarkerOptional, ToolMarkerSymbolicEdit, ToolMarkerSymbolicRead log = logging.getLogger(__name__) @@ -111,6 +111,36 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): return self._limit_length(result, max_answer_chars, shortened_result_factories=[create_shortened_result]) +class JetBrainsMoveSymbolTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): + def apply( + self, + name_path: str | None = None, + relative_path: str | None = None, + target_parent_name_path: str | None = None, + target_relative_path: str | None = None, + ) -> str: + """ + Moves a symbol to a different location (file or parent symbol) using the JetBrains refactoring engine. + References to the symbol are automatically updated. + + :param name_path: the name path of the symbol to move (e.g. "MyClass/my_method"). + :param relative_path: the relative path to the file containing the symbol to move. + :param target_parent_name_path: the name path of the target parent symbol (e.g. "TargetClass"). + If None, the symbol is moved to the top level of the target file. + :param target_relative_path: the relative path to the target file. If None, the symbol + is moved within the same file. + :return: JSON string with the result of the operation. + """ + with JetBrainsPluginClient.from_project(self.project) as client: + response_dict = client.move_symbol( + name_path=name_path, + relative_path=relative_path, + target_parent_name_path=target_parent_name_path, + target_relative_path=target_relative_path, + ) + return self._to_json(response_dict) + + class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): """ Finds symbols that reference the given symbol using the JetBrains backend From 2aa6603dbba0fbfeb6e1bf236cbea8971bd3d443 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 22 Mar 2026 12:04:34 +0100 Subject: [PATCH 003/190] Added safe_delete and inline_symbol tools (JB backend) --- scripts/demo_run_tools.py | 15 ++++- .../jetbrains/jetbrains_plugin_client.py | 36 +++++++++++ .../config/internal_modes/jetbrains.yml | 3 + src/serena/tools/jetbrains_tools.py | 59 +++++++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) diff --git a/scripts/demo_run_tools.py b/scripts/demo_run_tools.py index bad7ed60..5d2b3358 100644 --- a/scripts/demo_run_tools.py +++ b/scripts/demo_run_tools.py @@ -4,6 +4,7 @@ for testing or development. Here the tools will be operation the serena repo its """ import json +from pathlib import Path from pprint import pprint from serena.agent import SerenaAgent @@ -14,13 +15,17 @@ from serena.tools import ( FindReferencingSymbolsTool, JetBrainsFindSymbolTool, JetBrainsGetSymbolsOverviewTool, + JetBrainsInlineSymbol, + JetBrainsSafeDeleteTool, SearchForPatternTool, ) if __name__ == "__main__": serena_config = SerenaConfig.from_config_file() serena_config.web_dashboard = False - agent = SerenaAgent(project=REPO_ROOT, serena_config=serena_config) + # project = Path(REPO_ROOT).parent / "serena-jetbrains-plugin-copy" + project = Path(REPO_ROOT) / "test/resources/repos/python/test_repo" + agent = SerenaAgent(project=str(project), serena_config=serena_config) # apply a tool find_symbol_tool = agent.get_tool(JetBrainsFindSymbolTool) @@ -28,9 +33,15 @@ if __name__ == "__main__": find_file_tool = agent.get_tool(FindFileTool) search_pattern_tool = agent.get_tool(SearchForPatternTool) overview_tool = agent.get_tool(JetBrainsGetSymbolsOverviewTool) + safe_delete_tool = agent.get_tool(JetBrainsSafeDeleteTool) + inline_symbol = agent.get_tool(JetBrainsInlineSymbol) result = agent.execute_task( - lambda: find_symbol_tool.apply("SerenaAgent/get_tool_description_override"), + lambda: inline_symbol.apply( + relative_path="test_repo/nested.py", + name_path="OuterClass/NestedClass", + keep_definition=True, + ) ) pprint(json.loads(result)) # input("Press Enter to continue...") diff --git a/src/serena/jetbrains/jetbrains_plugin_client.py b/src/serena/jetbrains/jetbrains_plugin_client.py index 56b08758..b79b226a 100644 --- a/src/serena/jetbrains/jetbrains_plugin_client.py +++ b/src/serena/jetbrains/jetbrains_plugin_client.py @@ -519,6 +519,42 @@ class JetBrainsPluginClient(ToStringMixin): } return cast(jb.TypeHierarchyResponse, self._make_request("POST", "/getSubtypes", request_data)) + def safe_delete(self, name_path: str | None, relative_path: str, delete_even_if_used: bool, propagate: bool) -> dict[str, Any]: + """ + Safely deletes a symbol, checking for usages first. + + :param name_path: the name path of the symbol to delete + :param relative_path: the relative path to the file containing the symbol + :param delete_even_if_used: if True, delete the symbol even if it has usages + """ + request_data = { + "namePath": name_path, + "relativePath": relative_path, + "deleteEvenIfUsed": delete_even_if_used, + "propagate": propagate, + } + return self._make_request("POST", "/safeDelete", request_data) + + def inline_symbol( + self, + name_path: str, + relative_path: str, + keep_definition: bool, + ) -> dict[str, Any]: + """ + Inlines a method, replacing all call sites with the method body. + + :param name_path: the name path of the method to inline + :param relative_path: the relative path to the file containing the method + :param keep_definition: if True, keep the original method definition after inlining + """ + request_data = { + "namePath": name_path, + "relativePath": relative_path, + "keepDefinition": keep_definition, + } + return self._make_request("POST", "/inlineSymbol", request_data) + def rename_symbol( self, name_path: str, relative_path: str, new_name: str, rename_in_comments: bool, rename_in_text_occurrences: bool ) -> None: diff --git a/src/serena/resources/config/internal_modes/jetbrains.yml b/src/serena/resources/config/internal_modes/jetbrains.yml index 863f3beb..090c6b8f 100644 --- a/src/serena/resources/config/internal_modes/jetbrains.yml +++ b/src/serena/resources/config/internal_modes/jetbrains.yml @@ -13,4 +13,7 @@ included_optional_tools: - jet_brains_find_symbol - jet_brains_find_referencing_symbols - jet_brains_get_symbols_overview + - jet_brains_move_symbol - jet_brains_type_hierarchy + - jet_brains_safe_delete + - jet_brains_inline_symbol diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 68323872..f96bcd67 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -141,6 +141,65 @@ class JetBrainsMoveSymbolTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): return self._to_json(response_dict) +class JetBrainsSafeDeleteTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): + def apply( + self, + relative_path: str, + name_path: str | None = None, + delete_even_if_used: bool = False, + propagate: bool = False, + ) -> str: + """ + Safely deletes a symbol, checking for usages first. It is also + possible to request deleting of usages and cleaning up of unused code. + + :param relative_path: the relative path to the file containing the symbol to delete. + :param name_path: the name path of the symbol to delete. + A name path identifies a symbol within a source file, e.g. "MyClass/my_method". + :param delete_even_if_used: whether to force deletion even if the symbol still has usages. + Default is False (safe mode: will report usages instead of deleting). + :param propagate: whether to propagate the deletion to usages of the symbol and also + remove symbols that become unused after the deletion. Default is False. + :return: JSON string with the result of the operation. + """ + with JetBrainsPluginClient.from_project(self.project) as client: + response_dict = client.safe_delete( + name_path=name_path, + relative_path=relative_path, + delete_even_if_used=delete_even_if_used, + propagate=propagate, + ) + return self._to_json(response_dict) + + +class JetBrainsInlineSymbol(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): + def apply( + self, + name_path: str, + relative_path: str, + keep_definition: bool = False, + ) -> str: + """ + Inlines a symbol (usually method, but also classes may be amenable to inlining, + which turns invocation into anonymous class creation), + replacing all call sites with the method's body. + An error is raised when requested on a symbol that cannot be inlined. + + :param name_path: the name path of the method to inline. + :param relative_path: the relative path to the file containing the method to inline. + :param keep_definition: whether to keep the original method definition after inlining all call sites. + May be ignored in some cases (e.g., when inlining a class).. + :return: JSON string with the result of the operation. + """ + with JetBrainsPluginClient.from_project(self.project) as client: + response_dict = client.inline_symbol( + name_path=name_path, + relative_path=relative_path, + keep_definition=keep_definition, + ) + return self._to_json(response_dict) + + class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): """ Finds symbols that reference the given symbol using the JetBrains backend From 9735733802fba8d4c77bcc77acf11065bf4f5e2f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 24 Mar 2026 11:53:51 +0100 Subject: [PATCH 004/190] Added SafeDeleteSymbol in LS world --- .../config/internal_modes/jetbrains.yml | 1 + src/serena/tools/symbol_tools.py | 41 ++++++++ src/serena/tools/tools_base.py | 13 ++- .../repos/typescript/test_repo/index.ts | 4 + .../__snapshots__/test_symbol_editing.ambr | 24 +++++ test/serena/test_serena_agent.py | 98 ++++++++++++++++++- 6 files changed, 177 insertions(+), 4 deletions(-) diff --git a/src/serena/resources/config/internal_modes/jetbrains.yml b/src/serena/resources/config/internal_modes/jetbrains.yml index 090c6b8f..f9e60a4b 100644 --- a/src/serena/resources/config/internal_modes/jetbrains.yml +++ b/src/serena/resources/config/internal_modes/jetbrains.yml @@ -9,6 +9,7 @@ excluded_tools: - find_referencing_symbols - get_symbols_overview - restart_language_server + - safe_delete_symbol included_optional_tools: - jet_brains_find_symbol - jet_brains_find_referencing_symbols diff --git a/src/serena/tools/symbol_tools.py b/src/serena/tools/symbol_tools.py index 2df83bc1..2cf5c178 100644 --- a/src/serena/tools/symbol_tools.py +++ b/src/serena/tools/symbol_tools.py @@ -430,3 +430,44 @@ class RenameSymbolTool(Tool, ToolMarkerSymbolicEdit): code_editor = self.create_code_editor() status_message = code_editor.rename_symbol(name_path, relative_file_path=relative_path, new_name=new_name) return status_message + + +class SafeDeleteSymbol(Tool, ToolMarkerSymbolicEdit): + def apply( + self, + name_path_pattern: str, + relative_path: str, + ) -> str: + """ + Deletes the symbol if it is safe to do so (i.e., if there are no references to it) + or returns a list of references to it. + + :param name_path_pattern: name path of the symbol to delete (definitions in the `find_symbol` tool apply) + :param relative_path: the relative path to the file containing the symbol to delete + """ + ls_symbol_retriever = self.create_language_server_symbol_retriever() + symbol = ls_symbol_retriever.find_unique(name_path_pattern, substring_matching=False, within_relative_path=relative_path) + symbol_rel_path = symbol.relative_path + assert symbol_rel_path is not None, f"Symbol {name_path_pattern} has no relative path, this is likely a bug." + assert symbol_rel_path == relative_path, f"Symbol {name_path_pattern} is not in the expected relative path {relative_path}." + symbol_name_path = symbol.get_name_path() + + symbol_line = symbol.line + symbol_col = symbol.column + assert symbol_line is not None and symbol_col is not None, ( + f"Symbol {name_path_pattern} has no identifier position, this is likely a bug." + ) + lang_server = ls_symbol_retriever.get_language_server(symbol_rel_path) + references_locations = lang_server.request_references(symbol_rel_path, symbol_line, symbol_col) + file_to_lines: dict[str, list[int]] = defaultdict(list) + if references_locations: + for ref_loc in references_locations: + ref_relative_path = ref_loc.get("relativePath") + if ref_relative_path is None: + continue + file_to_lines[ref_relative_path].append(ref_loc["range"]["start"]["line"]) + if file_to_lines: + return f"Cannot delete, the symbol {symbol_name_path} is referenced in: {self._to_json(file_to_lines)}" + code_editor = self.create_ls_code_editor() + code_editor.delete_symbol(symbol_name_path, relative_file_path=symbol_rel_path) + return SUCCESS_RESULT diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index 18eed68f..200c2ae3 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -21,7 +21,7 @@ from solidlsp.ls_exceptions import SolidLSPException if TYPE_CHECKING: from serena.agent import SerenaAgent - from serena.code_editor import CodeEditor + from serena.code_editor import CodeEditor, LanguageServerCodeEditor from serena.symbol import LanguageServerSymbolRetriever log = logging.getLogger(__name__) @@ -58,16 +58,23 @@ class Component(ABC): return self.agent.get_active_project_or_raise() def create_code_editor(self) -> "CodeEditor": - from ..code_editor import JetBrainsCodeEditor, LanguageServerCodeEditor + from ..code_editor import JetBrainsCodeEditor match self.agent.get_language_backend(): case LanguageBackend.LSP: - return LanguageServerCodeEditor(self.create_language_server_symbol_retriever()) + return self.create_ls_code_editor() case LanguageBackend.JETBRAINS: return JetBrainsCodeEditor(project=self.project) case _: raise ValueError + def create_ls_code_editor(self) -> "LanguageServerCodeEditor": + from ..code_editor import LanguageServerCodeEditor + + if not self.agent.is_using_language_server(): + raise Exception("Cannot create LanguageServerCodeEditor; agent is not in language server mode.") + return LanguageServerCodeEditor(self.create_language_server_symbol_retriever()) + class ToolMarker: """ diff --git a/test/resources/repos/typescript/test_repo/index.ts b/test/resources/repos/typescript/test_repo/index.ts index 7b8bd1da..724a67c6 100644 --- a/test/resources/repos/typescript/test_repo/index.ts +++ b/test/resources/repos/typescript/test_repo/index.ts @@ -14,3 +14,7 @@ export function helperFunction() { } helperFunction(); + +export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; +} diff --git a/test/serena/__snapshots__/test_symbol_editing.ambr b/test/serena/__snapshots__/test_symbol_editing.ambr index 347862ca..f614627e 100644 --- a/test/serena/__snapshots__/test_symbol_editing.ambr +++ b/test/serena/__snapshots__/test_symbol_editing.ambr @@ -76,6 +76,10 @@ helperFunction(); + export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; + } + ''' # --- # name: test_delete_symbol_vue[test_case0] @@ -736,6 +740,10 @@ helperFunction(); + export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; + } + ''' # --- # name: test_insert_in_rel_to_symbol[test_case2-before] @@ -761,6 +769,10 @@ helperFunction(); + export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; + } + ''' # --- # name: test_insert_in_rel_to_symbol[test_case3-after] @@ -786,6 +798,10 @@ helperFunction(); + export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; + } + ''' # --- # name: test_insert_in_rel_to_symbol[test_case3-before] @@ -811,6 +827,10 @@ helperFunction(); + export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; + } + ''' # --- # name: test_insert_in_rel_to_symbol_vue[test_case0-after] @@ -1888,6 +1908,10 @@ helperFunction(); + export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; + } + ''' # --- # name: test_replace_body_vue[test_case0] diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 0b40ab28..e9dc6b2e 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -12,7 +12,14 @@ import pytest from serena.agent import SerenaAgent from serena.config.serena_config import ProjectConfig, RegisteredProject, SerenaConfig from serena.project import Project -from serena.tools import SUCCESS_RESULT, FindReferencingSymbolsTool, FindSymbolTool, ReplaceContentTool, ReplaceSymbolBodyTool +from serena.tools import ( + SUCCESS_RESULT, + FindReferencingSymbolsTool, + FindSymbolTool, + ReplaceContentTool, + ReplaceSymbolBodyTool, + SafeDeleteSymbol, +) from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind from test.conftest import get_repo_path, is_ci, language_tests_enabled @@ -589,3 +596,92 @@ class TestSerenaAgent: relative_path="ws_manager.js", mode="regex", ) + + @pytest.mark.parametrize( + "serena_agent,name_path,relative_path", + [ + pytest.param( + Language.PYTHON, + "User", + os.path.join("test_repo", "models.py"), + marks=pytest.mark.python, + ), + pytest.param( + Language.JAVA, + "Model", + os.path.join("src", "main", "java", "test_repo", "Model.java"), + marks=pytest.mark.java, + ), + pytest.param( + Language.KOTLIN, + "Model", + os.path.join("src", "main", "kotlin", "test_repo", "Model.kt"), + marks=[pytest.mark.kotlin] + ([pytest.mark.skip(reason="Kotlin LSP JVM crashes on restart in CI")] if is_ci else []), + ), + pytest.param( + Language.TYPESCRIPT, + "helperFunction", + "index.ts", + marks=pytest.mark.typescript, + ), + ], + indirect=["serena_agent"], + ) + def test_safe_delete_symbol_blocked_by_references(self, serena_agent: SerenaAgent, name_path: str, relative_path: str): + """ + Tests that SafeDeleteSymbol refuses to delete a symbol that is referenced elsewhere + and returns a message listing the referencing files. + """ + # wrap in modification context as a safety net: if the tool has a bug and deletes anyway, + # the file will be restored, preventing corruption of test resources + with project_file_modification_context(serena_agent, relative_path): + safe_delete_tool = serena_agent.get_tool(SafeDeleteSymbol) + result = safe_delete_tool.apply(name_path_pattern=name_path, relative_path=relative_path) + assert "Cannot delete" in result, f"Expected deletion to be blocked due to existing references, but got: {result}" + assert "referenced in" in result, f"Expected reference information in result, but got: {result}" + + @pytest.mark.parametrize( + "serena_agent,name_path,relative_path", + [ + pytest.param( + Language.PYTHON, + "Timer", + os.path.join("test_repo", "utils.py"), + marks=pytest.mark.python, + ), + pytest.param( + Language.JAVA, + "ModelUser", + os.path.join("src", "main", "java", "test_repo", "ModelUser.java"), + marks=pytest.mark.java, + ), + pytest.param( + Language.KOTLIN, + "ModelUser", + os.path.join("src", "main", "kotlin", "test_repo", "ModelUser.kt"), + marks=[pytest.mark.kotlin] + ([pytest.mark.skip(reason="Kotlin LSP JVM crashes on restart in CI")] if is_ci else []), + ), + pytest.param( + Language.TYPESCRIPT, + "unusedStandaloneFunction", + "index.ts", + marks=pytest.mark.typescript, + ), + ], + indirect=["serena_agent"], + ) + def test_safe_delete_symbol_succeeds_when_no_references(self, serena_agent: SerenaAgent, name_path: str, relative_path: str): + """ + Tests that SafeDeleteSymbol successfully deletes a symbol that has no references + and that the symbol is actually removed from the file. + """ + with project_file_modification_context(serena_agent, relative_path): + safe_delete_tool = serena_agent.get_tool(SafeDeleteSymbol) + result = safe_delete_tool.apply(name_path_pattern=name_path, relative_path=relative_path) + assert result == SUCCESS_RESULT, f"Expected successful deletion, but got: {result}" + + # verify the symbol was actually removed from the file + file_content = read_project_file(serena_agent.get_active_project(), relative_path) + assert name_path not in file_content, ( + f"Expected symbol {name_path} to be removed from {relative_path}, but it still appears in the file content" + ) From 6ef9158ff5bb23ddc694077ec040ef46eed8a6b4 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 31 Mar 2026 01:07:38 +0200 Subject: [PATCH 005/190] Add JetBrainsFindDeclarationTool --- src/serena/code_editor.py | 10 +++++ .../jetbrains/jetbrains_plugin_client.py | 23 +++++++++++ src/serena/tools/jetbrains_tools.py | 30 ++++++++++++++ src/serena/util/text_utils.py | 41 +++++++++++++++++++ 4 files changed, 104 insertions(+) diff --git a/src/serena/code_editor.py b/src/serena/code_editor.py index 430c8c2a..dfe4fe47 100644 --- a/src/serena/code_editor.py +++ b/src/serena/code_editor.py @@ -57,6 +57,16 @@ class CodeEditor(Generic[TSymbol], ABC): """ raise NotImplementedError("This method must be overridden for each subclass") + def read_file(self, relative_path: str) -> str: + """ + Reads the content of a file. + + :param relative_path: the relative path of the file to read + :return: the content of the file + """ + with self._open_file_context(relative_path) as file: + return file.get_contents() + @contextmanager def edited_file_context(self, relative_path: str) -> Iterator["CodeEditor.EditedFile"]: """ diff --git a/src/serena/jetbrains/jetbrains_plugin_client.py b/src/serena/jetbrains/jetbrains_plugin_client.py index b79b226a..52d21516 100644 --- a/src/serena/jetbrains/jetbrains_plugin_client.py +++ b/src/serena/jetbrains/jetbrains_plugin_client.py @@ -587,6 +587,29 @@ class JetBrainsPluginClient(ToStringMixin): } self._make_request("POST", "/refreshFile", request_data) + def find_declaration( + self, relative_path: str, line: int, col: int, include_body: bool, include_quick_info: bool + ) -> jb.SymbolCollectionResponse: + """ + Finds the declaration of the symbol at the given location. + + :param relative_path: the relative path to the file + :param line: the line number (0-based) + :param col: the column number (0-based) + :param include_body: whether to include the symbol body + :param include_quick_info: whether to include quick info about the symbol + """ + request_data = { + "relativePath": relative_path, + "line": line, + "col": col, + "includeBody": include_body, + "includeQuickInfo": include_quick_info, + } + symbol_collection = cast(jb.SymbolCollectionResponse, self._make_request("POST", "/findDeclaration", request_data)) + self._postprocess_symbol_collection_response(symbol_collection) + return symbol_collection + def close(self) -> None: self._session.close() diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index f96bcd67..7a943922 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -7,6 +7,7 @@ from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient from serena.jetbrains.jetbrains_types import SymbolDTO from serena.symbol import JetBrainsSymbolDictGrouper from serena.tools import Tool, ToolMarkerOptional, ToolMarkerSymbolicEdit, ToolMarkerSymbolicRead +from serena.util.text_utils import find_text_coordinates log = logging.getLogger(__name__) @@ -416,3 +417,32 @@ class JetBrainsTypeHierarchyTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptiona result = self._to_json(result_dict) return self._limit_length(result, max_answer_chars) + + +class JetBrainsFindDeclarationTool(Tool, ToolMarkerSymbolicRead): + """ + Finds the declaration of a symbol using the JetBrains backend + """ + + def apply(self, relative_path: str, regex: str, include_body: bool = False) -> str: + r""" + Finds the declaration of a symbol. + + :param relative_path: the relative path to the source file containing the symbol for which to find the declaration. + :param regex: a regular expression with one group, where the group matches the symbol for which to perform the lookup. + For example, to find the declaration of the `process` method in a call like `obj.process()`, + pass an expression like "obj\.(process)\(\)". + Provide as much context as necessary to render the regex unambiguous. + Uses Python syntax with MULTILINE and DOTALL flags enabled. + :param include_body: whether to include the symbol's body in the result. Default False. + """ + editor = self.create_code_editor() + content = editor.read_file(relative_path) + coords = find_text_coordinates(content, regex, require_unique=True) + assert coords is not None + with JetBrainsPluginClient.from_project(self.project) as client: + symbol_collection = client.find_declaration( + relative_path=relative_path, line=coords.line, col=coords.col, include_quick_info=False, include_body=include_body + ) + result = self._to_json(symbol_collection) + return result diff --git a/src/serena/util/text_utils.py b/src/serena/util/text_utils.py index e3c63e77..20c7b0b7 100644 --- a/src/serena/util/text_utils.py +++ b/src/serena/util/text_utils.py @@ -11,6 +11,7 @@ from bs4 import BeautifulSoup from joblib import Parallel, delayed from serena.constants import DEFAULT_SOURCE_FILE_ENCODING +from solidlsp.ls_utils import TextUtils log = logging.getLogger(__name__) @@ -523,3 +524,43 @@ class ContentReplacer: "Please revise the expression to be more specific or enable allow_multiple_occurrences if this is expected." ) return updated_content + + +@dataclass +class TextCoords: + line: int + """ + 0-based line number + """ + col: int + """ + 0-based column number + """ + + +def find_text_coordinates(content: str, regex: str, require_unique: bool = False) -> TextCoords | None: + """ + Finds the line and column number of the first match of a regex pattern in the given content. + + :param content: the text content to search through + :param regex: the regular expression pattern to search for; it must match part of a single line, + and contain exactly one group that captures the position of interest (e.g., the exact variable name to find the coordinates of) + :param require_unique: if True, raises an error if not exactly one match is found; + if False, returns None if no match is found, and returns the coordinates of the first match if multiple matches are found + :return: the coordinates of the match or None + """ + pattern = re.compile(regex, flags=re.MULTILINE | re.DOTALL) + matches = list(pattern.finditer(content)) + if len(matches) == 0: + if require_unique: + raise ValueError(f"No match found for regex: {regex}") + return None + else: + if require_unique and len(matches) > 1: + raise ValueError(f"Match must be unique; found {len(matches)} matches for regex: {regex}") + match = matches[0] + if len(match.groups()) != 1: + raise ValueError(f"Regex must contain exactly one group to capture the position, but found {len(match.groups())} groups.") + index_in_content = match.start(1) + line, col = TextUtils.get_line_col_from_index(content, index_in_content) + return TextCoords(line, col) From 213086b1626974a8921f3a71e801f3da124a316e Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 31 Mar 2026 01:22:00 +0200 Subject: [PATCH 006/190] Add JetBrainsFindImplementationsTool --- .../jetbrains/jetbrains_plugin_client.py | 17 ++++++++++++++ src/serena/tools/jetbrains_tools.py | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/serena/jetbrains/jetbrains_plugin_client.py b/src/serena/jetbrains/jetbrains_plugin_client.py index 52d21516..cd6b3b8b 100644 --- a/src/serena/jetbrains/jetbrains_plugin_client.py +++ b/src/serena/jetbrains/jetbrains_plugin_client.py @@ -610,6 +610,23 @@ class JetBrainsPluginClient(ToStringMixin): self._postprocess_symbol_collection_response(symbol_collection) return symbol_collection + def find_implementations(self, relative_path: str, name_path: str, include_quick_info: bool) -> jb.SymbolCollectionResponse: + """ + Finds the implementations of a symbol. + + :param relative_path: the relative path to the file containing the symbol + :param name_path: the name path of the symbol + :param include_quick_info: whether to include quick info about the symbol + """ + request_data = { + "relativePath": relative_path, + "namePath": name_path, + "includeQuickInfo": include_quick_info, + } + symbol_collection = cast(jb.SymbolCollectionResponse, self._make_request("POST", "/findImplementations", request_data)) + self._postprocess_symbol_collection_response(symbol_collection) + return symbol_collection + def close(self) -> None: self._session.close() diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 7a943922..33b4ac66 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -446,3 +446,25 @@ class JetBrainsFindDeclarationTool(Tool, ToolMarkerSymbolicRead): ) result = self._to_json(symbol_collection) return result + + +class JetBrainsFindImplementationsTool(Tool, ToolMarkerSymbolicRead): + """ + Finds the implementations of a symbol using the JetBrains backend + """ + + def apply(self, relative_path: str, name_path: str) -> str: + """ + Finds the implementations of a symbol. + + :param relative_path: the relative path to the source file containing the symbol for which to find implementations. + :param name_path: name path of the symbol for which to find implementations + """ + with JetBrainsPluginClient.from_project(self.project) as client: + symbol_collection = client.find_implementations( + relative_path=relative_path, + name_path=name_path, + include_quick_info=False, + ) + result = self._to_json(symbol_collection) + return result From 10307f045fa5e6ce2c5aff191411d4eec65a3990 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 31 Mar 2026 15:54:11 +0200 Subject: [PATCH 007/190] Permit rename of dirs/files in JB world, WIP --- src/serena/code_editor.py | 23 +++++++++++-------- .../jetbrains/jetbrains_plugin_client.py | 2 +- src/serena/tools/symbol_tools.py | 11 +++++---- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/serena/code_editor.py b/src/serena/code_editor.py index dfe4fe47..202efe0f 100644 --- a/src/serena/code_editor.py +++ b/src/serena/code_editor.py @@ -230,13 +230,14 @@ class CodeEditor(Generic[TSymbol], ABC): edited_file.delete_text_between_positions(start_pos, end_pos) @abstractmethod - def rename_symbol(self, name_path: str, relative_file_path: str, new_name: str) -> str: + def rename_symbol(self, name_path: str | None, relative_path: str, new_name: str) -> str: """ - Renames the symbol with the given name throughout the codebase. + Renames a symbol, file, or directory throughout the codebase. :param name_path: the name path of the symbol to rename - :param relative_file_path: the relative path of the file containing the symbol - :param new_name: the new name for the symbol + :param relative_path: if `name_path` is passed, the relative path of the file containing the symbol. + Otherwise, the path to the directory or file to rename. + :param new_name: the new name :return: a status message """ @@ -347,8 +348,10 @@ class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]): operation.apply() return len(operations) - def rename_symbol(self, name_path: str, relative_file_path: str, new_name: str) -> str: - symbol = self._find_unique_symbol(name_path, relative_file_path) + def rename_symbol(self, name_path: str | None, relative_path: str, new_name: str) -> str: + if name_path is None: + raise NotImplementedError("Renaming files/directories is not yet implemented for in the language server backend.") + symbol = self._find_unique_symbol(name_path, relative_path) if not symbol.location.has_position_in_file(): raise ValueError(f"Symbol '{name_path}' does not have a valid position in file for renaming") @@ -356,9 +359,9 @@ class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]): assert symbol.location.line is not None assert symbol.location.column is not None - lang_server = self._get_language_server(relative_file_path) + lang_server = self._get_language_server(relative_path) rename_result = lang_server.request_rename_symbol_edit( - relative_file_path=relative_file_path, line=symbol.location.line, column=symbol.location.column, new_name=new_name + relative_file_path=relative_path, line=symbol.location.line, column=symbol.location.column, new_name=new_name ) if rename_result is None: raise ValueError( @@ -425,11 +428,11 @@ class JetBrainsCodeEditor(CodeEditor[JetBrainsSymbol]): ) return JetBrainsSymbol(symbols[0], self._project) - def rename_symbol(self, name_path: str, relative_file_path: str, new_name: str) -> str: + def rename_symbol(self, name_path: str | None, relative_path: str, new_name: str) -> str: with JetBrainsPluginClient.from_project(self._project) as client: client.rename_symbol( name_path=name_path, - relative_path=relative_file_path, + relative_path=relative_path, new_name=new_name, rename_in_comments=False, rename_in_text_occurrences=False, diff --git a/src/serena/jetbrains/jetbrains_plugin_client.py b/src/serena/jetbrains/jetbrains_plugin_client.py index cd6b3b8b..f3ed6dc9 100644 --- a/src/serena/jetbrains/jetbrains_plugin_client.py +++ b/src/serena/jetbrains/jetbrains_plugin_client.py @@ -556,7 +556,7 @@ class JetBrainsPluginClient(ToStringMixin): return self._make_request("POST", "/inlineSymbol", request_data) def rename_symbol( - self, name_path: str, relative_path: str, new_name: str, rename_in_comments: bool, rename_in_text_occurrences: bool + self, name_path: str | None, relative_path: str, new_name: str, rename_in_comments: bool, rename_in_text_occurrences: bool ) -> None: """ Renames a symbol. diff --git a/src/serena/tools/symbol_tools.py b/src/serena/tools/symbol_tools.py index 2cf5c178..e17b74a0 100644 --- a/src/serena/tools/symbol_tools.py +++ b/src/serena/tools/symbol_tools.py @@ -408,27 +408,28 @@ class InsertBeforeSymbolTool(Tool, ToolMarkerSymbolicEdit): class RenameSymbolTool(Tool, ToolMarkerSymbolicEdit): """ - Renames a symbol throughout the codebase using language server refactoring capabilities. + Renames a symbol, file, or directory throughout the codebase using language server refactoring capabilities. """ def apply( self, - name_path: str, relative_path: str, new_name: str, + name_path: str | None = None, ) -> str: """ - Renames the symbol with the given `name_path` to `new_name` throughout the entire codebase. + Renames the symbol with the given `name_path` or a file/directory (if no `name_path` is passed) + to `new_name` throughout the entire codebase. Note: for languages with method overloading, like Java, name_path may have to include a method's signature to uniquely identify a method. - :param name_path: name path of the symbol to rename (definitions in the `find_symbol` tool apply) :param relative_path: the relative path to the file containing the symbol to rename :param new_name: the new name for the symbol + :param name_path: name path of the symbol to rename (definitions in the `find_symbol` tool apply) :return: result summary indicating success or failure """ code_editor = self.create_code_editor() - status_message = code_editor.rename_symbol(name_path, relative_file_path=relative_path, new_name=new_name) + status_message = code_editor.rename_symbol(name_path, relative_path=relative_path, new_name=new_name) return status_message From e6c97b99deca67cfe2551046c868a8f08255356e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 1 Apr 2026 12:13:11 +0200 Subject: [PATCH 008/190] Split off JBRenameTool from the RenameSymbolTool The JB variant has more options, the difference to the LS variant will likely grow in the future --- src/serena/code_editor.py | 33 +++++++++++-------- .../config/internal_modes/jetbrains.yml | 2 ++ src/serena/tools/jetbrains_tools.py | 17 ++++++++++ src/serena/tools/symbol_tools.py | 12 +++---- 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/serena/code_editor.py b/src/serena/code_editor.py index 202efe0f..da397dcb 100644 --- a/src/serena/code_editor.py +++ b/src/serena/code_editor.py @@ -230,16 +230,8 @@ class CodeEditor(Generic[TSymbol], ABC): edited_file.delete_text_between_positions(start_pos, end_pos) @abstractmethod - def rename_symbol(self, name_path: str | None, relative_path: str, new_name: str) -> str: - """ - Renames a symbol, file, or directory throughout the codebase. - - :param name_path: the name path of the symbol to rename - :param relative_path: if `name_path` is passed, the relative path of the file containing the symbol. - Otherwise, the path to the directory or file to rename. - :param new_name: the new name - :return: a status message - """ + def rename_symbol(self, name_path: str, relative_path: str, new_name: str) -> str: + pass class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]): @@ -348,9 +340,15 @@ class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]): operation.apply() return len(operations) - def rename_symbol(self, name_path: str | None, relative_path: str, new_name: str) -> str: - if name_path is None: - raise NotImplementedError("Renaming files/directories is not yet implemented for in the language server backend.") + def rename_symbol(self, name_path: str, relative_path: str, new_name: str) -> str: + """ + Renames a symbol, file, or directory throughout the codebase. + + :param name_path: the name path of the symbol to rename + :param relative_path: the relative path of the file containing the symbol. + :param new_name: the new name + :return: a status message + """ symbol = self._find_unique_symbol(name_path, relative_path) if not symbol.location.has_position_in_file(): raise ValueError(f"Symbol '{name_path}' does not have a valid position in file for renaming") @@ -429,6 +427,15 @@ class JetBrainsCodeEditor(CodeEditor[JetBrainsSymbol]): return JetBrainsSymbol(symbols[0], self._project) def rename_symbol(self, name_path: str | None, relative_path: str, new_name: str) -> str: + """ + Renames a code symbol, file, or directory throughout the codebase. + + :param name_path: the name path of the symbol to rename. Set to None for renaming a file or directory. + :param relative_path: if `name_path` is passed, the relative path of the file containing the symbol. + Otherwise, the path to the directory or file to rename. + :param new_name: the new name + :return: a status message + """ with JetBrainsPluginClient.from_project(self._project) as client: client.rename_symbol( name_path=name_path, diff --git a/src/serena/resources/config/internal_modes/jetbrains.yml b/src/serena/resources/config/internal_modes/jetbrains.yml index f9e60a4b..f7b2dd85 100644 --- a/src/serena/resources/config/internal_modes/jetbrains.yml +++ b/src/serena/resources/config/internal_modes/jetbrains.yml @@ -10,6 +10,7 @@ excluded_tools: - get_symbols_overview - restart_language_server - safe_delete_symbol + - rename_symbol included_optional_tools: - jet_brains_find_symbol - jet_brains_find_referencing_symbols @@ -18,3 +19,4 @@ included_optional_tools: - jet_brains_type_hierarchy - jet_brains_safe_delete - jet_brains_inline_symbol + - jet_brains_rename diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 33b4ac66..afb70283 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -3,6 +3,7 @@ from collections import Counter from typing import Any, Literal import serena.jetbrains.jetbrains_types as jb +from serena.code_editor import JetBrainsCodeEditor from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient from serena.jetbrains.jetbrains_types import SymbolDTO from serena.symbol import JetBrainsSymbolDictGrouper @@ -468,3 +469,19 @@ class JetBrainsFindImplementationsTool(Tool, ToolMarkerSymbolicRead): ) result = self._to_json(symbol_collection) return result + + +class JetBrainsRenameTool(Tool, ToolMarkerSymbolicEdit): + def apply(self, relative_path: str, new_name: str, name_path: str | None = None) -> str: + """ + Renames a symbol, file, or directory throughout the codebase. + + :param relative_path: if `name_path` is passed, the relative path of the file containing the symbol. + Otherwise, the path to the directory or file to rename. + :param new_name: the new name + :param name_path: the name path of the symbol to rename or None if renaming a file or directory. + :return: a status message + """ + code_editor = JetBrainsCodeEditor(self.project) + result = code_editor.rename_symbol(name_path=name_path, relative_path=relative_path, new_name=new_name) + return self._to_json(result) diff --git a/src/serena/tools/symbol_tools.py b/src/serena/tools/symbol_tools.py index e17b74a0..bff6f2fd 100644 --- a/src/serena/tools/symbol_tools.py +++ b/src/serena/tools/symbol_tools.py @@ -408,27 +408,27 @@ class InsertBeforeSymbolTool(Tool, ToolMarkerSymbolicEdit): class RenameSymbolTool(Tool, ToolMarkerSymbolicEdit): """ - Renames a symbol, file, or directory throughout the codebase using language server refactoring capabilities. + Renames a symbol throughout the codebase using language server refactoring capabilities. + For JB, we use a separate tool. """ def apply( self, + name_path: str, relative_path: str, new_name: str, - name_path: str | None = None, ) -> str: """ - Renames the symbol with the given `name_path` or a file/directory (if no `name_path` is passed) - to `new_name` throughout the entire codebase. + Renames the symbol with the given `name_path` to `new_name` throughout the entire codebase. Note: for languages with method overloading, like Java, name_path may have to include a method's signature to uniquely identify a method. + :param name_path: name path of the symbol to rename (definitions in the `find_symbol` tool apply) :param relative_path: the relative path to the file containing the symbol to rename :param new_name: the new name for the symbol - :param name_path: name path of the symbol to rename (definitions in the `find_symbol` tool apply) :return: result summary indicating success or failure """ - code_editor = self.create_code_editor() + code_editor = self.create_ls_code_editor() status_message = code_editor.rename_symbol(name_path, relative_path=relative_path, new_name=new_name) return status_message From 3929edede56627164bfdf2269ef65d1f2df3ec24 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 2 Apr 2026 10:31:48 +0200 Subject: [PATCH 009/190] _iter_gitignore_files: Catch PermissionError Relates to #1268 --- src/serena/util/file_system.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/serena/util/file_system.py b/src/serena/util/file_system.py index 3784a17a..d7d9f956 100644 --- a/src/serena/util/file_system.py +++ b/src/serena/util/file_system.py @@ -165,10 +165,14 @@ class GitignoreParser: def scan(abs_path: str | None) -> Iterator[str]: for entry in os.scandir(abs_path): - if entry.is_dir(follow_symlinks=follow_symlinks): - queue.append(entry.path) - elif entry.is_file(follow_symlinks=follow_symlinks) and entry.name == ".gitignore": - yield entry.path + try: + if entry.is_dir(follow_symlinks=follow_symlinks): + queue.append(entry.path) + elif entry.is_file(follow_symlinks=follow_symlinks) and entry.name == ".gitignore": + yield entry.path + except PermissionError as ex: + log.debug(f"Skipping entry due to permission error: {entry.path}", exc_info=ex) + continue while queue: next_abs_path = queue.pop(0) From a45fe0cf1096c8a0f05f17c4752477e02a3f1f76 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 2 Apr 2026 10:37:28 +0200 Subject: [PATCH 010/190] Exposed additional options in JBCodeEditor.rename_symbol [ci skip] Not forwarded to tool (yet) --- src/serena/code_editor.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/serena/code_editor.py b/src/serena/code_editor.py index da397dcb..2232f66b 100644 --- a/src/serena/code_editor.py +++ b/src/serena/code_editor.py @@ -426,7 +426,14 @@ class JetBrainsCodeEditor(CodeEditor[JetBrainsSymbol]): ) return JetBrainsSymbol(symbols[0], self._project) - def rename_symbol(self, name_path: str | None, relative_path: str, new_name: str) -> str: + def rename_symbol( + self, + name_path: str | None, + relative_path: str, + new_name: str, + rename_in_comments: bool = False, + rename_in_text_occurrences: bool = False, + ) -> str: """ Renames a code symbol, file, or directory throughout the codebase. @@ -434,6 +441,8 @@ class JetBrainsCodeEditor(CodeEditor[JetBrainsSymbol]): :param relative_path: if `name_path` is passed, the relative path of the file containing the symbol. Otherwise, the path to the directory or file to rename. :param new_name: the new name + :param rename_in_comments: whether to rename occurrences of the symbol in comments + :param rename_in_text_occurrences: whether to rename occurrences of the symbol in text :return: a status message """ with JetBrainsPluginClient.from_project(self._project) as client: @@ -441,7 +450,7 @@ class JetBrainsCodeEditor(CodeEditor[JetBrainsSymbol]): name_path=name_path, relative_path=relative_path, new_name=new_name, - rename_in_comments=False, - rename_in_text_occurrences=False, + rename_in_comments=rename_in_comments, + rename_in_text_occurrences=rename_in_text_occurrences, ) return "Success" From 33b4d08c9b7a55e13cdcfb6834679f638045a641 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 2 Apr 2026 10:58:37 +0200 Subject: [PATCH 011/190] Updated usage docs for codex --- docs/02-usage/030_clients.md | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index 8db7e6e8..98b615c2 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -153,25 +153,18 @@ For example, when using `uvx`, add the following section: ```toml [mcp_servers.serena] +startup_timeout_sec = 25 command = "uvx" -args = ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server", "--context", "codex"] +args = ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server", "--project-from-cwd", "--context", "codex"] ``` -After codex has started, you need to activate the project, which you can do by saying: +The larger startup timeout is to permit uvx to download the necessary dependencies. Once downloaded, the startup time is much faster. -> Call serena.activate_project, serena.check_onboarding_performed and serena.initial_instructions - -**If you don't activate the project, you will not be able to use Serena's tools!** - -It is recommend to set this prompt as a [custom prompt](https://developers.openai.com/codex/custom-prompts), so you don't need to type this every time. - -That's it! Have a look at `~/.codex/log/codex-tui.log` to see if any errors occurred. - -Serena's dashboard will run if you have not disabled it in the configuration, but due to Codex's sandboxing, the web browser -may not open automatically. You can open it manually by going to `http://localhost:24282/dashboard/index.html` (or a higher port, if -that was already taken). - -> Codex will often show the tools as `failed` even though they are successfully executed. This is not a problem, seems to be a bug in Codex. Despite the error message, everything works as expected. +:::{attention} +Codex currently ignores the [instructions](https://blog.modelcontextprotocol.io/posts/2025-11-03-using-server-instructions/) +property of MCP servers, so it is recommended to prompt the agent to +"read initial instructions for Serena" when starting your session, e.g. by mentioning this in your `AGENT.md`. +::: ## Claude Desktop From d52d24efe2061c554c570988f561f338ad3d709d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 2 Apr 2026 11:25:52 +0200 Subject: [PATCH 012/190] Docs: added `-p 3.13` to configuration examples --- docs/02-usage/030_clients.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index 98b615c2..bcb91e57 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -128,6 +128,8 @@ While serena can be directly installed from the GitHub MCP server registry, we r "type": "stdio", "command": "uvx", "args": [ + "-p", + "3.13", "--from", "git+https://github.com/oraios/serena", "serena", @@ -155,7 +157,7 @@ For example, when using `uvx`, add the following section: [mcp_servers.serena] startup_timeout_sec = 25 command = "uvx" -args = ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server", "--project-from-cwd", "--context", "codex"] +args = ["-p", "3.13", "--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server", "--project-from-cwd", "--context", "codex"] ``` The larger startup timeout is to permit uvx to download the necessary dependencies. Once downloaded, the startup time is much faster. @@ -182,6 +184,8 @@ Add the `serena` MCP server configuration "serena": { "command": "uvx", "args": [ + "-p", + "3.13", "--from", "git+https://github.com/oraios/serena", "serena", @@ -218,6 +222,8 @@ MCP server configuration: "serena": { "command": "uvx", "args": [ + "-p", + "3.13", "--from", "git+https://github.com/oraios/serena", "serena", @@ -246,6 +252,8 @@ Go to Settings / Tools / AI Assistant / MCP and add a new **local** configuratio "serena": { "command": "uvx", "args": [ + "-p", + "3.13", "--from", "git+https://github.com/oraios/serena", "serena", @@ -272,6 +280,8 @@ Add this configuration: "serena": { "command": "uvx", "args": [ + "-p", + "3.13", "--from", "git+https://github.com/oraios/serena", "serena", From 6faf12b9fa7d0ef158aa7c2f00d3432ba6864c31 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 1 Apr 2026 15:17:47 +0200 Subject: [PATCH 013/190] Update uvx run instructions to include Python version (3.13) uvx ignores not only the lock file but also the Python version specified in pyproject.toml. https://github.com/astral-sh/uv/issues/8206#issuecomment-4113536371 --- README.md | 2 +- docs/02-usage/020_running.md | 6 +++--- docs/02-usage/030_clients.md | 4 ++-- docs/02-usage/040_workflow.md | 4 ++-- docs/03-special-guides/serena_on_chatgpt.md | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d4fa11ed..2fadd208 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ See our [documentation page](https://oraios.github.io/serena/02-usage/025_jetbra Issue this command to see available options: ```bash -uvx --from git+https://github.com/oraios/serena serena start-mcp-server --help +uvx -p 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --help ``` **Configuring Your Client**. To connect Serena to your preferred MCP client, you typically need to [configure a launch command in your client](https://oraios.github.io/serena/02-usage/030_clients.html). diff --git a/docs/02-usage/020_running.md b/docs/02-usage/020_running.md index 5766a987..4e2ca372 100644 --- a/docs/02-usage/020_running.md +++ b/docs/02-usage/020_running.md @@ -21,7 +21,7 @@ In general, to get help, append `--help` to the command, i.e. `uvx` is part of `uv`. It can be used to run the latest version of Serena directly from the repository, without an explicit local installation. - uvx --from git+https://github.com/oraios/serena serena + uvx -p 3.13 --from git+https://github.com/oraios/serena serena Explore the CLI to see some of the customization options that serena provides (more info on them below). @@ -124,7 +124,7 @@ case, you can simply run the `start-mcp-server` command without any additional o For example, to run the server in stdio mode via `uvx`, you would run: - uvx --from git+https://github.com/oraios/serena serena start-mcp-server + uvx -p 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server See the section ["Configuring Your MCP Client"](030_clients) for specific information on how to configure your MCP client (e.g. Claude Code, Codex, Cursor, etc.) to use such a launch command. @@ -143,7 +143,7 @@ via the `--port` option. For example, to run the Serena MCP server in streamable HTTP mode on port 9121 using uvx, you would run - uvx --from git+https://github.com/oraios/serena serena start-mcp-server --transport streamable-http --port 9121 + uvx -p 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --transport streamable-http --port 9121 and then configure your client to connect to `http://localhost:9121/mcp`. diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index bcb91e57..e0c13e73 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -79,7 +79,7 @@ Serena is a great way to make Claude Code both cheaper and more powerful! use this command: ```shell -claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena start-mcp-server --context claude-code --project "$(pwd)" +claude mcp add serena -- uvx -p 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --context claude-code --project "$(pwd)" ``` Note: @@ -92,7 +92,7 @@ Note: **Global Configuration**. Alternatively, use `--project-from-cwd` for user-level configuration that works across all projects: ```shell -claude mcp add --scope user serena -- uvx --from git+https://github.com/oraios/serena serena start-mcp-server --context=claude-code --project-from-cwd +claude mcp add --scope user serena -- uvx -p 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --context=claude-code --project-from-cwd ``` Whenever you start Claude Code, Serena will search up from the current directory for `.serena/project.yml` or `.git` markers, diff --git a/docs/02-usage/040_workflow.md b/docs/02-usage/040_workflow.md index f8b3603f..380009c5 100644 --- a/docs/02-usage/040_workflow.md +++ b/docs/02-usage/040_workflow.md @@ -29,7 +29,7 @@ To explicitly create a project, use the following command while in the project d For instance, when using `uvx`, run - uvx --from git+https://github.com/oraios/serena serena project create [options] + uvx -p 3.13 --from git+https://github.com/oraios/serena serena project create [options] * For an empty project, you will need to specify the programming language (e.g., `--language python`). @@ -201,7 +201,7 @@ Depending on the language backend being used, the management of resources for th where `` is your way of running Serena. For example, when using `uvx`, run - uvx --from git+https://github.com/oraios/serena serena start-project-server + uvx -p 3.13 --from git+https://github.com/oraios/serena serena start-project-server ### Multiple Agents Accessing a Single Serena Instance diff --git a/docs/03-special-guides/serena_on_chatgpt.md b/docs/03-special-guides/serena_on_chatgpt.md index ec872696..d5b640ba 100644 --- a/docs/03-special-guides/serena_on_chatgpt.md +++ b/docs/03-special-guides/serena_on_chatgpt.md @@ -17,7 +17,7 @@ Run the following command to launch Serena as http server (assuming port 8000): ```bash uvx mcpo --port 8000 --api-key -- \ - uvx --from git+https://github.com/oraios/serena \ + uvx -p 3.13 --from git+https://github.com/oraios/serena \ serena start-mcp-server --context chatgpt --project $(pwd) ``` @@ -28,7 +28,7 @@ You can also use other options, and you don't have to pass `--project` if you wa or want to activate it later. See ```shell -uvx --from git+https://github.com/oraios/serena serena start-mcp-server --help +uvx -p 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --help ``` --- From 9f63606d7b80a635cc38f44ae882d50461cf0c7e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 26 Mar 2026 10:45:10 +0100 Subject: [PATCH 014/190] Update README and About section with enhanced feature presentation for v1 --- README.md | 246 ++++++++++++-------------- docs/01-about/000_intro.md | 109 ++++++++++-- docs/02-usage/025_jetbrains_plugin.md | 9 +- 3 files changed, 213 insertions(+), 151 deletions(-) diff --git a/README.md b/README.md index 2fadd208..7d1d1ae1 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,121 @@ -

+

-* :rocket: Serena is a powerful **coding agent toolkit** capable of turning an LLM into a fully-featured agent that works **directly on your codebase**. - Unlike most other tools, it is not tied to an LLM, framework or an interface, making it easy to use it in a variety of ways. -* :wrench: Serena provides essential **semantic code retrieval and editing tools** that are akin to an IDE's capabilities, extracting code entities at the symbol level and exploiting relational structure. When combined with an existing coding agent, these tools greatly enhance (token) efficiency. -* :free: Serena is **free & open-source**, enhancing the capabilities of LLMs you already have access to free of charge. +

+ Serena is the IDE for your coding agent. + +

-You can think of Serena as providing IDE-like tools to your LLM/coding agent. -With it, the agent no longer needs to read entire files, perform grep-like searches or basic string replacements to find the right parts of the code and to edit code. -Instead, it can use code-centric tools like `find_symbol`, `find_referencing_symbols` and `insert_after_symbol`. +* Serena provides essential **semantic code retrieval, editing and refactoring tools** that are akin to an IDE's capabilities, + operating at the symbol level and exploiting relational structure. +* It integrates with any client/LLM via the model context protocol (**MCP**). -

- Serena is under active development! See the latest updates, upcoming features, and lessons learned to stay up to date. -

+Serena's **agent-first tool design** involves robust high-level abstractions, distinguishing it from +approaches that rely on low-level concepts like line numbers or primitive search patterns. -

- Changelog - Lessons Learned -

+Practically, this means that your agent operates **faster, more efficiently and more reliably**, especially in larger and +more complex codebases. -> [!TIP] -> The [**Serena JetBrains plugin**](#the-serena-jetbrains-plugin) has been released! +## Features -## LLM Integration +Serena supports two code intelligence backends: -Serena provides the necessary [tools](https://oraios.github.io/serena/01-about/035_tools.html) for coding workflows, but an LLM is required to do the actual work, -orchestrating tool use. +* **Language servers** implementing the language server protocol (LSP) — the free/open-source alternative. +* **The Serena JetBrains Plugin**, which leverages the capabilities of your JetBrains IDE. -In general, Serena can be integrated with an LLM in several ways: +Language Servers, while very powerful, have inherent limitations. +The JetBrains variant is the more advanced solution, as detailed below. It is also more memory-efficient, especially in +multi-agent scenarios. -* by using the **model context protocol (MCP)**. - Serena provides an MCP server which integrates with - * Claude Code and Claude Desktop, - * terminal-based clients like Codex, Gemini-CLI, Qwen3-Coder, rovodev, OpenHands CLI and others, - * IDEs like VSCode, Cursor or IntelliJ, - * Extensions like Cline or Roo Code - * Local clients like [OpenWebUI](https://docs.openwebui.com/openapi-servers/mcp), [Jan](https://jan.ai/docs/mcp-examples/browser/browserbase#enable-mcp), [Agno](https://docs.agno.com/introduction/playground) and others -* by using [mcpo to connect it to ChatGPT](docs/03-special-guides/serena_on_chatgpt.md) or other clients that don't support MCP but do support tool calling via OpenAPI. -* by incorporating Serena's tools into an agent framework of your choice, as illustrated [here](docs/03-special-guides/custom_agent.md). - Serena's tool implementation is decoupled from the framework-specific code and can thus easily be adapted to any agent framework. +### Language Support + +#### Language Servers + +When using Serena's language server backend, we provide **support for over 40 programming languages**, including +AL, Ansible, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, F# (currently with some bugs), GLSL, Go, Groovy (partial support), Haskell, HLSL, Java, Javascript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. + +#### The Serena JetBrains Plugin + +The [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) +leverages the powerful code analysis capabilities of your JetBrains IDE. +The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs, +including IntelliJ IDEA, PyCharm, Android Studio, WebStorm, PhpStorm, RubyMine, GoLand, and potentially others (Rider and CLion are unsupported though). + + + +See our [documentation page](https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html) for further details and instructions on how to apply the plugin. + +### Retrieval + +Serena's retrieval tools allow agents to explore codebases at the symbol level, understanding structure and relationships +without reading entire files. + +| Capability | Language Servers | JetBrains Plugin | +|----------------------------------|------------------|------------------| +| find symbol | yes | yes | +| symbol overview (file outline) | yes | yes | +| find referencing symbols | yes | yes | +| search in project dependencies | -- | yes | +| type hierarchy | -- | yes | +| find declaration | -- | yes | +| find implementations | -- | yes | +| query external projects | yes | yes | + +### Refactoring + +Without precise refactoring tools, agents are forced to resort to unreliable and expensive search and replace operations. + +| Capability | Language Servers | JetBrains Plugin | +|-------------------------------------------|--------------------|-----------------------------------| +| rename | yes (only symbols) | yes (symbols, files, directories) | +| move (symbol, file, directory) | -- | yes | +| inline | -- | yes | +| propagate deletions (remove unused code) | -- | yes | + +### Symbolic Editing + +Serena's symbolic editing tools are less error-prone and much more token-efficient than typical alternatives. + +| Capability | Language Servers | JetBrains Plugin | +|------------------------|-------------------|------------------| +| replace symbol body | yes | yes | +| insert after symbol | yes | yes | +| insert before symbol | yes | yes | +| safe delete | yes | yes | + +### Basic Features + +Beyond its semantic capabilities, Serena includes a set of basic utilities for completeness. +When Serena is used inside an agentic harness such as Claude Code or Codex, these tools are typically disabled by default, +since the surrounding harness already provides overlapping file, search, and shell capabilities. + +- **`search_for_pattern`** – flexible regex search across the codebase +- **`replace_content`** – agent-optimised regex-based and literal text replacement +- **`list_dir` / `find_file`** – directory listing and file search +- **`read_file`** – read files or file chunks +- **`execute_shell_command`** – run shell commands (e.g. builds, tests, linters) + +### Memory Management + +A memory system is elemental to long-lived agent workflows, especially when knowledge is to be shared across +sessions, users and projects. +Despite its simplicity, we received positive feedback from many users who tend to combine Serena's memory management system with their +agent's internal system (e.g., `AGENTS.md` files). +It can easily be disabled if you prefer to use something else. + +### Configurability + +Active tools, tool descriptions, prompts, language backend details and many other aspects of Serena +can be flexibly configured on a per-case basis by simply adjusting a few lines of YAML. +To achieve this, Serena offers multiple levels of (composable) configuration: + +* global configuration +* MCP launch command (CLI) configuration +* per-project configuration (with local overrides) +* execution context-specific configuration (e.g. for particular clients) +* dynamically composable configuration fragments (modes) ## Serena in Action @@ -57,51 +132,13 @@ Note how Serena's tools enable Claude to find and edit the right symbols. https://github.com/user-attachments/assets/6eaa9aa1-610d-4723-a2d6-bf1e487ba753 -## Programming Language Support & Semantic Analysis Capabilities - -Serena provides a set of versatile code querying and editing functionalities -based on symbolic understanding of the code. -Equipped with these capabilities, Serena discovers and edits code just like a seasoned developer -making use of an IDE's capabilities would. -Serena can efficiently find the right context and do the right thing even in very large and -complex projects! - -There are two alternative technologies powering these capabilities: - -* **Language servers** implementing the language server Protocol (LSP) — the free/open-source alternative. -* **The Serena JetBrains Plugin**, which leverages the powerful code analysis and editing - capabilities of your JetBrains IDE. - -You can choose either of these backends depending on your preferences and requirements. - -### Language Servers - -Serena incorporates a powerful abstraction layer for the integration of language servers -that implement the language server protocol (LSP). -The underlying language servers are typically open-source projects (like Serena) or at least freely available for use. - -With Serena's LSP library, we provide **support for over 40 programming languages**, including -AL, Ansible, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, F# (currently with some bugs), GLSL, Go, Groovy (partial support), Haskell, HLSL, Java, Javascript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. - -> [!IMPORTANT] -> Some language servers require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details. - -### The Serena JetBrains Plugin - -As an alternative to language servers, the [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) -leverages the powerful code analysis capabilities of your JetBrains IDE. -The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs, -including IntelliJ IDEA, PyCharm, Android Studio, WebStorm, PhpStorm, RubyMine, GoLand, and potentially others (Rider and CLion are unsupported though). - - - -The plugin offers the most robust and most powerful Serena experience. -See our [documentation page](https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html) for further details and instructions. - ## Quick Start **Prerequisites**. Serena is managed by *uv*. If you don’t already have it, you need to [install uv](https://docs.astral.sh/uv/getting-started/installation/) before proceeding. +> [!NOTE] +> Some language servers require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details. + **Starting the MCP Server**. The easiest way to start the Serena MCP server is by running the latest version from GitHub using uvx. Issue this command to see available options: @@ -124,74 +161,9 @@ Follow the link for specific instructions on how to set up Serena for Claude Cod Please refer to the [user guide](https://oraios.github.io/serena/02-usage/000_intro.html) for detailed instructions on how to use Serena effectively. -## Community Feedback - -Most users report that Serena has strong positive effects on the results of their coding agents, even when used within -very capable agents like Claude Code. Serena is often described to be a [game changer](https://www.reddit.com/r/ClaudeAI/comments/1lfsdll/try_out_serena_mcp_thank_me_later/), providing an enormous [productivity boost](https://www.reddit.com/r/ClaudeCode/comments/1mguoia/absolutely_insane_improvement_of_claude_code). - -Serena excels at navigating and manipulating complex codebases, providing tools that support precise code retrieval and editing in the presence of large, strongly structured codebases. -However, when dealing with tasks that involve only very few/small files, you may not benefit from including Serena on top of your existing coding agent. -In particular, when writing code from scratch, Serena will not provide much value initially, as the more complex structures that Serena handles more gracefully than simplistic, file-based approaches are yet to be created. - -Several videos and blog posts have talked about Serena: - -* YouTube: - * [AI Labs](https://www.youtube.com/watch?v=wYWyJNs1HVk&t=1s) - * [Yo Van Eyck](https://www.youtube.com/watch?v=UqfxuQKuMo8&t=45s) - * [JeredBlu](https://www.youtube.com/watch?v=fzPnM3ySmjE&t=32s) - -* Blog posts: - * [Serena's Design Principles](https://medium.com/@souradip1000/deconstructing-serenas-mcp-powered-semantic-code-understanding-architecture-75802515d116) - * [Serena with Claude Code (in Japanese)](https://blog.lai.so/serena/) - * [Turning Claude Code into a Development Powerhouse](https://robertmarshall.dev/blog/turning-claude-code-into-a-development-powerhouse/) - ## Acknowledgements -### Sponsors - -We are very grateful to our [sponsors](https://github.com/sponsors/oraios) who help us drive Serena's development. The core team -(the founders of [Oraios AI](https://oraios-ai.de/)) put in a lot of work in order to turn Serena into a useful open source project. -So far, there is no business model behind this project, and sponsors are our only source of income from it. - -Sponsors help us dedicating more time to the project, managing contributions, and working on larger features (like better tooling based on more advanced -LSP features, VSCode integration, debugging via the DAP, and several others). -If you find this project useful to your work, or would like to accelerate the development of Serena, consider becoming a sponsor. - -We are proud to announce that the Visual Studio Code team, together with Microsoft’s Open Source Programs Office and GitHub Open Source -have decided to sponsor Serena with a one-time contribution! - -

- Visual Studio Code sponsor logo -

- -### Community Contributions - A significant part of Serena, especially support for various languages, was contributed by the open source community. We are very grateful for the many contributors who made this possible and who played an important role in making Serena what it is today. -### Technologies -We built Serena on top of multiple existing open-source technologies, the most important ones being: - -1. [multilspy](https://github.com/microsoft/multilspy). - A library which wraps language server implementations and adapts them for interaction via Python. - It provided the basis for our library Solid-LSP (`src/solidlsp`). - Solid-LSP provides pure synchronous LSP calls and extends the original library with the symbolic logic - that Serena required. -2. [Python MCP SDK](https://github.com/modelcontextprotocol/python-sdk) -3. All the language servers that we use through Solid-LSP. - -Without these projects, Serena would not have been possible (or would have been significantly more difficult to build). - -## Customizing and Extending Serena - -It is straightforward to extend Serena's AI functionality with your own ideas. -Simply implement a new tool by subclassing -`serena.agent.Tool` and implement the `apply` method with a signature -that matches the tool's requirements. -Once implemented, `SerenaAgent` will automatically have access to the new tool. - -It is also relatively straightforward to add [support for a new programming language](/.serena/memories/adding_new_language_support_guide.md). - -We look forward to seeing what the community will come up with! -For details on contributing, see [contributing guidelines](/CONTRIBUTING.md). diff --git a/docs/01-about/000_intro.md b/docs/01-about/000_intro.md index 3fbd8b64..c1b9de6b 100644 --- a/docs/01-about/000_intro.md +++ b/docs/01-about/000_intro.md @@ -1,14 +1,101 @@ # About Serena -* Serena is a powerful **coding agent toolkit** capable of turning an LLM into a fully-featured agent that works - **directly on your codebase**. - Unlike most other tools, it is not tied to an LLM, framework or an interface, making it easy to use it in a variety of ways. -* Serena provides essential **semantic code retrieval and editing tools** that are akin to an IDE's capabilities, - extracting code entities at the symbol level and exploiting relational structure. - When combined with an existing coding agent, these tools greatly enhance (token) efficiency. -* Serena is **free & open-source**, enhancing the capabilities of LLMs you already have access to free of charge. +Serena is **the IDE for your coding agent**. -Therefore, you can think of Serena as providing IDE-like tools to your LLM/coding agent. -With it, the agent no longer needs to read entire files, perform grep-like searches or string replacements to find and -edit the right code. -Instead, it can use code-centred tools like `find_symbol`, `find_referencing_symbols` and `insert_after_symbol`. +* It provides essential **semantic code retrieval, editing and refactoring tools** that are akin to an IDE's capabilities, + operating at the symbol level and exploiting relational structure. +* It integrates with any client/LLM via the model context protocol (MCP). + +Serena's agent-first tool design involves robust high-level abstractions, distinguishing it from +approaches that rely on low-level concepts like line numbers or primitive search patterns. + +Practically, this means that your agent operates faster, more efficiently and more reliably, especially in larger and +more complex codebases. + +## Features + +Serena supports two code intelligence backends: + +* **Language servers** implementing the language server protocol (LSP) — the free/open-source alternative. +* **The Serena JetBrains Plugin**, which leverages the capabilities of your JetBrains IDE. + +Language Servers, while very powerful, have inherent limitations. +The JetBrains variant is the more advanced solution, as detailed below. It is also more memory-efficient, especially in +multi-agent scenarios. + + +Purchasing the JetBrains plugin supports the continued development of Serena. + + +### Retrieval + +Serena's retrieval tools allow agents to explore codebases at the symbol level, understanding structure and relationships +without reading entire files. + +| Capability | Language Server | JetBrains Plugin | +|------------------------------------|-----------------|------------------| +| **find symbol** | yes | yes | +| **symbol overview** (file outline) | yes | yes | +| **find referencing symbols** | yes | yes | +| **search in project dependencies** | -- | yes | +| **type hierarchy** | -- | yes | +| **find declaration** | -- | yes | +| **find implementations** | -- | yes | +| **query external projects** | yes | yes | + + +### Refactoring + +| Capability | Language Server | JetBrains Plugin | +|----------------------------------------------|--------------------|-----------------------------------| +| **rename** | yes (only symbols) | yes (symbols, files, directories) | +| **move** (symbol, file, directory) | -- | yes | +| **inline** | -- | yes | +| **propagate deletions** (remove unused code) | -- | yes | + + +### Symbolic Editing + +Much more token-efficient than default editing tools. + +| Capability | Language Server | JetBrains Plugin | +|--------------------------|-----------------|------------------| +| **replace symbol body** | yes | yes | +| **insert after symbol** | yes | yes | +| **insert before symbol** | yes | yes | +| **safe delete** | yes | yes | + +### Basic Features + +Beyond its semantic capabilities, Serena includes a set of basic utilities for completeness. +When Serena is used inside an agentic harness such as Claude Code or Codex, these tools are typically disabled by default, +since the surrounding harness already provides overlapping file, search, and shell capabilities. + +- **`search_for_pattern`** -- Flexible regex search across the codebase with glob-based file filtering, context lines, + and the option to restrict to code files only. Useful when you don't know the exact symbol name. +- **`list_dir` / `find_file`** -- Directory listing and file search with glob support. Helps agents orient themselves + in unfamiliar projects. +- **`read_file`** -- Read files or file chunks by line range, for cases where symbolic access isn't applicable + (e.g. configuration files, HTML templates). +- **`execute_shell_command`** -- Run shell commands (builds, tests, linters) directly from the agent, + with configurable working directory and output limits. + +### Memory Management + +Serena provides the functionality of a fully featured agent, and a useful aspect of this is Serena's memory system. +Despite its simplicity, we received positive feedback from many users who tend to combine it with their +agent's internal memory management (e.g., `AGENTS.md` files). +Many other memory management systems for agents exist, and you can easily disable Serena's memory management if you +prefer not to use it. + +## Configurability + +Active tools, tool descriptions, prompts, language backend details and many other aspects of Serena +can be flexibly configured on a per-case basis by simply adjusting a few lines of YAML. +To achieve this, Serena offers multiple levels of (composable) configuration: + +* global configuration +* MCP launch command (CLI) configuration +* per-project configuration (with local overrides) +* execution context-specific configuration (e.g. for particular clients) +* dynamically composable configuration fragments (modes) diff --git a/docs/02-usage/025_jetbrains_plugin.md b/docs/02-usage/025_jetbrains_plugin.md index 32c69591..3f51021e 100644 --- a/docs/02-usage/025_jetbrains_plugin.md +++ b/docs/02-usage/025_jetbrains_plugin.md @@ -23,10 +23,13 @@ The proceeds from plugin sales allow us to dedicate more resources to further de There are multiple features that are only available when using the JetBrains plugin: * **External library indexing**: Dependencies and libraries are fully indexed and accessible to Serena -* **No additional setup**: No need to download or configure separate language servers +* **Enhanced retrieval & refactoring capabilities**: The plugin supports additional tools (e.g. type + hierarchy retrieval, move, find declaration, inline symbol, etc.) +* **Improved multi-agent support**: A single IDE instance naturally serves arbitrarily many agent sessions * **Enhanced performance**: Faster tool execution thanks to optimized IDE integration -* **Multi-language excellence**: First-class support for polyglot projects with multiple languages and frameworks -* **Enhanced retrieval capabilities**: The plugin supports additional retrieval tools for type hierarchy information as well as fast and reliable documentation/type signature retrieval +* **Multi-language excellence** and **framework support**: First-class support for polyglot projects with multiple languages + and frameworks (whatever is recognised by your IDE as a symbol will also be available to Serena) +* **No additional setup**: No need to download or configure separate language servers We are also working on additional features like a `move_symbol` tool and debugging-related capabilities that will be available exclusively through the JetBrains plugin. From 2d78040cf928d9531087a521094a996632b0c7bf Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 2 Apr 2026 18:20:45 +0200 Subject: [PATCH 015/190] Auto-generate docs pages from README: About & Features --- README.md | 1 - docs/01-about/.gitignore | 5 +- docs/01-about/000_intro.md | 101 ------------------------------------- docs/autogen_docs.py | 35 +++++++++++++ 4 files changed, 39 insertions(+), 103 deletions(-) delete mode 100644 docs/01-about/000_intro.md diff --git a/README.md b/README.md index 7d1d1ae1..feba7f7b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@

Serena is the IDE for your coding agent. -

* Serena provides essential **semantic code retrieval, editing and refactoring tools** that are akin to an IDE's capabilities, diff --git a/docs/01-about/.gitignore b/docs/01-about/.gitignore index e1fbb867..463af074 100644 --- a/docs/01-about/.gitignore +++ b/docs/01-about/.gitignore @@ -1 +1,4 @@ -/035_tools.md \ No newline at end of file +# auto-generated files +/000_intro.md +/005_features.md +/035_tools.md diff --git a/docs/01-about/000_intro.md b/docs/01-about/000_intro.md deleted file mode 100644 index c1b9de6b..00000000 --- a/docs/01-about/000_intro.md +++ /dev/null @@ -1,101 +0,0 @@ -# About Serena - -Serena is **the IDE for your coding agent**. - -* It provides essential **semantic code retrieval, editing and refactoring tools** that are akin to an IDE's capabilities, - operating at the symbol level and exploiting relational structure. -* It integrates with any client/LLM via the model context protocol (MCP). - -Serena's agent-first tool design involves robust high-level abstractions, distinguishing it from -approaches that rely on low-level concepts like line numbers or primitive search patterns. - -Practically, this means that your agent operates faster, more efficiently and more reliably, especially in larger and -more complex codebases. - -## Features - -Serena supports two code intelligence backends: - -* **Language servers** implementing the language server protocol (LSP) — the free/open-source alternative. -* **The Serena JetBrains Plugin**, which leverages the capabilities of your JetBrains IDE. - -Language Servers, while very powerful, have inherent limitations. -The JetBrains variant is the more advanced solution, as detailed below. It is also more memory-efficient, especially in -multi-agent scenarios. - - -Purchasing the JetBrains plugin supports the continued development of Serena. - - -### Retrieval - -Serena's retrieval tools allow agents to explore codebases at the symbol level, understanding structure and relationships -without reading entire files. - -| Capability | Language Server | JetBrains Plugin | -|------------------------------------|-----------------|------------------| -| **find symbol** | yes | yes | -| **symbol overview** (file outline) | yes | yes | -| **find referencing symbols** | yes | yes | -| **search in project dependencies** | -- | yes | -| **type hierarchy** | -- | yes | -| **find declaration** | -- | yes | -| **find implementations** | -- | yes | -| **query external projects** | yes | yes | - - -### Refactoring - -| Capability | Language Server | JetBrains Plugin | -|----------------------------------------------|--------------------|-----------------------------------| -| **rename** | yes (only symbols) | yes (symbols, files, directories) | -| **move** (symbol, file, directory) | -- | yes | -| **inline** | -- | yes | -| **propagate deletions** (remove unused code) | -- | yes | - - -### Symbolic Editing - -Much more token-efficient than default editing tools. - -| Capability | Language Server | JetBrains Plugin | -|--------------------------|-----------------|------------------| -| **replace symbol body** | yes | yes | -| **insert after symbol** | yes | yes | -| **insert before symbol** | yes | yes | -| **safe delete** | yes | yes | - -### Basic Features - -Beyond its semantic capabilities, Serena includes a set of basic utilities for completeness. -When Serena is used inside an agentic harness such as Claude Code or Codex, these tools are typically disabled by default, -since the surrounding harness already provides overlapping file, search, and shell capabilities. - -- **`search_for_pattern`** -- Flexible regex search across the codebase with glob-based file filtering, context lines, - and the option to restrict to code files only. Useful when you don't know the exact symbol name. -- **`list_dir` / `find_file`** -- Directory listing and file search with glob support. Helps agents orient themselves - in unfamiliar projects. -- **`read_file`** -- Read files or file chunks by line range, for cases where symbolic access isn't applicable - (e.g. configuration files, HTML templates). -- **`execute_shell_command`** -- Run shell commands (builds, tests, linters) directly from the agent, - with configurable working directory and output limits. - -### Memory Management - -Serena provides the functionality of a fully featured agent, and a useful aspect of this is Serena's memory system. -Despite its simplicity, we received positive feedback from many users who tend to combine it with their -agent's internal memory management (e.g., `AGENTS.md` files). -Many other memory management systems for agents exist, and you can easily disable Serena's memory management if you -prefer not to use it. - -## Configurability - -Active tools, tool descriptions, prompts, language backend details and many other aspects of Serena -can be flexibly configured on a per-case basis by simply adjusting a few lines of YAML. -To achieve this, Serena offers multiple levels of (composable) configuration: - -* global configuration -* MCP launch command (CLI) configuration -* per-project configuration (with local overrides) -* execution context-specific configuration (e.g. for particular clients) -* dynamically composable configuration fragments (modes) diff --git a/docs/autogen_docs.py b/docs/autogen_docs.py index 5b4ca422..8c828809 100644 --- a/docs/autogen_docs.py +++ b/docs/autogen_docs.py @@ -1,5 +1,6 @@ import logging import os +import re import shutil from pathlib import Path from typing import Optional, List @@ -170,11 +171,45 @@ def autogen_tool_list(target_filename = "01-about/035_tools.md"): f.write(text.build()) +def autogen_about_intro_features(): + readme_path = Path(__file__).parent.parent / "README.md" + with open(readme_path, "r", encoding="utf-8") as f: + readme_contents = f.read() + + match = re.search(r"(.*?)(.*?)^(## Features.*?)^## ", readme_contents, re.DOTALL | re.MULTILINE) + assert match, f"Failed to extract about texts from README.md. {__file__} probably needs to be updated." + + tagline = match.group(1).strip() + about_text = match.group(2).strip() + features_text = match.group(3).strip() + + autogen_info = f"\n\n" + + with open(Path(__file__).parent / "01-about" / "000_intro.md", "w", encoding="utf-8") as f: + f.write(autogen_info) + f.write("# About Serena\n\n") + f.write(f"**{tagline}**\n\n") + f.write(f"{about_text}\n\n") + + jetbrains_marketplace_link = ('```{raw} html\n' + '

' + '' + '

\n```') + + with open(Path(__file__).parent / "01-about" / "005_features.md", "w", encoding="utf-8") as f: + f.write(autogen_info) + features_text = re.subn(r"^#", r"", features_text, flags=re.MULTILINE)[0] + features_text = re.sub(r' Date: Thu, 2 Apr 2026 21:02:04 +0200 Subject: [PATCH 016/190] Streamline 'Language Support' page, removing some overlap with other pages --- docs/01-about/020_programming-languages.md | 26 ++++++---------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index 241e0b62..11585ea6 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -1,7 +1,7 @@ # Language Support Serena provides a set of versatile code querying and editing functionalities -based on symbolic understanding of the code. +based on symbolic understanding of the code across a wide range of programming languages. Equipped with these capabilities, Serena discovers and edits code just like a seasoned developer making use of an IDE's capabilities would. Serena can efficiently find the right context and do the right thing even in very large and @@ -13,7 +13,7 @@ There are two alternative technologies powering these capabilities: * **The Serena JetBrains Plugin**, which leverages the powerful code analysis and editing capabilities of your JetBrains IDE. -You can choose either of these backends depending on your preferences and requirements. +See the [Features](005_features) section for a detailed comparison of the capabilities provided by the JetBrains Plugin vs. language servers. ## Language Servers @@ -113,25 +113,15 @@ see Serena's [memory on that](https://github.com/oraios/serena/blob/main/.serena ## The Serena JetBrains Plugin -As an alternative to language servers, the [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) -leverages the powerful code analysis capabilities of JetBrains IDEs. -The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs, -including IntelliJ IDEA, PyCharm, Android Studio, WebStorm, PhpStorm, RubyMine, GoLand, and potentially others -(Rider and CLion are unsupported though). +The [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) leverages the powerful code analysis capabilities of JetBrains IDEs. +The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs. When using the plugin, Serena connects to an instance of your JetBrains IDE via the plugin. For users who already work in a JetBrains IDE, this means Serena seamlessly integrates with the IDE instance you typically have open anyway, -requiring no additional setup or configuration beyond the plugin itself. This approach offers several key advantages: +requiring no additional setup or configuration beyond the plugin itself. -* **External library indexing**: Dependencies and libraries are fully indexed and accessible to Serena -* **No additional setup**: No need to download or configure separate language servers -* **Enhanced performance**: Faster tool execution thanks to optimized IDE integration -* **Multi-language excellence**: First-class support for polyglot projects with multiple languages and frameworks - -Even if you prefer to work in a different code editor, you can still benefit from the JetBrains plugin by running -a JetBrains IDE instance (most have free community editions) alongside your preferred editor with your project -opened and indexed. Serena will connect to the IDE for code analysis while you continue working in your editor -of choice. +* See the [JetBrains Plugin documentation](../02-usage/025_jetbrains_plugin) for a high-level overview of its benefits and usage details. +* See the [Features](005_features) section for a detailed comparison of the capabilities provided by the JetBrains Plugin vs. language servers. ```{raw} html

@@ -140,5 +130,3 @@ of choice.

``` - -See the [JetBrains Plugin documentation](../02-usage/025_jetbrains_plugin) for usage details. From aa78d9751df1ee16a270d139c1f556f9f279729f Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 2 Apr 2026 21:17:23 +0200 Subject: [PATCH 017/190] Further improve JetBrains docs page for v1 --- docs/02-usage/025_jetbrains_plugin.md | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/02-usage/025_jetbrains_plugin.md b/docs/02-usage/025_jetbrains_plugin.md index 3f51021e..b6da5037 100644 --- a/docs/02-usage/025_jetbrains_plugin.md +++ b/docs/02-usage/025_jetbrains_plugin.md @@ -17,23 +17,30 @@ especially for users of JetBrains IDEs. **Purchasing the JetBrains Plugin supports the Serena project.** The proceeds from plugin sales allow us to dedicate more resources to further developing and improving Serena. +How it works: + 1. Install the plugin in your JetBrains IDE + 2. Configure Serena to use the JetBrains language backend (see [below](configure-jetbrains)) + 3. Open the project you want to work on in your JetBrains IDE and activate it in Serena (see [below](jetbrains-workflow)) + 4. Start coding via your MCP client as usual ## Advantages of the JetBrains Plugin There are multiple features that are only available when using the JetBrains plugin: * **External library indexing**: Dependencies and libraries are fully indexed and accessible to Serena -* **Enhanced retrieval & refactoring capabilities**: The plugin supports additional tools (e.g. type - hierarchy retrieval, move, find declaration, inline symbol, etc.) -* **Improved multi-agent support**: A single IDE instance naturally serves arbitrarily many agent sessions -* **Enhanced performance**: Faster tool execution thanks to optimized IDE integration -* **Multi-language excellence** and **framework support**: First-class support for polyglot projects with multiple languages +* **Enhanced retrieval & refactoring capabilities**: The plugin adds additional [tools](../01-about/035_tools) (e.g. type + hierarchy retrieval, move, find declaration, inline symbol, etc.) + and transforms the underlying mechanisms of shared tools to build upon the IDE's capabilities. +* **Improved multi-agent support**: A single IDE instance naturally serves arbitrarily many agent sessions without requiring additional resources. +* **Enhanced performance**: Faster tool execution thanks to optimized IDE integration. +* **Multi-language excellence** and **framework support**: First-class support for polyglot projects with multiple languages. and frameworks (whatever is recognised by your IDE as a symbol will also be available to Serena) -* **No additional setup**: No need to download or configure separate language servers +* **No additional setup**: No need to download or configure separate language servers. -We are also working on additional features like a `move_symbol` tool and debugging-related capabilities that +We are also working on additional features like debugging and advanced introspection capabilities, which will be available exclusively through the JetBrains plugin. +(configure-jetbrains)= ## Configuring Serena to Use the JetBrains Plugin After installing the plugin, you need to configure Serena to use it. @@ -82,15 +89,16 @@ you will see `Languages: Using JetBrains backend` in the configuration overview. You will also notice that your client will use the JetBrains-specific tools like `jet_brains_find_symbol` and others like it. +(jetbrains-workflow)= ## Workflow 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 -2. Open the project's root folder as a project in Serena (see [Project Creation](project-creation-indexing) and [Project Activation](project-activation)) +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 tools as usual -Note that the folder that is open in your IDE and the project's root folder must match. +Note that the project folder that is open in your IDE and the Serena project root folder must match. :::{tip} If you need to work on multiple projects in the same agent session, create a monorepo folder From d214de40b4295ee438875ff2a5b23a0ebf052031 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 2 Apr 2026 21:43:13 +0200 Subject: [PATCH 018/190] Update JetBrains tools for v1 * Add missing descriptions * Add missing tool markers * Partly improve apply docstrings --- src/serena/tools/jetbrains_tools.py | 47 +++++++++++++++++------------ 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index afb70283..86f2b571 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -114,6 +114,10 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): class JetBrainsMoveSymbolTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): + """ + Moves a symbol, file or directory to a new location using the JetBrains backend, updating all references + """ + def apply( self, name_path: str | None = None, @@ -131,7 +135,6 @@ class JetBrainsMoveSymbolTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): If None, the symbol is moved to the top level of the target file. :param target_relative_path: the relative path to the target file. If None, the symbol is moved within the same file. - :return: JSON string with the result of the operation. """ with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.move_symbol( @@ -144,6 +147,10 @@ class JetBrainsMoveSymbolTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): class JetBrainsSafeDeleteTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): + """ + Safely deletes a symbol using the JetBrains backend, checking for remaining usages first + """ + def apply( self, relative_path: str, @@ -162,7 +169,6 @@ class JetBrainsSafeDeleteTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): Default is False (safe mode: will report usages instead of deleting). :param propagate: whether to propagate the deletion to usages of the symbol and also remove symbols that become unused after the deletion. Default is False. - :return: JSON string with the result of the operation. """ with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.safe_delete( @@ -175,6 +181,10 @@ class JetBrainsSafeDeleteTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): class JetBrainsInlineSymbol(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): + """ + Inlines a symbol using the JetBrains backend, replacing all call sites with the symbol's body + """ + def apply( self, name_path: str, @@ -182,16 +192,14 @@ class JetBrainsInlineSymbol(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): keep_definition: bool = False, ) -> str: """ - Inlines a symbol (usually method, but also classes may be amenable to inlining, + Inlines a symbol (usually a method/function, but also classes may be amenable to inlining, which turns invocation into anonymous class creation), - replacing all call sites with the method's body. - An error is raised when requested on a symbol that cannot be inlined. + replacing all call sites with the symbol's body. - :param name_path: the name path of the method to inline. - :param relative_path: the relative path to the file containing the method to inline. + :param name_path: the name path of the symbol to inline. + :param relative_path: the relative path to the file containing the symbol to inline. :param keep_definition: whether to keep the original method definition after inlining all call sites. - May be ignored in some cases (e.g., when inlining a class).. - :return: JSON string with the result of the operation. + May be ignored in some cases (e.g. when inlining a class). """ with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.inline_symbol( @@ -217,14 +225,12 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMark max_answer_chars: int = -1, ) -> str: """ - Finds symbols that reference the symbol at the given `name_path`. + Finds symbols that reference the symbol at the specified symbol, i.e. returns symbols whose definitions (e.g. a function body) contain a reference to the given symbol. The result will contain metadata about the referencing symbols. - :param name_path: name path of the symbol for which to find references; matching logic as described in find symbol tool. - :param relative_path: the relative path to the file containing the symbol for which to find references. - Note that here you can't pass a directory but must pass a file. + :param name_path: name path of the symbol for which to find references + :param relative_path: the relative path to the file containing the symbol (must be a file, not a directory) :param max_answer_chars: max characters for the result (-1 for default). If exceeded, no content/a shortened result is returned. - :return: a list of JSON objects with the symbols referencing the requested symbol """ with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.find_references( @@ -279,7 +285,6 @@ class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOp :param depth: depth up to which descendants shall be retrieved (e.g., use 1 to also retrieve immediate children). :param max_answer_chars: max characters for the result (-1 for default). If exceeded, no content/a shortened result is returned. :param include_file_documentation: whether to include the file's docstring. Default False. - :return: a JSON object containing the symbols grouped by kind in a compact format. """ with JetBrainsPluginClient.from_project(self.project) as client: symbol_overview = client.get_symbols_overview( @@ -420,7 +425,7 @@ class JetBrainsTypeHierarchyTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptiona return self._limit_length(result, max_answer_chars) -class JetBrainsFindDeclarationTool(Tool, ToolMarkerSymbolicRead): +class JetBrainsFindDeclarationTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): """ Finds the declaration of a symbol using the JetBrains backend """ @@ -449,7 +454,7 @@ class JetBrainsFindDeclarationTool(Tool, ToolMarkerSymbolicRead): return result -class JetBrainsFindImplementationsTool(Tool, ToolMarkerSymbolicRead): +class JetBrainsFindImplementationsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): """ Finds the implementations of a symbol using the JetBrains backend """ @@ -471,10 +476,14 @@ class JetBrainsFindImplementationsTool(Tool, ToolMarkerSymbolicRead): return result -class JetBrainsRenameTool(Tool, ToolMarkerSymbolicEdit): +class JetBrainsRenameTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): + """ + Renames a symbol, file or directory throughout the codebase using the JetBrains backend. + """ + def apply(self, relative_path: str, new_name: str, name_path: str | None = None) -> str: """ - Renames a symbol, file, or directory throughout the codebase. + Renames a symbol, file or directory throughout the codebase. :param relative_path: if `name_path` is passed, the relative path of the file containing the symbol. Otherwise, the path to the directory or file to rename. From efabaa634661864e7141df5c8783ed95a2eadf59 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 2 Apr 2026 22:17:21 +0200 Subject: [PATCH 019/190] InitialInstructionsTool: Shorten tool description (for auto-generated tools list) --- src/serena/tools/workflow_tools.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/serena/tools/workflow_tools.py b/src/serena/tools/workflow_tools.py index 158170a4..343f26e2 100644 --- a/src/serena/tools/workflow_tools.py +++ b/src/serena/tools/workflow_tools.py @@ -120,10 +120,8 @@ class PrepareForNewConversationTool(Tool): class InitialInstructionsTool(Tool, ToolMarkerDoesNotRequireActiveProject): """ - Provides instructions on how to use the Serena toolbox. - Should only be used in settings where the system prompt is not read automatically by the client. - - NOTE: Some MCP clients (including Claude Desktop) do not read the system prompt automatically! + Provides instructions Serena usage (i.e. the 'Serena Instructions Manual') + for clients that do not read the initial instructions when the MCP server is connected. """ def apply(self) -> str: From 0f479a0ee234efa9f1b058ce8231c723992b5b27 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 3 Apr 2026 11:46:21 +0200 Subject: [PATCH 020/190] Update instructions of JetBrainsMoveTool (and renamed to remove 'Symbol' from name) --- src/serena/tools/jetbrains_tools.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 86f2b571..fde4aeee 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -113,28 +113,39 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): return self._limit_length(result, max_answer_chars, shortened_result_factories=[create_shortened_result]) -class JetBrainsMoveSymbolTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): +class JetBrainsMoveTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): """ Moves a symbol, file or directory to a new location using the JetBrains backend, updating all references """ def apply( self, - name_path: str | None = None, relative_path: str | None = None, - target_parent_name_path: str | None = None, + name_path: str | None = None, target_relative_path: str | None = None, + target_parent_name_path: str | None = None, ) -> str: """ - Moves a symbol to a different location (file or parent symbol) using the JetBrains refactoring engine. - References to the symbol are automatically updated. + Moves a symbol, file or directory to a different location. The target location is the new "parent" + of the symbol, i.e. the moved entity is never renamed by the operation, only moved. + References to affected symbols are automatically updated. + + Valid moves: + - Symbol: + * (relative_path, name_path) -> new parent symbol (relative path + name path) + * (relative_path, name_path) -> top level of target file or directory (target_relative_path) + Always consider the concrete language-specific semantics! + - file: valid for languages like Python, where files are modules + - directory: valid for languages like Java, where directories are packages and can contain classes + - File or directory: + * relative_path -> new parent directory (target_relative_path) - :param name_path: the name path of the symbol to move (e.g. "MyClass/my_method"). :param relative_path: the relative path to the file containing the symbol to move. - :param target_parent_name_path: the name path of the target parent symbol (e.g. "TargetClass"). - If None, the symbol is moved to the top level of the target file. + :param name_path: the name path of the symbol to move (e.g. "MyClass/my_method"). :param target_relative_path: the relative path to the target file. If None, the symbol is moved within the same file. + :param target_parent_name_path: the name path of the target parent symbol (e.g. "TargetClass"). + If None, the symbol is moved to the top level of the target file. """ with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.move_symbol( From 6870c520310bdab2796cfa6127441d1697221952 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 3 Apr 2026 11:45:51 +0200 Subject: [PATCH 021/190] Update 'jetbrains' mode definition --- .../resources/config/internal_modes/jetbrains.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/serena/resources/config/internal_modes/jetbrains.yml b/src/serena/resources/config/internal_modes/jetbrains.yml index f7b2dd85..a08b189f 100644 --- a/src/serena/resources/config/internal_modes/jetbrains.yml +++ b/src/serena/resources/config/internal_modes/jetbrains.yml @@ -1,6 +1,7 @@ description: JetBrains tools replace language server-based tools prompt: | - You have access to the very powerful JetBrains tools for symbolic operations: + You have access to the very powerful JetBrains tools for symbolic operations. + These party replace regular tools you were informed about: * `jet_brains_find_symbol` replaces `find_symbol` * `jet_brains_find_referencing_symbols` replaces `find_referencing_symbols` * `jet_brains_get_symbols_overview` replaces `get_symbols_overview` @@ -12,11 +13,13 @@ excluded_tools: - safe_delete_symbol - rename_symbol included_optional_tools: - - jet_brains_find_symbol + - jet_brains_find_declaration + - jet_brains_find_implementations - jet_brains_find_referencing_symbols + - jet_brains_find_symbol - jet_brains_get_symbols_overview - - jet_brains_move_symbol - - jet_brains_type_hierarchy - - jet_brains_safe_delete - jet_brains_inline_symbol + - jet_brains_move + - jet_brains_type_hierarchy - jet_brains_rename + - jet_brains_safe_delete From c7df1167ae4515fb6eb93b084c2370cae1ceb8b3 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 3 Apr 2026 13:50:45 +0200 Subject: [PATCH 022/190] Fix: exclude project memories from initial instructions if read_memory is disabled --- src/serena/agent.py | 45 ++++++++++++++++++++++++++++++-- src/serena/project.py | 23 +--------------- src/serena/tools/config_tools.py | 4 +-- 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 91431192..c5cec083 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -2,6 +2,7 @@ The Serena Model Context Protocol (MCP) Server """ +import json import multiprocessing import os import platform @@ -35,7 +36,16 @@ from serena.ls_manager import LanguageServerManager from serena.project import MemoriesManager, Project from serena.prompt_factory import SerenaPromptFactory from serena.task_executor import TaskExecutor -from serena.tools import ActivateProjectTool, GetCurrentConfigTool, OpenDashboardTool, ReplaceContentTool, Tool, ToolMarker, ToolRegistry +from serena.tools import ( + ActivateProjectTool, + GetCurrentConfigTool, + OpenDashboardTool, + ReadMemoryTool, + 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 @@ -81,6 +91,9 @@ class AvailableTools: def contains_tool_name(self, tool_name: str) -> bool: return tool_name in self._tool_name_set + def contains_tool_class(self, tool_class: type[Tool]) -> bool: + return self.contains_tool_name(tool_class.get_name_from_cls()) + class ToolSet: """ @@ -648,11 +661,39 @@ class SerenaAgent: # If a project is active at startup, append its activation message if self._active_project is not None: - system_prompt += "\n\n" + self._active_project.get_activation_message() + system_prompt += "\n\n" + self.get_project_activation_message() log.info("System prompt:\n%s", system_prompt) return system_prompt + def get_project_activation_message(self) -> str: + """ + :return: a message providing information about the project upon activation (e.g. programming language, memories, initial prompt) + :raise: AssertionError if no project is active + """ + proj = self._active_project + assert proj is not None, "A project must be active before calling this." + if proj.is_newly_created: + msg = f"Created and activated a new project with name '{proj.project_name}' at {proj.project_root}. " + else: + msg = f"The project with name '{proj.project_name}' at {proj.project_root} is activated." + if self._language_backend == LanguageBackend.LSP: + languages_str = ", ".join([lang.value for lang in proj.project_config.languages]) + msg += f"\nProgramming languages: {languages_str}." + msg += "File encoding: {proj.project_config.encoding}." + + include_memories = self._active_tools.contains_tool_class(ReadMemoryTool) + if include_memories: + project_memories = proj.memories_manager.list_project_memories() + if project_memories: + msg += ( + f"\n{json.dumps(project_memories.to_dict())}\n" + + "Use the `read_memory` tool to read these memories later if they are relevant to the task." + ) + if proj.project_config.initial_prompt: + msg += f"\nAdditional project-specific instructions:\n {proj.project_config.initial_prompt}" + return msg + def _update_active_modes(self) -> None: """ Updates the active modes based on the Serena configuration, the active project configuration (if any), diff --git a/src/serena/project.py b/src/serena/project.py index 5995c3e7..a20623e3 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -1,4 +1,3 @@ -import json import logging import os import re @@ -291,7 +290,7 @@ class Project(ToStringMixin): self.language_server_manager: LanguageServerManager | None = None self._language_server_manager_init_error: Exception | None = None - self._is_newly_created = is_newly_created + self.is_newly_created = is_newly_created self._agent: Optional["SerenaAgent"] = None # create .gitignore file in the project's Serena data folder if not yet present @@ -379,26 +378,6 @@ class Project(ToStringMixin): def path_to_project_yml(self) -> str: return self.serena_config.get_project_yml_location(self.project_root) - def get_activation_message(self) -> str: - """ - :return: a message providing information about the project upon activation (e.g. programming language, memories, initial prompt) - """ - if self._is_newly_created: - msg = f"Created and activated a new project with name '{self.project_name}' at {self.project_root}. " - else: - msg = f"The project with name '{self.project_name}' at {self.project_root} is activated." - languages_str = ", ".join([lang.value for lang in self.project_config.languages]) - msg += f"\nProgramming languages: {languages_str}; file encoding: {self.project_config.encoding}" - project_memories = self.memories_manager.list_project_memories() - if project_memories: - msg += ( - f"\nAvailable project memories: {json.dumps(project_memories.to_dict())}\n" - + "Use the `read_memory` tool to read these memories later if they are relevant to the task." - ) - if self.project_config.initial_prompt: - msg += f"\nAdditional project-specific instructions:\n {self.project_config.initial_prompt}" - return msg - def read_file(self, relative_path: str) -> str: """ Reads a file relative to the project root. diff --git a/src/serena/tools/config_tools.py b/src/serena/tools/config_tools.py index 7a0472cf..bdab4706 100644 --- a/src/serena/tools/config_tools.py +++ b/src/serena/tools/config_tools.py @@ -28,8 +28,8 @@ class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject): :param project: the name of a registered project to activate or a path to a project directory """ - active_project = self.agent.activate_project_from_path_or_name(project) - result = active_project.get_activation_message() + self.agent.activate_project_from_path_or_name(project) + result = self.agent.get_project_activation_message() result += "\nIMPORTANT: If you have not yet read the 'Serena Instructions Manual', do it now before continuing!" return result From 87accc6dd7b2cd22b6bbaa2d187766951cd765d5 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 3 Apr 2026 13:54:34 +0200 Subject: [PATCH 023/190] Fix lifecycle of dashboard viewer not being tied to main process --- src/serena/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index c5cec083..1f6c09ae 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -592,7 +592,7 @@ class SerenaAgent: stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - start_new_session=True, # Detach from parent process + start_new_session=False, ) def get_exposed_tool_instances(self) -> list["Tool"]: From 1ad26d5af317abd568a0ae35d066aeccf4151b26 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 3 Apr 2026 14:37:13 +0200 Subject: [PATCH 024/190] Fix: apply exclusions from base modes to exposed tools --- src/serena/agent.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index c5cec083..e73bd7a8 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -239,6 +239,13 @@ class ActiveModes: self._active_modes.append(mode) return self._active_modes + # TODO: apply caching like in get_modes + def get_default_modes(self) -> Sequence[SerenaAgentMode]: + return [SerenaAgentMode.load(mode_name) for mode_name in self._default_modes or []] + + def get_base_modes(self) -> Sequence[SerenaAgentMode]: + return [SerenaAgentMode.load(mode_name) for mode_name in self._base_modes or []] + class SerenaAgent: def __init__( @@ -371,7 +378,7 @@ class SerenaAgent: self.serena_config, self._language_backend, self._context, self._active_modes, self._active_project ) self._exposed_tools = self._base_toolset.to_available_tools(self._all_tools) - log.info(f"Number of exposed tools: {len(self._exposed_tools)}") + log.info(f"Number of exposed tools: {len(self._exposed_tools)}. Exposed tools: {self._exposed_tools.tool_names}") # update the active tools (considering the active project, if any) self._active_tools: AvailableTools @@ -427,11 +434,20 @@ class SerenaAgent: # consider modes # Since modes can be dynamically turned on and off, we don't include their definitions directly, - # but for the initially active modes, we make sure that the tools they enable are included. - for mode in modes.get_modes(): + # For the initially active dynamic modes, we make sure that the tools they enable are included. + for mode in modes.get_default_modes(): tool_inclusion_definitions.append( NamedToolInclusionDefinition( - name=f"InitialModeInclusions[{mode.name}]", included_optional_tools=mode.included_optional_tools + name=f"InitialDynamicModeInclusions[{mode.name}]", included_optional_tools=mode.included_optional_tools + ) + ) + # For the base modes, we also apply the tool exclusions, since they apply throughout the entire session + for base_mode in modes.get_base_modes(): + tool_inclusion_definitions.append( + NamedToolInclusionDefinition( + name=f"BaseMode[{base_mode.name}]", + included_optional_tools=base_mode.included_optional_tools, + excluded_tools=base_mode.excluded_tools, ) ) From da105faad160a73f3a4d6aa504fe3db0952bd40d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 3 Apr 2026 14:59:19 +0200 Subject: [PATCH 025/190] Improved MoveTool description --- src/serena/tools/jetbrains_tools.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index fde4aeee..dc6f41a3 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -120,32 +120,30 @@ class JetBrainsMoveTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): def apply( self, - relative_path: str | None = None, + relative_path: str, name_path: str | None = None, target_relative_path: str | None = None, target_parent_name_path: str | None = None, ) -> str: """ - Moves a symbol, file or directory to a different location. The target location is the new "parent" + Moves a symbol, file or directory to a different location. The target location is the new parent of the symbol, i.e. the moved entity is never renamed by the operation, only moved. References to affected symbols are automatically updated. Valid moves: - Symbol: - * (relative_path, name_path) -> new parent symbol (relative path + name path) + * (relative_path, name_path) -> new parent symbol (target_relative_path, target_parent_name_path) * (relative_path, name_path) -> top level of target file or directory (target_relative_path) Always consider the concrete language-specific semantics! - - file: valid for languages like Python, where files are modules - - directory: valid for languages like Java, where directories are packages and can contain classes + - target is a file: valid for languages like Python, where files are modules + - target is a directory: valid for languages like Java, where directories are packages and can contain classes - File or directory: * relative_path -> new parent directory (target_relative_path) :param relative_path: the relative path to the file containing the symbol to move. - :param name_path: the name path of the symbol to move (e.g. "MyClass/my_method"). - :param target_relative_path: the relative path to the target file. If None, the symbol - is moved within the same file. - :param target_parent_name_path: the name path of the target parent symbol (e.g. "TargetClass"). - If None, the symbol is moved to the top level of the target file. + :param name_path: the name path of the symbol to move (empty for moving file or dir). + :param target_relative_path: the relative path of the target directory or file. + :param target_parent_name_path: the name path of the target parent symbol. """ with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.move_symbol( From c65b9aaaac200635e71680166c62a6d9d0331c48 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 3 Apr 2026 15:21:32 +0200 Subject: [PATCH 026/190] Dashboard: enable text_select and zoom --- src/serena/dashboard.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 9a8f3f50..cf450891 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -742,6 +742,9 @@ class SerenaDashboardViewer: width=self.width, height=self.height, hidden=self.start_minimized, + text_select=True, + zoomable=True, + ) assert window is not None self.window = window From e39830df4d819822ed4967e4b25057d1508ff355 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 3 Apr 2026 15:25:35 +0200 Subject: [PATCH 027/190] Minor improvement in find_declaration docstring --- src/serena/dashboard.py | 1 - src/serena/tools/jetbrains_tools.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index cf450891..257b8540 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -744,7 +744,6 @@ class SerenaDashboardViewer: hidden=self.start_minimized, text_select=True, zoomable=True, - ) assert window is not None self.window = window diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index dc6f41a3..fdffa191 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -446,8 +446,8 @@ class JetBrainsFindDeclarationTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptio :param relative_path: the relative path to the source file containing the symbol for which to find the declaration. :param regex: a regular expression with one group, where the group matches the symbol for which to perform the lookup. For example, to find the declaration of the `process` method in a call like `obj.process()`, - pass an expression like "obj\.(process)\(\)". - Provide as much context as necessary to render the regex unambiguous. + pass an expression like "obj\.(process)\(process_input_arg=37\)". + Prefer regexes with sufficiently large context around the group to render the match unambiguous. Uses Python syntax with MULTILINE and DOTALL flags enabled. :param include_body: whether to include the symbol's body in the result. Default False. """ From 60067f327433c9a4a5f3897acf705299bfca13ca Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 3 Apr 2026 15:05:36 +0200 Subject: [PATCH 028/190] Mark some new tools as beta tools and denote in tool list --- docs/autogen_docs.py | 11 ++++++++--- src/serena/tools/jetbrains_tools.py | 8 ++++---- src/serena/tools/tools_base.py | 10 +++++++++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/docs/autogen_docs.py b/docs/autogen_docs.py index 8c828809..6dfd37f0 100644 --- a/docs/autogen_docs.py +++ b/docs/autogen_docs.py @@ -153,9 +153,10 @@ def autogen_tool_list(target_filename = "01-about/035_tools.md"): with open(target_file, "w") as f: f.write("\n\n") f.write("# Tools\n\n") - f.write("Find the full list of Serena's tools below. \n") + f.write("Find the full list of Serena's tools below.\n\n") f.write("Note that in most configurations, only a subset of these tools will be enabled simultaneously.\n") - f.write("Tools marked as optional are disabled by default.\n\n") + f.write("Tools marked as *optional* are disabled by default.\n\n") + f.write("Tools marked as [BETA] were recently introduced and may not be fully robust yet.\n\n") tools_by_module = ToolRegistry().get_registered_tools_by_module() priority_modules = {"serena.tools.symbol_tools": 1, "serena.tools.jetbrains_tools": 2} @@ -166,7 +167,11 @@ def autogen_tool_list(target_filename = "01-about/035_tools.md"): module = module.replace("serena.tools.", "") text.with_line(f"* **{module}**") for tool in tools: - info = " *(optional)*" if tool.is_optional else "" + info = "" + if tool.is_optional: + info += " *(optional)*" + if tool.is_beta: + info += " [BETA]" text.with_line(f"* `{tool.tool_name}`{info}: {tool.class_docstring}", indent=2) f.write(text.build()) diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index fdffa191..1c4ef63f 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -7,7 +7,7 @@ from serena.code_editor import JetBrainsCodeEditor from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient from serena.jetbrains.jetbrains_types import SymbolDTO from serena.symbol import JetBrainsSymbolDictGrouper -from serena.tools import Tool, ToolMarkerOptional, ToolMarkerSymbolicEdit, ToolMarkerSymbolicRead +from serena.tools import Tool, ToolMarkerBeta, ToolMarkerOptional, ToolMarkerSymbolicEdit, ToolMarkerSymbolicRead from serena.util.text_utils import find_text_coordinates log = logging.getLogger(__name__) @@ -113,7 +113,7 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): return self._limit_length(result, max_answer_chars, shortened_result_factories=[create_shortened_result]) -class JetBrainsMoveTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): +class JetBrainsMoveTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, ToolMarkerBeta): """ Moves a symbol, file or directory to a new location using the JetBrains backend, updating all references """ @@ -155,7 +155,7 @@ class JetBrainsMoveTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): return self._to_json(response_dict) -class JetBrainsSafeDeleteTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): +class JetBrainsSafeDeleteTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, ToolMarkerBeta): """ Safely deletes a symbol using the JetBrains backend, checking for remaining usages first """ @@ -189,7 +189,7 @@ class JetBrainsSafeDeleteTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): return self._to_json(response_dict) -class JetBrainsInlineSymbol(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): +class JetBrainsInlineSymbol(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, ToolMarkerBeta): """ Inlines a symbol using the JetBrains backend, replacing all call sites with the symbol's body """ diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index 200c2ae3..cb2b299d 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -110,6 +110,12 @@ class ToolMarkerSymbolicEdit(ToolMarkerCanEdit): """ +class ToolMarkerBeta(ToolMarker): + """ + Marker for tools that are considered beta features (may not be fully robust) + """ + + class ApplyMethodProtocol(Protocol): """Callable protocol for the apply method of a tool.""" @@ -407,6 +413,7 @@ class EditedFileContext: class RegisteredTool: tool_class: type[Tool] is_optional: bool + is_beta: bool tool_name: str @property @@ -429,10 +436,11 @@ class ToolRegistry: if not any(cls.__module__.startswith(pkg) for pkg in tool_packages): continue is_optional = issubclass(cls, ToolMarkerOptional) + is_beta = issubclass(cls, ToolMarkerBeta) name = cls.get_name_from_cls() if name in self._tool_dict: raise ValueError(f"Duplicate tool name found: {name}. Tool classes must have unique names.") - self._tool_dict[name] = RegisteredTool(tool_class=cls, is_optional=is_optional, tool_name=name) + self._tool_dict[name] = RegisteredTool(tool_class=cls, is_optional=is_optional, tool_name=name, is_beta=is_beta) def get_registered_tools_by_module(self) -> dict[str, list[RegisteredTool]]: """ From 83bd14b38b1ce92a37970788e0e242e6c30eb551 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 3 Apr 2026 15:35:03 +0200 Subject: [PATCH 029/190] Mention beta features on JetBrains docs page --- docs/02-usage/025_jetbrains_plugin.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/02-usage/025_jetbrains_plugin.md b/docs/02-usage/025_jetbrains_plugin.md index b6da5037..c8af4a14 100644 --- a/docs/02-usage/025_jetbrains_plugin.md +++ b/docs/02-usage/025_jetbrains_plugin.md @@ -14,15 +14,15 @@ leverage the powerful code analysis and editing capabilities of your JetBrains I We recommend the JetBrains plugin as the preferred way of using Serena, especially for users of JetBrains IDEs. +How it works: +1. Install the plugin in your JetBrains IDE +2. Configure Serena to use the JetBrains language backend (see [below](configure-jetbrains)) +3. Open the project you want to work on in your JetBrains IDE and activate it in Serena (see [below](jetbrains-workflow)) +4. Start coding via your MCP client as usual + **Purchasing the JetBrains Plugin supports the Serena project.** The proceeds from plugin sales allow us to dedicate more resources to further developing and improving Serena. -How it works: - 1. Install the plugin in your JetBrains IDE - 2. Configure Serena to use the JetBrains language backend (see [below](configure-jetbrains)) - 3. Open the project you want to work on in your JetBrains IDE and activate it in Serena (see [below](jetbrains-workflow)) - 4. Start coding via your MCP client as usual - ## Advantages of the JetBrains Plugin There are multiple features that are only available when using the JetBrains plugin: @@ -40,6 +40,12 @@ There are multiple features that are only available when using the JetBrains plu We are also working on additional features like debugging and advanced introspection capabilities, which will be available exclusively through the JetBrains plugin. +:::{note} +With Serena's JetBrains tools, we try to offer the latest features. +As a result, some of them are considered as beta features (see [tool list](../01-about/035_tools)), which may have some quirks. +Please report your experience with these tools if they do not work as expected. +::: + (configure-jetbrains)= ## Configuring Serena to Use the JetBrains Plugin From 827f98fde986075b5148a381c46f02e98ee99000 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 3 Apr 2026 16:06:20 +0200 Subject: [PATCH 030/190] Add news item for v1 --- .../resources/dashboard/news/20260404.html | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/serena/resources/dashboard/news/20260404.html diff --git a/src/serena/resources/dashboard/news/20260404.html b/src/serena/resources/dashboard/news/20260404.html new file mode 100644 index 00000000..debdfb63 --- /dev/null +++ b/src/serena/resources/dashboard/news/20260404.html @@ -0,0 +1,23 @@ +
+

Serena v1.0 Is Here!

+

April 3, 2026

+

+ After a full year of work since the first version of Serena, we are excited to announce the release of Serena v1.0! +

+

+ Since our last update, we have now added support for exciting new retrieval and refactoring tools, especially for users of the JetBrains backend. +

+
    +
  • safe_delete, jet_brains_safe_delete (beta):
    Safely deletes a symbol, checking for remaining usages first
  • +
  • jet_brains_move (beta):
    Moves a symbol, file or directory to a new location, updating all references
  • +
  • jet_brains_find_declaration:
    Finds the declaration of a symbol using the JetBrains backend
  • +
  • jet_brains_find_implementations:
    Finds the implementations of a symbol (e.g. abstract method)
  • +
  • jet_brains_inline_symbol (beta):
    Inlines a symbol, replacing all call sites with the symbol’s body
  • +
+

+ We are far from done. Many new features will be coming your way – for both the LSP and JetBrains backends. +

+

+ Thank you to our contributors, users and customers! +

+
\ No newline at end of file From 0f6d46afeef2d81500175f506352f020f644f882 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 3 Apr 2026 16:06:58 +0200 Subject: [PATCH 031/190] Prompt, minor --- src/serena/resources/config/prompt_templates/system_prompt.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/serena/resources/config/prompt_templates/system_prompt.yml b/src/serena/resources/config/prompt_templates/system_prompt.yml index 125a6818..de648821 100644 --- a/src/serena/resources/config/prompt_templates/system_prompt.yml +++ b/src/serena/resources/config/prompt_templates/system_prompt.yml @@ -3,7 +3,6 @@ # is encouraged (via the tool description) to call at the beginning of the conversation. prompts: system_prompt: | - You are a professional coding agent. You have access to semantic coding tools upon which you rely heavily for all your work. You operate in a resource-efficient and intelligent manner, always keeping in mind to not read or generate content that is not needed for the task at hand. From f65980bd8cb9d71d744f1fb06cae9018902bdf4e Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 3 Apr 2026 16:16:58 +0200 Subject: [PATCH 032/190] Set version to 1.0.0 --- pyproject.toml | 2 +- src/serena/__init__.py | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3d693eb9..680d8599 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["hatchling"] [project] name = "serena-agent" -version = "0.1.4" +version = "1.0.0" description = "" authors = [{ name = "Oraios AI", email = "info@oraios-ai.de" }] readme = "README.md" diff --git a/src/serena/__init__.py b/src/serena/__init__.py index a2c1cf0a..3ab32039 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.1.4" +__version__ = "1.0.0" import logging diff --git a/uv.lock b/uv.lock index 4151f3ae..ec3f4bd7 100644 --- a/uv.lock +++ b/uv.lock @@ -2906,7 +2906,7 @@ wheels = [ [[package]] name = "serena-agent" -version = "0.1.4" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "anthropic" }, From 3c77e362caddc956dc280038f4c3f9b8c6c569d9 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 3 Apr 2026 16:57:19 +0200 Subject: [PATCH 033/190] JB client: added version check to new methods, renamed move_symbol to move --- src/serena/jetbrains/jetbrains_plugin_client.py | 7 ++++++- src/serena/tools/jetbrains_tools.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/serena/jetbrains/jetbrains_plugin_client.py b/src/serena/jetbrains/jetbrains_plugin_client.py index f3ed6dc9..810618ba 100644 --- a/src/serena/jetbrains/jetbrains_plugin_client.py +++ b/src/serena/jetbrains/jetbrains_plugin_client.py @@ -425,13 +425,14 @@ class JetBrainsPluginClient(ToStringMixin): self._postprocess_symbol_collection_response(symbol_collection) return symbol_collection - def move_symbol( + def move( self, name_path: str | None, relative_path: str | None, target_parent_name_path: str | None, target_relative_path: str | None, ) -> dict[str, Any]: + self._require_version_at_least(2023, 2, 14) request_data = { "namePath": name_path, "relativePath": relative_path, @@ -527,6 +528,7 @@ class JetBrainsPluginClient(ToStringMixin): :param relative_path: the relative path to the file containing the symbol :param delete_even_if_used: if True, delete the symbol even if it has usages """ + self._require_version_at_least(2023, 2, 14) request_data = { "namePath": name_path, "relativePath": relative_path, @@ -548,6 +550,7 @@ class JetBrainsPluginClient(ToStringMixin): :param relative_path: the relative path to the file containing the method :param keep_definition: if True, keep the original method definition after inlining """ + self._require_version_at_least(2023, 2, 14) request_data = { "namePath": name_path, "relativePath": relative_path, @@ -599,6 +602,7 @@ class JetBrainsPluginClient(ToStringMixin): :param include_body: whether to include the symbol body :param include_quick_info: whether to include quick info about the symbol """ + self._require_version_at_least(2023, 2, 14) request_data = { "relativePath": relative_path, "line": line, @@ -618,6 +622,7 @@ class JetBrainsPluginClient(ToStringMixin): :param name_path: the name path of the symbol :param include_quick_info: whether to include quick info about the symbol """ + self._require_version_at_least(2023, 2, 14) request_data = { "relativePath": relative_path, "namePath": name_path, diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 1c4ef63f..00ae3a51 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -146,7 +146,7 @@ class JetBrainsMoveTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, ToolMa :param target_parent_name_path: the name path of the target parent symbol. """ with JetBrainsPluginClient.from_project(self.project) as client: - response_dict = client.move_symbol( + response_dict = client.move( name_path=name_path, relative_path=relative_path, target_parent_name_path=target_parent_name_path, From 9f28a2d75f5d05d5279c73d2992f700db398f6d9 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 4 Apr 2026 10:14:52 +0200 Subject: [PATCH 034/190] Restore LLM interaction section, adding block diagram --- README.md | 19 +++++++++++++++++++ resources/serena-block-diagram.afdesign | Bin 0 -> 47098 bytes resources/serena-block-diagram.svg | 1 + 3 files changed, 20 insertions(+) create mode 100644 resources/serena-block-diagram.afdesign create mode 100644 resources/serena-block-diagram.svg diff --git a/README.md b/README.md index feba7f7b..84519fa1 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,25 @@ approaches that rely on low-level concepts like line numbers or primitive search Practically, this means that your agent operates **faster, more efficiently and more reliably**, especially in larger and more complex codebases. +## How Serena Works + +Serena provides the necessary [tools](https://oraios.github.io/serena/01-about/035_tools.html) for coding workflows, +but an LLM is required to do the actual work, orchestrating tool use. + +Serena can extend the functionality of your existing AI client via the **model context protocol (MCP)**. +Most modern AI chat clients directly support MCP, including +* terminal-based clients like Claude Code, Codex, OpenCode, or Gemini-CLI, +* IDEs and IDE assistant plugins for VSCode, Cursor and JetBrains IDEs, +* desktop and local web clients like Claude Desktop or OpenWebUI + + + +To connect the Serena MCP server to your client, you typically + * provide the client with a launch command that starts the MCP server, or + * start the Serena MCP server yourself in HTTP mode and provide the client with the URL. + +See the [Quick Start](#quick-start) section below for information on how to get started. + ## Features Serena supports two code intelligence backends: diff --git a/resources/serena-block-diagram.afdesign b/resources/serena-block-diagram.afdesign new file mode 100644 index 0000000000000000000000000000000000000000..49b898a1f4f03e6e313e2023eebbbf4d5a1b18ed GIT binary patch literal 47098 zcmYhi1ymeO(*}yWySuwXAh^4`JHg$P#ogWAEx3iSc!IkJmta8x1mC&5-+#}!xAx5T z%$ez$p6;irpX!={0+c0Dq2Qogy*xE&6fn5XQb_ED&I6;Q=wk^X$HG>mSj1?5;~x#2&)wkNinXnK z`HIM_tuFXO2i1P&qH>#L&n8LAu|i!!#-Soa;Md2fSDKU#Tm&uaNN2Z-kDN$Wl2p+Hn3hnct_6ypajCdnO9<2&Ls|}TZj6Ce!fu#B3PLNK ztviA$yfLm55_!nL@C1zBRD%}R;V5)YJJH}EJhH)V6&1sMm@Z68UD3D9iPj#ug*-E6 z`rSRGt(;K>H6n?o6P`|0OLwBy(CagxjWPl50Yp3xTLH&xS#&Lhn@yJS@i7Etqq(>M z4nD*i#7*@k9;Is#+0VM!U-M$V3|QLvWiRsCEVb=>7*Ai-Fdt#!ONwGuk|o z1=Rc)8(B8Ke@?>vl%2y=wAf>DM}m-`$)9y8QC!BMUMzG!CWvUgTH;f!f^gX!d4s zi_w}uaTC+4gEBNavIKS^mmqE}L~v@FYb6Ihj*)eroQtP$kN9TNI>A8WT94Q&A^&DP z8~RRSPg<@QOoa6+cv=Jo(7hB_70$zm6%GeiTSV3X!c(v<*nsO5FiDuM1%$G1kCs6B z1xYU2fXC&^WNl`4=qqNJ&t*>>=ln7r0M3C>^37>LV?m6gj9Xz)K1<=A=AiP80@VGL z^~@0Q%ivV4x1!Q-e=V!0N!U%Je>vFZtW;x4eu$@)L+RBS_ZQ)q%6ieUOGY zA>s(|y9YL*NEYuNiHB>0aQmM6!d8b+W9VW(lykE?zPw;oKzm;k1f7S zdKS09S;8S|WDF(8ex)32LOAdMLRX?bGA!b1Q#P1s1ZFGcPG^Ah-|C-!173%D#sGR} z&mhr(O$bn*kioNY$1$7v=da27%1*PzGXxaS3+B4|>kU%%?zKG^SJRvjQQFE<=YJJZ z#y*>@1vtb0{`0KWR4Nj$EL`GOTGsw2uG{Li9gu#J7bMrzM-_n3OZ*7jtW}EKG;I|_CP!- zuWsq`&bTHL`)!t5cvSQ)L*)B>V`i z!iD%P7VvRq<;*$rQ;j@=_+fPSTW<5?+xenrd7Xp2UaYtNfoC)>@z(=Zr!IU|bsdc# zf7m6}0_IY{Rn7#Bn~{;y+JLA$S^U_YHYz=tyXQ+X z2VaO{3cs2Kv7dhcTW)RWX(*^YX&sO~(M1xIaImt^2*c!0tNA`PfJYQN*Zs)~i5hK* za&BgAAi-40XbupEf`P&m#+oMI{0G8#24N)l?4m>d{d5`q&&&byES-$jv~8jk;45~{ z|G_HR)avA+Mw-}UqUKA{t1!e`Lna#7JQfwXUBBh9Rcy=ZQFK#|@mXil9tN@-4>|9F zR^62xj3BdTivt^ zCpK}Bif#wMpuPMGP^ivmXwlhUH33c%bsAUs=)QOmz{RepcE`?P{n^Fta%jhnn=g3Q zs+vUX7{+cK{Qn-^PuPJqvCTY5DBc-^LH`YXhZ zM>iZ|E}zbufp@cWc(L^cN`mZgr|FbJ#N{STNXdwQ329 z4sMTEX0!g|ya60_xlg^-80dJK9`))g|wI z)>+;;|IVUP#nK1}B&%%$2Y2SZT>MjoeL6l~)hLmi`~ZO3@muX4;Px49Q8QLA#{w9& zGyj2|7_Z-ee2CkD6t%BZG7@d#NLT4^&8@uhThmU1(i1OU!&mBM?8v8gaUXD3` zmx>5{bl#dCnVkuaODwl7Z~oNhj0X|VT?o|RtSZKH^y4>E0c!+ZI`U~gSMB*!t24~Z z`E_ZJh5YiimZVzkq0{s)RL-J%1l~7cF1KSdiP`71C3aVS6HqhAe%Xb#9NLBJeG>q__WdF zp;%s_Ox1iQhjB4sAEX9$?~$nG=;gSuqHrYq=NrqBlA?Ss{O7mgdL1yQ|IFp~K#1Tc z*;G+x*vB~V{D(&Ijr(4?tWeT^UW~zT_{h<8UQ4(!M`LH%DGWz&xPHWrBHBhbZH4M2 zav(knl}Z&GL@7mkjzBZet8SGOr%&&yIt0+d+`AwO!x$Lqo__>96(zCMyLU&1f67Ft z!>4_@w)hnmg#fi{5sB>y-vEP)rX8X`02iW^L9C}{0mWFOK^4c{z^K-ML+ieBo z;AtiGux>;+B|(Fd1B*8-ka8=okGN;@*#Lm5vbxO*OseH={@#USfz2vi4s&u#))D2&O?0GdWljgHy1 z)rA#IS|Pt$m{L2p_Z%zDN;A2?{9~^}T6~^a*z2lAF#Tz=rteQGJqKvA9{*A(f`S<1 zW(nm~Om$5YaRn_QtBOP=mm)bkLA(EnXwu9RG^=ank9q8N$F0Xt z!Vdd6VEo1%&HFrwAzRNKk7m>Gu}*{A>J$o#qnT(bLCegzvm^}3MgC$PidjB|m2z&u zl`4@A=h`!bXAq4lD}|`d`_s+Gaj9@7c6TY&w1x;=`lQ$@_8L3Lca6XQh$DVvxUeBEgVWQn45L@6 zU=!oAh{iDxGI3F{u}+`$Y(DT_)BO0uT9FR9Gmj$1Ln)SBdn z-Y6xZL4OhINHCyiE^ntfo??=XMwhaILLi)$#DG)m-(~-Dg07?3?jSj!tUEeU&oGL& zRnC8MTxtVHP@;iRGB0A(QC7=hylw&LHkNzwC4E8oT+V%hx*=DuvGdf%|5Pl@p5tb| z6#VjctdM;kV14Rn<%+JAc6kK5RQ6iGiPvhQY&-$k_F@|W*-r|qJv@{h5OayM45O9l zV7nVJRdJ{G5Xp2Heoe^8yv~xXE#RXSf`BqZR48D9 zfM?S#=3h7%5%rN=!J?w{G(nAoi+yHoH=a|KKgFPYIKd+>rl^^b#M12rI-9czsmnhdZRn*UVjjcZe^RJXskEEv98nXDO(bMyBx$=A zN<@eQJyH=32vx9)p{E*>lQmy9IB2mD(bd%~EO8DKuw$&pRnXCre<26vl%Vm`Q-x~h zX-;S*kq}WR+a(a)9wO25TF{fa`wWp$S4K0DrL-<34_VDi6I<$GS2*uJzyW~#azZMj zx(u!7ltdJf7;%VWRn!hH6E;_K*3B4YVYfv~&3~-Ds<`TNXnYpz|Jc~oKqS&s-Fw#m5j>jSw(E3d=ib5+TO;U9fG$%8B=`J1G)Yq58MwuvO;^oav~-trcY%x&)yE-4^hA^%bbxRdpw`TV+$Jy*H%!63FCc@4aGT@d9w}GHIpp%}3yhoAqJM zk3>S>ok%|Xz#ZeFg~RqQ`8CzB1myk~jf@zhk|#HQq*vAgNUNHZwA^@+vYdEpraF4* zY;7JXtLZFR&wp?#=FZrSSl*60mQGi*qWoYI=3%V=>v2KCMW{<$-kzWvU)N`A6VRz5 zpVfG7!=Ll$hasdtq@Wc&liAEbGqY@=+Kga}At}$hO;8`iU~LC*vzos))ThC3DyAAn@aAa_Q3XZ%y;0Hy97FWK|bPd z%g;))R$WF31+koT+&`NKmGn;y8|dt<&_!7l$v;en+6%X;E;5`D*u)!w3Cg6HntlAtQ5P*A77{Q4_O>ZjZ>iHm5W z@mnf40EWu;RIY!5xx*{4qg8fyVK*c5`rsV(58|giYgfSnVQ;$$697PjTmeCTG!XM% z=NIs}jtd~Doz1Zs2XOM)oogLwteRRc@&UMT}VpQHecL3OjofUR_A?yN^G>i(M>vG9*p!&eaQe@b`3%BCE^ z6;YsEUESjj{A(A&BM9?#he5uMC8PF^H|%*9I%_M#Z4XX|a52;tBEn#n^GO~TK)bfvruPS4f>DrSVAqjSho-zZBz-pBraQUQcxCfp zE@%q1^tMv13^KL}Hh$gY+8wDn5K6h@DeAPoFWur_{8{Uic;I4LkUvb;rlCplz>Qmn zQSCOB9CO^UgMeg2L%sXdg8BfOE&SdmI);&Esy)h}$b5f|ckHMyu8=)4<#@`R4fkVM#l8z$#Q*UJy-pSnr7y2kNB;;v;+VV~7cW=%op7dMXqI6imYgWBG}lZ)BmfzN+8u21vt zqSSZHNFi5Ux_->BUGGf*f?V20Z$@VBmCPM(ALKKluD(vuy#gN_PqU(A&q9(f_m7=i>OO#Y zl$-1e(Sp@!I|HWa3vp0^90EQ+;AOSFqzCs!2k7e2-r>4h9f0PW%oN~UevP7JS!VmE zylgTAcq!>tk34_XMtF}1xbcr70g(rY(Ta!tjMd_oZZd&C_N>F3HmzDCs-CKlVg?<7 zryagh&$k`N#W1PH7@!K?77QdV0r{^-?*`c@vvT|9#`s?m`{<^TvS!J61k_t+4OV zy3dE-9e>%OKYL%Hr9W%!4K~(un&Q6!7$al!SoCytX|Wd*O+VMA#+>>d>)dPcirg9= zffEySciz(sWT;1~-#zRxd^MA%8!;OjSs|d)R^b%@%ch%<`F^K)SD!w%a=jCJai@jv zw?KDNQj!`=rqa>j;o=Mg!db_~IYDhNK)X-&aBhzv z;MVT-+bPbavx(yn*+UAY&lx)$3QI0yE)382+QZVGPd36kj@#M$1iLItPTW=SB{KSt zyJ0X`{`WhMcCMxye1jS}M?hWToo~sOrqAvTkDhX4BiY}z@B#3}X>bF;sa%M24v0*; zO~P-vLb}-X-)AnjV_xacoxGt#zvl#_IZe|EAhRXjL3Zz0S;>_AFPpTkXU642JjXu@ z2(Gq8J_nuT4%gRSF@4nDeyP;|aMzLRzG+Mo34p9U*v^enhA}oFX|=0RfnYR(g?u^AHLIa zTB67uh4k9GxNZe&yyvR}+`K6#f|nmMVHUD|*R882!<~&1eON$ae6AGGqJFNTukZ@G zbLc9n&xr1QRu6Dcz96QF_z?kFUlIk0ue^R;w5|qutA4isX646Pw*avoI1`^{eO5MY z&)z_KvG=mu0fZ!<#kaCP0|#&t_6Ot4DUjlR&TErerIQ_1!!kgSF5y}_;(cj;AsC{^ zIk7np8v!w2@+%V*I{{=(In~=8F2NX3&!EtYm$JU)IEa409Do{Zp78^=VO}!n&?AWK z4pc}=dY4>w49ce90RC%qwCCudx|(?qS+FGFfa4V5k|X_*HBR>p zM86-qbmDiMnMBI}>^Iv0vy++m_BGVU9p$$LS#Y?EJHWM5 zh(G3s&jNg8?q`KRB+d}rfk-Nwf*`Ys!xcAuOsLPmJcfUhj-cM0cwj`;F^CMm0ty|k zpk*upvg0kfQF)RU&+y$)G+A#GL@uf}C!V*XV>(~(-fU8a69AOR^Xxa@2Ll|`obJdb zX87-mt06n^Qe?=7iNTRTjbRxqyn;g#Ao&DPax+9$2~=oYGS{-eN4C@|`8>#XXXs%l zz6xX-XiFgrWg>v{x(vi35$q9x`3ds7Gj3U0To+1j(iRbqk7d=8x1Hz&c?{l`(+2&uF zZ&CsEyx`M6pd|D=P}R3gX!ULfOdowFxBJ!r+ev`Lp#&t5QzZ8rC_uXdK^C^2DCJR2 z$J00SV}o9<*q%*thLFmlxgh2P{VdKtuiHH^`Mr@hEv||9i#_%8xvT1vJ zTEy*`M@T&ec=WN`_ghGEcAOWya1Kek=qI?K2 zxe)z&z(2dQfddi$4FJwQ8|rs@&i@K0M}Ei8KOh|S4qO?-Iq-f@!0T_^ab{*?;020q zqMB(k^K-Q%N*-NViDd{LpLJP@rW6VZ`;)30>ylp7`ei~D2ZixyW9Y<>p zi(2+a{9%#!cHnl>NR-kBK&oXBI#lOyVYfd45s8C*Rl(z&KJuc z)|hnN$9X;hd61CGjJU|)*!;I6h^r5AV|o@x0Jq`8=++H*2|p#57uCD{e%rx60ffPA zpG&XbFZ%tmqkiXNzdv&IGnJN6E`f;KYUG^+Fd#B$>*5!dNZ_idSRm8W&HcbWdC&03 z6v%+k;GXU5=x?C^gg}fFW@4t^1aY=rj)-z!b}WtLA)bbckwfAux$Cbx8jZYdX^&00 z>u;d8j;En_W-Py3@;?@PSIfJ96n_^MyNsm(mRG27U0LPW%#oR1vFKTA{mDY zUDsszEl32gRdk1U90&L=447Xcfz8h2%;ieuiJ|N!6@pF`Iml4ejYZcrVBbbIK16KP z)-)TDXZT4wv8T1Y_IgKJru7~aeo9TV&H{0PgY59pS74m!do`m6$(GhwR=cHXK9X~F z?FF6X4j0Rd@V}m3u3evgo7@9t3h5_316=9?QM?28K;)>7dP*JX1ox5Unq4ci_YmVntk>SlAH3-*!i@Ydr@5 zb{g34QDXhEJ6|XoF^|p96lOmK{^cBPMXJnAX$u}*UF=Cui4Gr{QZse_W4+=t0HBcO z%dM;HmW$!|tgNr&yx~|8bwwrcJ8uelS?@jJI{3kqs|&IWP)vNM5qa~|(D0gv_c{l{ zf12BopLEhhy1;B2tT8lCM*`n$b~52#cHRllD%(CdeC-u-#ZEqQ++|ADD_2#wc9g(2 z+|_&Dvj<%Eh-BY`w^p_)f01u0zy08NX9aIRMD*S&J@wv0Xd$yJ5NnA&*ib$AJ?MJo z>b#$pfJ9@4xmDD~xdhpNch~s~fHtdGGbe)tAKh+u##FL3&>_drf|&lMLxGLNB>zck zqZ>lmh_yAV4!^Jupj(-n=?g&|w5hAx$3(t{hJFKMhPc`3;Q#_q|5=ES9Cs_QvY^h( z_n$019u#BD3G%WMmyGM^%D#5ZFv&r#D4s!TQ%B!54B?AJ`Id!giJ!m$Cjh@q-Sd0g z?;~de*%4`N0+xj$YMVk4E`?B#ATIne1hI(^+M~B-mJ6ri|Od&n&{uQ>Th01@V0mM$m>^2 z8N|Ob+W^OK*1`fJ{7HY&$N--YORmR52&lT!fzxY=4Fr|cFR6p6*k$5kG!F@jUg!;T zy1wXN>g_>H96qS9gcOEHNhrfwdx(VuKHq(J8!Ryv9lsZ0`F@}&Ilwq35rv(`F8nSZ zzO3^(b$$8BuoRz*Hy)OQUA<0MwW>omWPfri0!Sm3bamt>#phh}?^F^AZTo#rPz150~pEozJnzY5~^tiBqf zN>71#wN93Z28>ebmM*0wqS>huUPSl0V~q-o%MZP`ztT$EsK$O55-Zx&zwFu?7O%O7 zao6h<$IL@BZ4w_YPnja6S@r~n*MCmVTm$vU2r*GjuGeGhDwN$=b?kzV!)2s6?nF%j zW|_Pi^rllpzcx+pmGXsFMb&PxemnSkNvz2hZIrHE{0yFl{1*1(!{a~|{LTu*zV;z3CHp+hMtoM8z%(oL7P$e3f z;x0)?BovG52Zs*elRRQg3H*qLh*;+i7DH#=hE9#aDx>BrT=+9k-<(u7{|@!H@{Gf5 z?bQ3{Lt;U0T;RR!By*^mSrKMUu<5U>EK_hzAgHUJcKT{uFkU$+*=_42>iV)B5%o7! zE#CF+5zG>s)1Dom@VN>`RrIg?(OK^}yF zqSW(Ra_xgi9)J_iE0-<#2UL(zagz~gX^vtBc$XAR7SY7x$6PYP^8`PyCNkp1qCQ4J zEHbA|vTzJ!^rF~23Tz*c)@0{OE2QV_e`yY^!?0ux47Fp!9mO$I^pOXNCM6NfBINK1 z4NqQ6WG|giob3M&{X&+API^(-2i=j2O7TnwD65?GD%r}JO4-T_W=8qFQY+0JGab_p zeaS`o3s_+`7Oa}s0FTN~Kj^LlJRUFe<*uQOh-tZKo9Ua$hrPj+H`oVhjg`0scL)_> zHQEJ3eu`B8%7(eP_9sUp-PS8gbHemtXd)1{(4}oC$N7Ohk<3c9fds>JqF0?RJxwXE z^&5d!SNDVH>rphx+;FUF%%PMF$44O9@kN}sS^kUGva#xr;Ra-#e2NcaSQ@^#bro7A zW(9#FaAR<$Dxa^s0abZI&v^1JIP zOLoDeEk*b(%EL)6mQGB;(xr{?wpnc3Y;F(kUK8`}@$(lu&vd~Qg@A3}09x-EmcK)I zI)rBz6JC|*qTk8uTN3dqQYUAdRLD8hH-{4RZa*anrs6KKM;u}9 zvCS>6ig@%$BS**O&}Lz&GrP(XKfJP@CPaJs$r=lu1jLUmh?QCB>7>GB6T+LvRfXkl zn@WZ&tRky?G&d@0hdIYx$Kgiu)O63n8xK_SP?bT#AuTj2^yNT-+s6OU(o#$2KTI&a zMmE@#z~aYEK^ko);o6Zz_`5GC4tJm$AvjOw?%<+{|CYIia`NRCG!Y;&J@50nckUvj zz#*5wUY|5|@&clh5nxDKb_fyUkThc>mV(lBc{U5S-e_s3=A(6mA%T8?iu+t&Wp0rB z1zRo3t>c8i!pY`)1}i_K(XKr(Q38)5IV`wK0!+DYCu5nVN%N)cM3_I zK@Psxv*$OIwg8AzGCqH$9TB2DpOKj8S3g~>)RIYe zq)t|U+PZpP(p|)Uz3H!G$x)tJkv<$t2$V5@l5e5@I+s4)J66M%lt0Cbn|irgBvYBI zS^U$9XF`^cw}d!X5+)Er(GHVj(;B11d_W#$ergN$N$eGNQ>9RYE!xm4Of9pFgXI~vpJGwTd_WOr6TFTPyFe_Y=&6BtXq zo1SLUb>MOHtDS4R-AG|>I>$(81H%%HE}n;D(2aFFlz+DmP4f!j{zG?B!X{Jxk3YMk ztg!c6lDWnDd|5+Ka2<&sN7#+Y$F9*yecmt)rnUcYl4X~1af7`UfRn2fB119}StjE$ z;OL{*hnX)0zhFx^8kiSkd4W&yGMm-OAu?+Bq~lC`cQ?724Yh@5A#H~ z_b;bCMoU*K6j-T>$$@3WE0exHnQd5)&cRw2ww~dMs5lY&kA=J8PBu>Qs{+6H?tP^+ z6}qKgG{wU()8{vds}JY(uHc<97Kb#53yMmDQtEW7iPF6h3jON z8QSJKh@3du4GAayM9N@K@Ltu9{BGwc{}80StWRfO{%PA_Gb`1|3DNN`*Gu|$nG|p*pm3r-)l8IVMHY19qo{1iqDG9h z5kmBL=~bnZ=`TQfgk=1SyN6*f~uPmfQZRSo!|b!BHt zwd=*9!c?)2tfuFQV!IwkgraJi=aJ%W3FTi5zSHA&Bu17YGpuj_tyNS_Cn4jNw3P9T z(Cz*uQ!`WY?Q9W|XvfuPE$b)$v{J(|@~pwIf$Rptw2aVeL#qMon`R%4VR4QEL4_&# zuj`3vBG&1o*zB&$#3*I$_?K^TGxQ9ywvq<)w)}kvSO$)Av{^GDF{3GEi{=)UF^Tw2 zKQgjG57L{JRUX;4gB6=kH8vBwEXD&%0!qkE^G5Wx1X!Q8Pn*(AKdtib-nv6_ViA8h_lDgqSse2<5E z{r-GW`VIqTiS5n9-8>{&%&X2kK5=$uC4t<@tOJa{rFDOGJlUn1#%^qU+CT5kMHx!l z6<{7APO-t)&$s4vynZX^NWuE_g;H?_Rd0o^Y4Ar3rBu`h<_4|RSGoAAOJ7ldetn@2?Cb3$~g(wv`=vzs=^ z{skt#P^v9QNk31$6-S4(PZZaj)l&4Y(`(|o>lt8<&^#?cLzELg>-I^`d-Lyfy~ab& z8H$JR?WT1h3$b?XAT>(7P1GYpd6!CStn*qQ2-n+wa=KZA##qBrikyr-&-etdkQD{L zDeeR<9lokHyT^ywRYcx1PAyrmaLIAR{h zD^O6{X%)h@zMMRTS=m;kG2>rcC8qUOvr3JokFvepGVf?Kt_vEYaw;`8cQUudXc(Wa zdSr;{7bCVa-5C&3R0S_W*M0Ps>zRE$B%NkP3N#7dU~=ZRkJCz8fANvHkn+Jfe|8o5 z<-BE`2A4gKBVb6~N0&LoWlv00MIReDVU61vz-joxnq@;;sywgni0zSsxls@vo8QTe z+vLOWs2WRj>(xhIURtZGSQWe!nCs4JVW@W)a8$mYU0u}O;8U6B6nPwXH?f?<+>8g# z<&>U&>trOOy6d^XDA~1By3g%>fLjRIIm36%OYo=1!E;B}712-_Bvg1%OUyOn+wQN} z$G92u&i6%GgK?cXh-1-+RJ)aUW;1l48ln$M+ZVbPoxA zfo=B=Od-K~L%f?9BGhHm6vq)Z1&2t8eQmw6`9^_1NYDCPAnU5P_}`9fDeTHrlU&I> zqRF&HP;FQvvV@Nh7il&6>E7joy@`}pffcPDJ)=W-P-P=Yup?^VKY zOVnI($b;hk>gvBS;K(M{Qy`9iB-X?QS;B=m3$N8vpJOzY{k4qwC2pOpWuhI^Lc>F) zY~E_(jHDHr%DJ4>Q0=SxrAw(G$Fe)f;+)H;vr25^TdD%~NU_TEni}VAlN`MJ4s;pg zQ^Az~ZT{#Nn=56Lo8Mak>3Mg=Suv0eI&jL(?Fk$aZMpkUu{U2Omb^VvXzl=RCbP2q znpBIIaeFw&=Yxc`y^njwf$pS`ErCcbEk!mcRb#wmrW=o zOJBW-9C9nkgnOa+TyPrPF3Yn2sq%F+8uR?SNU23gzMdVxYPX9jts4H$IENw*=X3*F zt&s2Mer@JYpg`^WV~C@H*!XEoVK=)v3zL&$8QE$OSt6MboWaw!bdaRS@;I%#?3<4Fk>)H?YvOEw z-(3p&V1jTE8S6!oV(k#&96ctFjNO4_PX zvH^knNbu25_zp?*TJfhSdfS z@?YP6eA=Up2+r$TN4!>~(Sg#S39^RLUq^(&5+B-Wnh-}tulCnCg*}Z^^>+Lg!A?oM zG_3QbC053|dTtYn=!l!NdVKNgU9Pu(4o60`g8@5j7;32GkT%WGE?Yl!@f%YRQwNUG z-(eUh8-ZLkjJ_8nEb_NJ0mUGL+0p52BE8VSJoYANEDdj1N%t3U#DrlBg?uue@z(K^ z)FD#9w~!&5E(*tfb|!T;;9dT|FYGG8P<44dTFOugi^$N-m51N6wc;*F&40nDo9zeg zhdhKYskjGk$jLN5T?NJr3$&atL*=vBm*rnVKqGMBQDKS?*h*f3? zW1hmX!3)6azcg(uL}FB&}BOXcYmigvZ+^Q+EYn zTOlCRe#v>;vYzo{64!|zfqou)Uw$Ll8C8-1!@zYcAii}CdN7}^Bat;DoWA_qn>vbS zfl@ilH#3Fia`rVS92c9Fe2koapDg&rM`~BDPJgs(kA`Sx&@t}6nYw7o~*bu58r%iVQVCef+@enpP+6?AX&~PoXz!dKcq%vL6@OEAQ2Vm*> zm-D2+Z;+LyjfTjy>zDMX!;CT7jK$X-4*K{>$svC{%9HayD_(8iy+akGJ&{4Z5|2oN z-+y38P*qI+2Qrin8G<3IR~bTI)NY%kNVx8VE%Q1)ciZj(uM_<8mggZp3Mt^xSRIHc zHH9O4`C6)_&s`xX$(N}Q&BOd!g;m;Ib7T*Vmm2R8<$B_5e(Y3%nV~aF_~A>@j<#f$ zgypgae{${zrU&~S@i=$yrMcgL?dA`Jek7~AjK2o%A<%TJ%CWePz%G0r4l;o}0Wvcj z?%Y^r=6W#blPLI2^A3GBfo#jJT7&++(yvOjUu_Ky$B4QzM7B zmvZ}$7PGg%@j&%`SQk8X5|&ZO_z5PVh|OnMBd!7S9Nw}>{b{sX4%?j?Pw?$)2HDf) z>w<5a&9f8Kk9ajM4V0^+u752#bCRvenFnr(H~(;9V#Ol#zs!pb(%Sm#&Gk>`ckcbc z(k~-|8u>}?bp31Tf-nS|HUq=ey_gb9+RLzCZ0Sa?#ci9>_RqN;(s{Yb5bRe}gy7rm zSE^aKo_R0-9?D@%MKaVD?1MfZW~{7TUAW<5kXB;?ep5jL zb6lciXYIN!k3RtExfF^>^$C@J!MHkZIW9b((XtB1Mc>Xk;6#fUCh=VW{gZ`3E1W)_ zTG`o4>^k^E^IJmsf8$Y@p zA(Zy`P2817SXGO(G>cTv3b4copX;7ex}10$Z{pOZ>#qDY`y+=3qe}@Kkj+2TiT;hV zUU>$7PI-W2)m3-AUlP6oi!NwId34tT!~!?c=;6PJnf1!=AggYvOOi8 z9FWgfE=^sHnPd$YWzoT}T!+!cGFv^EZs>+{0g+iG>|F?IAsV^p*nu&)5t%P!9lmPa zQp%qXVI^hKkHK&-=s>K*3eak-nL-DRK!7!$2Y;ZZwG`MWwmiwCB>OcYfEqCkrVgpy zWoC(`(q)DI)*!5G;N#QYXax5D)PnGNVcRk*d^iC5`e(<9RL&upLx<9DTj35s z`({jk3ljxK!>?cnk2aiR;tqy-?w+seec%kmDiXrwucf5@2#0-ZJURgx+;@9hnSbcNyTNeuF}(`7GBSTM^aM6^9>NI{}?n4 zL!zT$gm=(T(Y9%q>gl)B|M^L)yM>=5f-yk?Jod0L`3HFc9TUr@RcVuj-+Z?#8WX2k zPGhGJd4p_M`(!{A7JrX2DdnCF-F-jzCA$m?FsL>^ykO&@REH5VqBb&YJoq^6sjd*4 zaV#^B*g$$AyzMCnS^$)~E4Zw;E&@@{zkU4!sckRT*&3EYt1{QS0!c+Xl+#d`oeZ5g8b|_!uCa-w?ZpWZ8VKv|^UEpl>ZQLHedy&4&x51W<7T!rD}aY*k(W{g8d1jY~CRc+o}R<2as9A z^~fW%*$yoqBongBKMpb+9MOM0kGG?JA=zRqyaO0r#QNlz;O2SS z&mBDMr;nh$o)NeOp1lnRolEFrn38%wK<>NS6{>CY(56uy8 ziA8l4Jiq__Ks!GsH)B?8e{|KTC03X5jv3e}Njp~HvDzazYL&vEd;tCGf3CMHe7-2Z zgOW_}R9|$>7z^6V-VaYKo^=8RTrsW<3&a;NAt0@|F~z0cj^prF5o2+izhZ{NGxsm4 zixTb5G@mXd#IfzL#Znt47xl-vYy)5zrlb+!k(7C=YUGsdIjJamf%}Lb2?Rw{gU=>p z7Hs5lfsM?5Zr4#S93q$P!biAYUw8j1={~dK=ly8)Zlsq*y`kLM!x>FjC^T!0iPIB+ zD`K8kVZoD2=GDls zs3XQH`+ul=%c!`TtzEQncXxLQ?k)-L?(QzZf(3$maCZn!Ah^3b!QI^g1V4*+@9(>3 zf8*Xicbq?`M$_GE)w68YteSH^v%26|B;>o-p0TJj@YH6ZQ5fRE-;#YgjYrspAopJK zr8(nD#jrT_rgkWK-&i2 z`mqrQlROMYnEPu>MW?S zPCG3r0X&+&r6oTGvuKe-G}LtSLPDVDxFu^gzg#wVjn#IF$ihkHo84w#lE(?% zPktSKoI^C|$(0~K?pBPgM9(Asvx?RKW%A|;R|OvpHo!`4C{GG5?f@n)+Hp1=x*I*X z7|JCm%gRe6$gLw0mdXTsHP1`GRBAjs{Y4MPvHp!}C~zbLv3ZzGI=+PZ1K}6rqVTV< z^sJN&6MA0eP$(_s2`UaR#*eOHSZIE09a%W6EIHsCx#UxhSQd34n?zPnHO`1gnLhcLrUidB7P9rW3@H{J*e1Ua%R+4 z?S!#UU&PE}WG$bz=q?rMek3vPyRb6+6h7dVY=uhfb*Ox3$)ulf?wF~BR#LWsnUzF= zWry&mA4`ECQ-3|A(f_7Rv||yJHBlv@V}DUTl31<^@tjW{bN7-Xf}#5n;f)O9tKD+| z@)PQ{(=4XBTJ4hhj8b3R5@jD2J7D}rSY-klDDq+mfN7yYLv~O(!4ke4>)oFS-FTGo zF#2R@$dwtAfweOv$}A2{h9}M2gEv}XL@dHparl{Jq!J5VK3;usHk6Gwt0j>CGi~+B zC|JBJI3ry@Q2c`0`60N3WupGC7s0ZXv12tBWDU>G^5FB6>tM&*`hvI0Xsnm!3e zc^}N%Ab8WI$mc62U!-RP622wrP=%8o?0geUNd{T38pK1K?Z4ok<&f3WQOoBdN~b$> zK$-TdB)Xa`^@f04sw)#`ybb~ru9A4V+x>afyC3(dziO;*+bj2)HdywnUk$~1&0f->Yjs`A`@I(RodR=6&Csm` zIh(rG3-QdjP;q?1UElwFQdQghg(MKZno%J6j{DBBzSgl0LV?Az*{g0tvz8!ADR+Xq zbI0y`8a?xZet~b5kImiN^oUP9_VAoj_oZ%%o`ud8nb(RzU(4w&`vCz|Lx zWH}?SGj@TN0MiL5TF>sUqiTUHo<(MF=HHk~-&Q+iK4YIwXHzkWA}5xGgV&{C7U%=d&j-qi@-L9jk7)=f+)oNWn+ue#HAz+5ki8Y zyyCkM93Eub&bsrPKQ2NnK4gCbvkrrdkG$3|sIH;->ID^Boy+VX{ON4Y z3*(+SdM~4UQxDe?3!N;}Ab!b9ND4u5giuhvtUeDz0ir-u>Lnun>VV?ac4x-DRrAs> z$1u8DPSczHnF0EYIG4j7!qEG24z8j7Op^5*%=KP}%9L~g1@v0!g1yg@)`1kN)=T7f zjt2F{Ul(@^*FwM|$q{5mkRvh>A8#Q9m$`L6FR&T$KF$U)b^I08KM(u7-XtMrj}MyD ztfhm*9`QT{4Vk~}oit~h`JU!RC=?xjE2cl;#qX>$`YwZN0go^u`q58w&OCu3QR=mM z@n=}tXb)&95)@}8=@^YYJQ2*^-=2xhaWDNX1Vzz5(@aZbr-p5(WhK|b=L*3~V2{%< z`A%xqG z^NG|5->*?9GXBu7QJ(wg3_0Zj^%40#eo>2P0>A1Bor&YOQw>HtiNZzbfu+(nF2gbr zj<)r5ZuU~u-J`=4M`Yx`R7lQ#1ZiVRWGj9u4-3LN<1SZXu8{sZAI|xeO?RKfgNa*$ zBQYy~()n~DbmI%QhD}e=#z3nFnaIlwxR)>L|HMD*TpE!Xa-v<1j*Pq6cr+9ViPt_h zskr>!74j$X@TxCMxA)`cs;5GGfu_2B+Rum10=^5-iJzX3(?!s?X(FhYLrKgkX!xp> zkh>sw@Oz24$jSQW^J>KlD;KnRjDtkwcONv)cxBLLTa_aF8~)T@{j@U~S8=+W8vJ1j z>(I5b8?Akez#Ld%vRbEMEVj}n$rpxJdAFwC51&p3t@hLE1JBp5SYr{K(lV(aRs6z` z826uZ%RBI-jJmMosN1+emZOz7?QM6v*3Y)yV=r!`E@H#M1SC#2VN< zU9`PfD%4HdVbR{BP*B_c9>Z+Ukms@8R^*qtkqYIxg3oON0&Rm(<8FlXaeJwX7{u0o=!VW_V^W8j z(JT+db2&cf9GN$^@<-A3a3lP;8Y{U3<^Gd61j3}Mz+xK&ke$xAH4lB$40T0V@*PjR zv?0P0)U|uvYX483ze>wn3o*6tMhuwN9IEoSt`gC#!5}@37dJ~=$2-!0xS7SmoPO}m zz*5g7E6JK+;8rSu<2m4Uh^XsiWe>PrK*Vtv`0_tyBZlq=s_&ijj+Krw?Hem7K`dvH)qLC}cS>Po@KTu-? zaFvoz9RhO4(_qUY?*tAWalgVQ`TU^&(urq-l)w4w`>0bI>E9>YJDhlQD5D^ad8M!;ca+1nU4#;<00rw?h$F_`ZS1*B}z zQ(z^+(O6_S`4{Sj$72*|HQ2E7x|TiGxFM?V7WBK(>7JhK(MB)6*eoG|J$6m65qLYb z)e8k+Fa#Vk`(`gGjcpL6cMs5O*ee^uNKviEenL?do%osmv7n>6@IkQ#a|ZJQ`(8DbH5^F&@w}6- zZh;u(RPS0OCh~`i_xH_6GsHjJpS?VtFl4nq_1k!oFDrftLlBP$?j`=a9@m$>2&>c8~u z3-dJNv0nK{(cpUx6Qspin<;zhx9pa2{$>jp31W%wc5CE|LkRZ4ri5qzn2}2nc)KX^ znDd-P(>D(Mgj4{z8%h`#iad+m~T9)Ozuo; z4nSAjY9x6mJZe6&yq}e&BNLc69IBsOtAPmqS;#cd(BVoid$n*~os=hzE)E zVJiFg%geCcnXn_ev@BG*3e_mdSc)J$5ojz?|I4VuT~CoAfdPU@ixp&QiZiXVG5DhY z%j>@3UePdgKs+AlCx+Aq31GRf$>+&2qarm%wMkCT&QlVcnDic{ZsS6c3G9_n^K z5*Y?65Abm$H*`HksP)C_WaK3AbBgE;BX;)&M)M_&{BGY-&f@$2ye7v%t+6M&RId4x z3nN`I<8_s8Y#T)haW^!k=f{V@1o=Na%2yJl9A;4C5wbXtMG5tW*S|{bDk5`6%M<<8Pv@Y)ruksKeYjKb z=)1yQZO_>%SQAi{;x1@DPSY`w9-dUX7wtX>EX(1KVB&SB65Eal*oU>D*@9`zz;eo4 z)=2QInkMi<^VY#Td?cVMEW)l76Mjz1!V@8k7j_RVYZ4;mT3p`T3XrYQTDUK4OzXYm zIP|fe{jPh{$rQGo;c}9Umg*B0=wbPd!KxXOlQrEg`K#aDlYxR?OJ;&+0Cw_xyE^&} zbOA%4vLw^(#R~i#&ob-&%7qHtlSma_(Sdkuei^KmO&xrwZ=nv9_!H>wd}F zO!{b=P8;`ooipBfZT?XDJ2b)bJeT+M*V%(V#~W`Oi9tzd6aId$h9~#LOiN^Tj%5S1 zRft8S;DODd46?5LTcq&7Pb63gXcdm-hRC>*pC4~QVC?=SIs+|@4|9?aX;0&{Zc6;N zUuGIF94}hksedfciKqpK-`l!qY=0|(7Yudy=wklxO0DLyO|cb-wMehXn3@JIA}c$c z*LB0a;(uVTgEBTTv5I7%wW$_r;LGnmvN1OK{B8f}q4~kTdz+lUa*$Q!`0DM)egE+4 zvqC%FuT_OQCF>j0^Uv_T*d;a$DpLqbuDKmNU0IUwcEyyvjbmdU0(y-qU&}O29gtlE z5OXtA(wZA_AB{^~bpGc4=>+ZJI22FtIYuDvjOS#S^U>N56@5xD_6ZzGOKW{I;dQ?E z`$&vy=I6-SEC!LjSc58wv2Tv=H)MTC&1O4PB8^zGe%X)W(!Db>gNa zpuW3spy3a$zX%s|Tfz^BFD1hR1A$S1#7IFBX=G<$A`p}!^bqJEQAo@o5lYEmOej!c zDk_K)65K3UBp{fe@A4~T(6nvT1rM$sYfy}jv+Sv(-=P1&j)&nB&TiQ1dWfquGMEBM z6L-ym?2Jg?S1+dXXXu)@_=}UmDMUNGmta!ao;M`-4J5V;+<+$8!;u*oG51-aaS?af zuKLG<>w7*Ff)6g+6CLrS>*R0{wX-eXdfG(-jQhPn%wbiHI7L5k7SCga3Pu)r5$&J^ zGH8ZGSV$m$mWpmpn8K_H0N2e@Z$(lQWd4$~h5h?l<( zqlnLmw8$?_vaf#-^8Kr6Scf(Y5+q(hZ#Kl9=SFil&xt%B&|mc;#3xQUPi4Fy-C{+k zdhEAN@41QOG&cjVMU7^B@Zv-ROR$$NHzGA(-=U-V5)Kd;_%*LhU7TNfTM|}S&5JAe z%d*|=TOvCM`~`IG*LRP4_T+B0xh)3xfBIIjv47U-L!3j$-_z~l>t2qNRRj{TN;sB2*8{Wlf-^>qH%80hKo`T3bc>jU8{sNwkT?$3`q z5S#=j4h0o>Lt%yuF@`@;BTFgU`x+f&O0L)o!~F* zoS{Q`whL9|GQFB#SbBr5AD^KDr6FGTy^m}Y$uBmsqbc6NAYPfQl5NR@{9C`s)e8^ruO7;VOtnLE&olbbtlc599-XmaoOiM{NFdO4p3Ra)Hz+{|69NR9jozB|)gAp@LHSan`U(P_ zR$IKifxUphRzsI)KU2QmA%eYu0d4%>FU3+Y&Y%FWX*g)cL$F+oI1mWkXbKmXf+8k> zEC4o&MfxwEv0?z^y%smd5s2umTr-X$#{|m#c7{ka(f*-1e5FQoR9vfYWAlQ0T8lj@WA zH)Q7xS>_KS@i#KlLz+S$2Ml|lH?YI~r*e+}?|!7!6j&3q1`3Gs6aq5@vgSXLXV4%u z_j2w?R5M=p~S^t_;DU}*{n&?+J9YgID47g`L# zmICdzC&oAAx6uj|d!*dE7a0mr1|u)%?9Wf7+HK*@Q|+;qg{|GaGf|K4z3Q+Zi|w-BT|oVhiK|86%mj(4fNm;YL{ z`~PcEZ-jrrKsZ3J00V)8y>CkWd*8nNKbs&O?tksrG#wKK#|OT9?&#UWfXp0Cnazyd zfCE9GvVTL~Px4+~aQ^uKLC$wLK!@bv_{P&{k|9N)yXEm39 z;EIjKDDQ9{_;>jKvqt;B;Q#a*V1HF1yYxTsDW(v;cR24q4PEm;&QA7z4xqUIj3@pF zj)a)z_zpLIm%%?*06xt=ysMv@mA%RPMLL1!Cjvg-%TM4tSh$kHCnR|McQlf;l!OXk z;NI&VVBe*y>>d5S1?MEC?FuX%-}n9pmc)Qe00NPMq$NbvJTp(%JiZWVW?jBbj;*Gx zTqIR_r!dMUugb2f5Yi|(;~SBp>R2H>fxL{C4kUf7I(>&!>KLDmaFPWexxP)IqoZeF z^2cPE@Si6~GU~Q_=pWs2O_KaHM@U%=w;%p}|I(v%Kf%kskv4Sy#LR5m>U|)UT9asA8~W#iY`p_i8$+|7)v<*rvf? z$(Y-<$cHg>ZKWtx7u_CkQBBy`z-BpqFppU{NUuX}bUu&bn83q-G=0tycIY~(iQoDx z=f)!zHXGK5QGfsBHMpJjd};753mmEB!Qr=uE~bi1PWyauG_lN&wD>!{!Y>fSp=bH0 zh65l#e5k5%lpsKSeVCMR0e~D?p`^jz#VjY9@c&*tEC9e+yT{ZLw47ks((pw7fgk;eUPNTK%2rq~5QKLCY}}VbkQaFDI_dv^b-fezV3A4h4NA+!CZF@U_v5YvNP| zQ<6$xO0={SLRSYF;!{IBt0`|x6nH+x3nJ$!74h{Bb7o+P5p+aZe3T?^=|(t1BcZ=4 z(y&=(R7C$`TUvk^Il5V?HA&vOgUzl8HHrcOYRE%;VGx5_$Pf^v3E8ETADW~HGa2d_ax zO8ge|GAG3wGrtbJ02z6D1}8_O%(Oh9<@f3b^C8dt+5QlQi~^d{eBz_1vc zW@J)Kr9Wxiu?@(CLNIadIo1=tOhnnr`vI+D-7cc48D>6zXNa+=WFLx+J|*_|0;Acu zwxrI|m+e5&7=b6%{N6P!K|C`J{b;U(kC^flXmCz{e~_Tx-kZ2v$}Rpv!%2Bp4AwwQ zb*cgvxb=K(M6UDVy*)#WSV%gpM`n}Wc1XSxT%Knjr6?6(CLlqN1|d@CJ%H~I@}Ff! z&m#lRat9TnD>HK8{P4t7Ly8>##?%gyR91E;NS~Lc7i#$+seD+qG$i#Tx*Z6%9q2cv zW&&On)};2{ox|WtjA~nl(!GebUXdSB+NEB%TSb(I482j-6SPgKI3oisqL@+2X-Bkn zP;zzb%+xkS&U;oA5*I2DKXb=#2`M}X87hM?0*X^esId^@1syrd`ejD2ii3<9n-e?b3n*Zz2qlcyBbhqhx?L~ zgYwI?*6`HYb{GbUpCHCaxXISH(6BHN?h?!ZZAs zv7Xw65cadm%!sC_A>99y1GpKz5m}#ZvP{|^M3^v|1Pr2)#69=rtHnwTqL3!U_tDIA z^y@V4+e2ezY1vofX==Vqx)MvCF86u!Hp`_`S9%gz%B77N$yi~hcnS8s2GD|R{?}yQ3q+B`4Jaj6~q-i)U@iqFJISSLG{MuFoU0$`*fQQ4%=FAsp z6*ct=-SHN`Ld}OM0*r=zD}}mhGnrgpmk(xE*;hgNMRxdUqHGKTx$j$dI*68h4Sxtl z$k5=>k8P>&kc^txYb`MoU}I5_ZAC>z>zbX+u=8~OI1h#)-$y4aV`eVRPGw!|J%8hx ztH<&1qe?fCxC z|AH^-e7-3)Ms6u&YetQ?>rh%_goq}@t^X5TGlFITZNa_3R<}dG_?cYmo`0?lwj8_r zX%{ucTC_x{kMn`T;PEhMmU3zqu~?d%ZFw~Jh)+Y28kE<;noK?(mtOPU;>=IHqY9eEwE)13St=YYUJR(Q-A-u1I&0Sy9hgiq z7TtF-fB<2JA=bxp*>`KDy)KUzDjI&ZSX5F&Qg6(jna`0Ks9hAGjc_s9uwJ4qQ&1e2 z1*BbC2F0G1`x4zb)9NTVo#&9w^=v5CKA!r6^3%dAe-?iZ)s$DTk|RbI|7%2=T%62+ z_0ltJZ5Qe72ceZ)xO`96nBc6oxW@JZS)$wAOo5w z=#x|6b8j^%_jD3F+w@A7|4OXGEj*@lmLAhGtJH^x1&s+xUx+ z?oe>{YlD5&#fbdFXmC8FNEjRo8(U9<{R)fxP38y;I9P+@h5}of*<`k0uvlIs;g(r$ z9Q%ca_nqJ;a{T@Y0Q`ru0z)9PZ;f{}N#``*WW^1KknLBvABO z{t4)g(*u6FS%0qkJtHj85&obOpF(yeCyQVGx{qtLa$y9q$J-rw?J~C^^(MuzJ3{Am zU`Q+PSDeu4-7&M3X|l~Vg_?077Ju}90RL&BAIem4Sh2(LB}sOuRPXEjS+9vJD!~y1 zyCnHGO@RrkcAaKEEqu$Ytp6GH%m_}wQYLOTV@UeFVZ>~X8!)DAgD&($K^i5Kuzuo_aCy(1;w~eTkd`E?xO6^}_3_G`gxqvszP|0*)+CS&7<3}z!h)|bCkK=G zi9qA~xCBvFwXL;_`V!D9N6YQl8&S5>13jfJ(Vjd~Y}gd8`}Bw@D4?}H@p*fk%QC$V zKp)-go)D(vg+OC-;oqVVHTgj4^P=>`lD`fM(b4d8-V}im2v$)Pt7T14=Yk{rMr4hP z?{fteF>(N$1w9lkrKvLh!dpa)y<$S6C(;iIME0~U6OTMv`t<}aGd0HMzcj*d!+WGNk7Q;iRtJN`dy(N)Ng|KG{wJQ5X2CeZ(}yuo>q+GFFNs+zh-gZ z18*kh0X4LZA$6r2F?L3fnScx%HkgQq`SwV)z?t7po-s#nh|P5E6TI}m*F$9Ph_VC? zHiWe_=;HAF($pWjO)cpTz47XDhHG46YoDYU(&TGOzNwLa9QgAsx2(WjJ=)V~b6YQU zG%QUfM!#3&01eR+KaD+k+TW9a)ql|1ChBgkueaBVi@Qj6`506|Z@dU}s=R{)&o$s^ObDBi3u4jUiIu zl_%5`0zwVXjG5Wl9~Bja_4SE8Jz~V(hh!R6df{bd0=ILj720(nAAFBwzdWsMSOC-0 z-}mYHzufNJP0!8sJ>H%)@5JyQ9vzXzOVt>4gZFH)Y&=YO{7vzHNP**P4bMB)aI@r# z$lqkD7)axW@aRUwoc~(9!kP)JN5od1jCSsr=QZ1VScQmACcNb8&}ZUdO8MP#)xWec zX&vH&>Fqu`o?VgIyk%|40ieHNmActo@yc@K=#+JjHg@ZB71?%}w|#>}42=na6e~lz zkA9fyy=nSlI#-PNhJKJfI>c9(EZ;`gzm*Z-GH*0p!Kj7{7Q|KQPXhOaSQ1-5v*dRE zN^s^VATGcYFrVRCp@){<4E%#FOr6I6ivMa?v1B1$;imZ0 zbOo~BH&xG-n;9O(SErf{F_%^3JIdk$n9f;3(OM%}X3 z8{a7uV8RMCL2s5Y1SoPou%9nIPFoPq)ZN~CX(wiHjCo*tX!FvrBeqQUDw(-QmF4kQ zoP$T+ilT6UTUi`nA`y)GBd}Ke-(C(|Ce}PQ#ufb^Re?vi@PB)vV_?{6-FRW%c)e?+ zqodn0C3(UPm-vduZV}~KqdRPm*f>lAR=7&Uij25*(Ii= z6qL~y6%}b!{^kys3LDKhzf}3MCg^BOf?XbTtt;dnNZxV{#QS|vaVaT-{?CK{QSGnY zD1IysPuu;G+HIa!P50+*HxsovNWDhm>72k2VsYB;_+^KngvtMAoZGOb=^LtQiU2h2 ze=`*m+Ks|pm;|x3%&hEbpjpUS$|M=^YXpzlcUpnRQrK@FOX%CE4fxF!ps((Lpv76T z-1>Z5#FqOQ(--+k4hSk_2K&rTtKJq3(;WzWZFYA32mLiKqAs$i zW9m|b#g`ZWZe}~8;YviX)8G69LJ5|f8|`-)#&}B1Xw}(O$?CV&5-g#`Wuz`6ma&lZ zf?!iPnL0(^IaxXVaH+V!0)D9ha>U>E18KU_uL+!gh`gh#viwCGTZgkfV3u4R3*RoW zACc`OrrqGHo8Kv#qkB9gmQ_EHr2#MC+%FsYd~r(_R;RMPV=7Gf0JvJ-&(ZwT`goX( zJ}7s81au6aFhcmhcNbI_XlEs^|CM%Y%@|TbsjQBfV2IQ9g#g?TbOgkyj=jmgkKR}6 zm86#ENP0)%0q>7wk?ypxZPRggPC(6A z&Nsf8G4j<1tdzgU`a17i*Mzp01NFWCriR9cQc9Zfei0-3 z3K`mol&uLW%L#h742(}2XyKQzWo3C**lLl1`P#TPi=p{=h>auz9P*4@+*ebV+V{5U z(sHwuX!z7gQL%UH8FWC%pZbF*2+2pP-;_4p-K`?9EJ5&|x*}U{ri2M-)Dwx&C+Ys3 z-HBE>$+}It3QD#)@umYNBz2(gK0-NCz}BMrA!g0*uXL~^$) z7J*8fu01b60E-qf&OO#pQaoQLAwn>!}uPL`V^<)B1o1m-LSz!4!nra^CT#;ZJaGG0a;hL-e8^ z#ejt;Op`244|H(d8loTM_>Ox{NVwFmg~%U0`IedYJLoTcvdKM`%KaP)&*Nz<-0d&I z$RA(qx~bRFLf9?&fS6=AL~+}3UH+vdCn4&a5)%KcUYkxX1t!B4iq%a-$>HIWIbn-hi(~BGY zogCA3gQ3tz0vI;H$BQU4kstjeD#~uR%cLj24*cBgPQB^0Z*IA2l})eN9JJ``rMCK@ zxHGam;{tSyr5mA((KLYG3yUlpBr&oS=MUda^8e(XR~jKEX#kz{pzUbj3GZxyt>QKg z1B>F8XNAvuZCkEpOT`esc@kdr9Y(oXMruL}Mhq(qfWmIjWhwCxLumM- zT(QF>;6ClRJ$=Y2venmsC>c*ZWTB3hVw`v+3LW~xSIRtIQa7n6X^foy`S@jYQz|Sy zgo~}6IRt`CK!Jar>Y;>*{~Y%U^23=xAJOPR_Rn$1W1SXU+OnOvJ}$3N@^Q_??@d1yyNQ0^OW-qgOmcZO)}& z{-k~5k(OvteM;&{39E9%zSb~}Oxcq5t~5-7FLINU78=@Mx+q*z{2V>2)=*Kw6k1H0 zuW~Ni*VONi(7Q#;)Gof->VkH}{3H?TDj7b2@ocwtQh%G{E1LM@P|Anff4M0LW$kCC zIFtS&VphXCn1eQ3j*q9oFvcGOuxRY<;_{>!u5~&(W5JvT3z>aKJRcE zRW}sGoAac|zbQZ!N_n-#YZTOvGNu-X!(kqn8L_*$$x~s5?>H|{ zGvS96E2pOn2faNT`#m*krm3e--ib%o|3SQ7Kfy7XRHvFYc$M|;lK{|Tbjbl~*FPCggnx~LRa$EwQF?9SU) zczh9UW9H}5AM}HY+BrlC{yQJJlB&;WiZ`)4zii=mH|!zjhU*yqK6*%xJ8)_>=h~d! z%4?4IOfk_0NUAm)8)JPIFA-)4Fs?P?WbDI`{@A13ra+*Ok>n&WF#@H|Qvpp}LZq(V zWWPj+7&*WsYhlXmCRA_38g3Th7Kw3%Lwg(I8KJie4aQS${i%Ds<3g~jwE zP1AP{q4ukHJ1akD72SH5D>t2EVw9zer*{T7N7zhym zvg%ka%(E9(--dc(t2mNsR#{=BNuIxuCnu*@u9C=R4UAO|Hyw z#`X#>6((BQZF(nb|ymUwcm+fA*#SmznjtJF5sY*AaAERnv8%2fw7s7aEO#%AT zSF@t#!G`Eya>@j{(~y`ldQ(%5o@rc)JX++8m{|*RT2%5Rwpu(s~%3o)9;#38QcSN-tP# zvZAiO5FX*LN)!GaKw@=FiW*~7if@pT<%QKRk?&0GZ_SVA0P`U9;f6lqVPP=abZ^xK z`eRRp0b9nY+x?MP+Rt~^T~BTHhAlNah4tMxDQ@@yk&)+mqs|WW&n>Gc91p_6BFnf8 z9E~mh-$50$EQC=;>6bS0M6EIP+YNY_2No(*T|i_=V+&OjO-$PM+Ujcp`k#cYkU z6ZgWm4#Zy01e5mVWayEq`U$bK4(1BR$Pg6W&U-KrWIuh0Ipj?hywJL(+*UtJ1}d&P z6KPKVsI$K){ja$In@g3Pt2;H{<P4p?lf(qy#__KO7CN54{nOjKA@rJ+w6x5{-y zbI(BC>)5Mav$2kXrtNA1G7A%yaDOsx!qMrq1-$dV-BdX$019_WX-1eEmNq`^$8tJouOV zn$ny&KF)AlHD__S-$f@)Uh;VDC{a8SEHAp&Cq?v3?>;%%a(^zz$}l!|q6{{kc3OJR z1{Kq{y$w)dxJP`?e!p!X2ZDBPU;NzgCar1}9N+NLL4_ZCljP&jzf;fJnU9;1caz3nRZP1wh=fpvdl~CAo1eyG4^20_Yh{ab<(Zl3 zh7zxpX~Z*9DQgj*w7-9H(Q2Kq==z?wlY`G&r*_lC4q<@U_*0H+vnnZoF4CCu;?wGB z1v_tIBugM{qNA+Po}oBk8vK;bGEQkhFMUw;E2^Hv;!)p_dTv2%r99TylO13kWd(So zo*HPbk{R=@`Uek}o(P#TNQ$nr-Xf#QY;hfyf*Aa{I!4SFeKqf_?8%rzPTeM(jxxZo zCus5RTmEP@zWj2f2bI!KKn=Y4FpSBNJT=o=ez{m@`We!#4J7QS_2|(o3)`_hfP_f# z)<_1BvVaEC&5r3H$^ubLb#diiH+&WX9Ai!^qHGp=5CLnN0fKd@9pcsBHQ2nlFUG2j z9?x>bl{r{1)#7T&k9T8lsFV(g+-3x{i|NYR023kvfNU9 zff*8-HKRgcQk3X~JWc`2%R2efv;c|0sNaHFuD-wDsRv|nfRu3)LfPSZYoHm21RH+W zhBvC^M19@wc{hR|5`^sTes!$H=*sAM0PqR!wjs9T$KhfdO|l@TK$>azHwzvX479k9 zr)@-5XZ!Qw(Yr;XU!D)>@fD5O8Qb2{y)cL(n!f7)hU{RsHcF+4z9n z1ieXqzkkxGsv(U#CGcNRXEyKctP9&e%*IH@nQiJJh8C#pn!B8Q9RYmQpVK=i*JF3@ zdAOz?{baa+jf;bB7?RzLjc4k3solggpnN%Z&Dr^eW3cJ7@kES;H6zb&okrcxNidY^ z%n2mqJng0obL+AxCZ7XDJbLL0?i4L1X-O;X$i_dsB?HNmh%Gsqq%c+>n*n4(EGsN) z`D%;I*%zA#dNZQSbfgA)tTbR+c1}IYB7oqF>#P2!FolfR{$mMt%4q=tdG+aEH&5vn z^s~PhTqBa5I0l?(-<>YT>^@_5YQ93cv!72)2+%>#e+Py484MprtO?$B(8D1Wg&rvH z*MOItB9t!J@g39cL`8T<4(VEji#0aAJXd!&V+d%L@QW4y;a{}m>{ zP2SR_a|R=1bL~$ItyZofiTHn` z1F0Pm4^P%rnrs3McY?bpm5({lYFFb;$xi+ZXSzA$a~q$XKO~@PhArA;dr!CA9If9r@w9faE32|Lxwt6LLEI1_-^2H$Ibu z@b4tvM{0h7lmh}_(Jju4UK7EWZ50hiY{`W$0iIT=YUB}Rc!vrf%KE!>1rQc$nf=OF zJ`LZJ5Xn+z^3KxgKd^3Zp|N}~Z9(cygBHp(>9JWw z=I6H|_+fiCP-CUnX#^dplxY2+4O9YND%>X%yDDBWg>+!u0}Zt3e(hFsl3}%oG^&TO zY>h5ieiSrCYHe13XzMCfN7~}_(J~lP^zh47*U9Ob8J7unx!K)?&R5Q?tSlVXpN-5( zGSM%A)4-ee_d)~w?Vv$UTlKKWG~?ooi_$MuSLNQc zkS&={%IZL}NnQA+O92@CZq0x|9w57;$H`0a?rLknA}j!+0QWvDxBtB7e6%-PIc)xW zxCB0Hd<5eS(xcv50%kI2#rRJ#V8q19Rd@&2yrC3Y4}aRT z0HHwlQ}DFW7*o^Ba{+xGsR%|UCOqa&fOR=JIU#+SU5nTQzJEk*`RHO}W5XC!xsH#I zD=hj*oLyWd0l^U;A3r`J0S*r2)YMdAX=z?_bNbSfY0FXi-hz8$QBhIW@81de`3%6` z%i7qyxjNm+X=$N}LA>?#6$kIvW7DfKuAPQ7TBzf33GLWg7i4pWg#V>__6BM#DrN72 z&9N)k9WnicP>s%JYiqga!&yZ^R)AJ-Zk1nXwjFJ&myO85ySQ05Hqo0chs&FL?oZi@oq(zOz!tuQf=nd9}2Ck zlN!d@VM<4wu2He?lXC25?L=mz!7B@cs6+M}(sX(8`-i%APHs*j1G881y^^)#17<&0 z1V|x8s~BD1CjO-;*2R1s|lewTZn> zw@twF(bCb;(9w~#wPkT{Ob1mT5?0-BFc|Bn>QI}Jd@e!3P#6rg>!Duw8;-Z^xG zyF1AM)0(6UqhZ1QMw3O5WKn(IGB>fnP4@|~llV{a1({`MlOjmwz;%DJ&L1-l`+}Oh z#}S>@pVY0DJK^1ea;&CUnXPuG5uH~~X{ zyE@dmSPAd}o$jGinz<@d@jTDu5<`6z16#(bzQZ>I121Bg;cHV5J<|C3_llRF7ng%z zqO!Rr%-Y+z20L-eV)Tgx8CHFLJy4jm4`SNd3Q9_i^6JfEZfgiEp6HpnC!nBW1 zPluaL<-~%)W;4J4&$Cm-` zmT|FUrO%t#hojv#NiDAGd?u;J97;_LSK+d=CKjDDkL-e@2P*z?arV#U*bCZWd6F9* zju_-NrsHA#J-)iXlG_M24v@@YeF9*Q@3%{V96XIb0H8iTavVb*{*-H~R&&SFw>7_m zcW+PD{@lgcsV8c<8craw`IdLNVF586Id|Ulvy6gGHP%W2SwjX`Nxup zms7^{4chlv3`9c>ba20oY@$gEK3AvdAH*U${|@IVacF2c#!T;6E_X$N*W6noGgRoF zWx-&NzUevC2T9x>#55fbE6sQ;N3E=^yw}%{oD~-q+erTsts1APt}X>cY;SLmM?g^N zd1QHbc$g&Q!k}6JJwc5_GcpE&i}CUC@xJIY`q^cqtO&$aZPuFV5Q0CO9lHxL6>#Nl zR>hnBn;mXCCphtK^q)FWkaBs%b%`r?lY`d6)W?zT!-k$yPlbGq4g&%QBkl+f(eKFR zzYe2skVt7w?X~#TAF2O(sjAN(;N`LVi}j2ES^_>m8=is+qb%O zi7H>8w92F>!)x&jGF$6UZjYmfx+rw}`(y4lpBWO#klV&rRmsVvPTTQ|AI8S~P-Yn7 z#g|bpEGwfazKXd!kHG*I2nVoafkzq2mYMN6o84$2_;b8D0!p&vC~bKLsM&O2lAX?A zamVLKUsjbqq_jHGVb$;uAATkxXtcs$*|Lj0}!e74UFAGlw_h zrDj#-Hf+aBIcbIge&~c8a-LTuy4r2z-%y`_xT*6X=frbKZQ?_2WI5@$2~Uz?(!g$m zrIw7CMb-;QGCf90{z3GYrT_$uSLC>%*G}42A*?sX_c#aMxYmX;p$ou2DM5R|Ga+6cF& zBKA2kA;LR3etK??ccG^_JfAq$bARIu&kjOQ-Ii|5LOLLJnT7?_d<7s)@!7@Cm7YoeDB+k z$Qm|{$2T?arRP+_t{Wv5F3!&9gI`+O!--kg)m?Mu#UAEByw@wj0f|G9iqwJoQA8&< z`^P%MuOR`sjoZ3(5CDov!rwi6Tf5*s`KIUeY2>@fiX+0=`|Tb;B|d3&=8L&ipWws2 zEy;I$e0LkZvip7Y#6IeMY_m!JE6>jf233T zsdIa>s;8%?e&I_;!H76#ze^**(e~gcVXWYb?%p8Mny$xiLj6jJFqhVJsn>)pRJ+L87O}s)3)x$Q`__dwj{c(lTH-_1pzOBF~>(;JP#9Q zH%Rb9(7v&EDZKzP$$)`jV`Br52G=Fo^Hf5$-1FXc)PBe-x)_!Fm#K;F`=9K2k{J0K za@e-soTzlQ-kd=LsmiTrt~~8-i+T8$dM9X9FjiN(49JVEX}nx!;9N1Jv|g>YOp@FQ zrhZBSzjvC=J@w5!RV5f(2@A}ATZWB(l{bXD(sLLPVnKD>Af1;sy;p`XX7qhOxpiq)@Ge!+MJjJvilRs zWU21ihDOTa`dLbtv>&wNLh_4pH5fo5VV%;2{edm-x7D|Y#Pq<0v!?R-*nL|qsq}6+ z8WPzTv|~~Y;$tBh9-fUsm@u*o48UYYb6jrpv<|Dh<5w)WN?)Xc$k<7QIu0YP%OU{- z4<8@yFe_~6xk~)$(1%5DZZGMd^1D~KkDKW82WY$Wacn~vx|=G!9rOeP^wvIbHwuObL{|ft zY%l`6(J+dok?Z-onzptjQOw?Ix3UY=bND$8x#9Q(lK*W1(^Us7OFZUr=6c8JvYL@K zOi{aM!&hY_SCEBzPYAC!Bb+_rfw6~2lF{v9;`Iungo%^W4_#LGuGMcjLrkwka0ez zWTp3o^KJu*o!@azS&*t_feS^l6aHzl&tZeK^gu07qq@3!|Ihs=6=nzmkb3o)$L&nl zjtmb=Gbd2!EuDLlyccSD=PXD?LrK>F{sapP3%E!+XwPF3zZZlrD54C1+~B@s7qz)Y z+G*5i0ZTz@=d+MC4+DEzUtaqqv~bw z?-8I?{h$_c-`~iIQJw1aFMEgZvWV-U8yy|h_cf0}9s=!F{^ghgqdfNUS>y~b0b z3pbtN?O%T#E5F~*gLGPs6421s_g+LK0&0Q`?`8|8wVmAxK>g-q?5|@UhqRaI)kYg# zFOibo=pyK$3h4XocXIf~mg?0Wf(7;`k-k6uu4?_-_s*(-K*jGakVoCGV+bQU&$|R# z+HTI~kp&VrHebEGa35fbo-YJ=Z)s*O-Cpfkfz!yC;o&at=E4DjmXRjrD6#cA z=xVnyW{Vqo`&vzXC`nE=kJr}-X16g?Ytyh430a&AbceW8XNZQd;LR$Ir+D0qe zh0CQx;?XrGv<%z$?)HXC?CWtHHIi3_-+rn?;$}p`pz;JW!|&FU-(f~xkZSEi>m@N- zk{7_i%G=ws@$vD^_+IU>8nsXvjN1UIV@sUh1Htw6xZe#a%ZIX#P1nOdquV%ABkx!@ z)YwxzEQoOW^?r!N3Wzo>tgS=wDf#z{)O^A>M+#id_f6b)raD3Bh(L?Hw02I>(UF7p z{10}Lfa4s!&^i>*?G+So*%KrDZrA13BUe2?>Ev3(x_*(8dg{m%H`r zG^`|j6V$iK4LczrAs4r2t!geVJV2HD`;~oOeu?rzHp0xw7!(9)HDVkl>dr#Cf;y6?^?sHzfxyW5sG|IE&Q1+)!K zZAXfoQSE&lZ>*TK1IY?u3}SON!8>(-l*$qZwg9EF1!HGqqV5!Ol5|PnRRi* z>CW`#<|fD@0g)LHI=wgE(*AX*t+JcsM&ASg1AE}?v-fz?7BD>mH34t1)fI3s*Wn17 zcGSu6=xFD2HBl)qFOk{VS*;Hrj((T>;DZbt$VNQ#NesfJ>xQ3~_uu$9g0%Hq=Js;D zHBK=Pz;oa?--1InG(V37lKR-_&L;9@*Bta28griKhjsRVlMQ(Kt63p5*=1z}-`d)w zd~c63p^ABRoB63u!s&4j(pH?jIFNfqZxhSq2b05kBClC>2GYf|Am{o7yA80EI^`TM z*}J1tTXaR-4j7tsp1*g}!kOx8q>6y$4zg^J?E%)OLv3nG3Iz-Xo2Y{<>*m}6Flz+R zvtioB-TnRM0MMcm_4omf8~pPqGZ2=5Lu-*%`0noR-S}L(96~$8ljd$tuko{bg8SsB zlFm4bkCkkFKb<1KQ1+Wb`EePtMCj6?=1CX>Kg?AvH=tPDR2xuEIw+?~3Y$tuzDbPaqgZ;qG0Po2HrVP|x`a6{Ws`qioHlZuRu2Z#Lnc_vX3kx;Ae9^(*b7wC zO?%?#n@*=}>t-EX4G@-9i)U9o0Bf9WaLb{<2o4?ymImHJ^bd%!&w;THA4gFe;{Sp% zPfkvX-kvZ2_uqej%zJ(iUU6~7TJtK($zk{XWCQLzfcADDOzm$;2kNS&O;3me_Sg*!gT&`` zao2-8;1>QgxS_zo0&!WIDkD8TJ(YxS8hFf$3ZB4TzG7qynXIzXG%)b#`H92|xK}>@ z7|-E1@O{eQf!L0fm@IXKfq)9Q8ka2;>SAiVrOPZvtcwRJ+HPMv>pnC@+d7W~jT7jT zj$ZFs7#?<47JYa_*$7YpaBD%=4Z<&To{XTYf=)!_YRl(dxw`N|!Iw$7#?o!I?z23EWysgl2cM*cfLG4tO2qDPElM{b)w$BzBe2kw7!G! zZ#0AN47Ic#a9*eG`~V#aj~+{0*qiRo)PLBFWS{@%*e)>V@fx^x>l9f|vfR>Adk^mH zY^IHd=;@vOrY0(FZ6TjaUP=K6Iv{|hrKQI-65s=rQLWMqrDN!QAj5ArAqh;kfq_A8 ze*Q3sbN~#_1bEZvQW;R*zo--E;|+BHIzrm!$4Yfr08JdQ_R)?JcqJqt;}33XVl21{ z*RTH6L#uZ8z(D7wYsQ%eXuu_r#~@p70VScB;#IJ500hIQcOGEgL%S1Hd24jO4KNtS za9t1tyrv>pb`sRBccxcSQ32?g!6CnjvokkV!1v6XZ7Wv2nn+?6oqsbk{Ve58{<97R zRaH;`V{&tI;{-`3ioGj`Z8;4jCnpmT6Ca>`_>dee{&;*^T0h!-0Eh@8NX3235lD8_ zHN*XzGiM6ST~J2P1(1Z40bPQ8;UcGC_d8z<@!W62P8RvX4&oFdQc_J_-R`s2J0E}w z33?vVIX0h=6n`jR`;JW`XqCGxO(e6J;kBEUpC8O;JBAL8fGq!{3Axmv${N8TZ%2x` zZ`E2(QIfPf3Q{MbcVX`m(`pm0K!)*4oR_)aFomM-?;CaK}JGa5yUv-nuP zK%R?V0fR6H9E59MiH8p#ir!rx5fKqp_}%%oT#l9lOAinkC2)_GwKb59cmnUXv9lwi zJsQEk4Wc(71$9l$_6Uxa?h!SKD8%xjKjw3V2+ z|1DqU;GzFJq#cU-ksQv00MUyntiLI;TH{06>f+*VDR)O#QKAQjUvGDI*dF-5yuRPI zC?<(zByfKFTcwn$(3WbJBaJZ8rhgWbiH&U)@cefRTh>>4pmO%OzsT^=nJ5}-`fq&v z2%32vIPKV)zOrKOElvwe5W1Pi=w|DK=;cOEVEbaRN75` zS6HtR@q^i{wjVg;w)84eRB}D_(>|mAx>|}%X3fs~XK{vXQP^y3m@Gz`ermibwEfJ; z$QJglAS#DZ566b0&nofRBU^o=3}k*YBGg*lok~cGlj<*Rq?6BuIr>6(exkgU)y_?kQK zM=TF15@>8(iS5z+*Zzm!>Rr~kfyGk?Q?uK?jee#cJwsb=Q(lUL3-1}v^>o&cm4RpT zezYO&--oQt50*O5y~SBc9*-2LwU&4i0+j@oipG0i3KZB7AbjaMSVq`5IE{Y7QL5npt(uY_mRgGj4%&iRn27sz;EwEV4P@ zK{@49dQE3AuM$U=4E_2@Z(-X2f;iPgUZ$#`4(mV5OKcDJ?XUBf=S*P3Sm51-LDlEd;)Sx%% zWSu(X;>=I?If`36k?Tye&z#$YJK5AAnCzidx%GRZhXo@FZf<-)gm7Km=C6DWWk%|m zzS`Q!!fwpK4>5&HgXrps*fAj*lxiq@B7o`KIBVpQ-jdA(0`YZNwp3U zPuz3l+$`0S->JJ}=|qGXmHUw9sCWK?Sz86qPkUWXx>si`ZM*)BZ8M-{V|dLFa1T24 zXJ#1F!ALg@VS{k^{a&o}NB*GURKw*zUTSFDH}^F!cG#2M-J)NdTtcz6D2n6-h)I?A z;gO9sZeiOmQ(l5-Stcejt%p?yQ+F+)?{ab*p4BZYB}%9<%w4aq`M}v1T_|1dNp{4j z8F?2M7lT$g(RF{cxn2I%$9MeIU>sCEmNyGB^NWhYfNA;kiNd|{2KHY$j2Rafm-|>k znw8--lv|_XlXCpJl?h+!gyuA;`@9n1A^sNC7;Vg#nxq*gr)2d6SBFPo@WFK!r%V`u zD7%PAbOxOWUziLr{kEX9|JtA-Nk1!tSWoBLeZE}A`tH;pz5neR&P{rT5?3iHxQIX| zS-o911QR-;@(>a0trIv5MuhRfKLPwAolnpBi<4CvW1N-T$e(LMJ-n`@YG}&LPW(Rx z^5GY+PT3Q)bb+xrukr_5JP1^>ua3DaP|xQ;-Wk00!Dw$rZww z@aCB7+;;I?UGc0imrqt@tktP=O!k*oT@*vEGy-2!j0SyN$)~^fBQ{N5*7+5P5kAi; zXInMz)}XBZCZC1Mgs8t{PkaDKp-FtaDLhnI_o{665o7bLg@RVq)*I3eoM~Qt-5xrH zycB~p1{#8m^BLJpWcXGOhqKIB9>Ek$ZX;je!4}iL8gp6~u6t2GBkY`DxF?ANQQ?kR z>ga28a@v_6AziW?2EF(%1497;9zBmnkggW||61`OL56VD3_+{6&-aD>VcD0my`c{= z{5X|sIXE>!XrF(*k*Y4dW+YxR&0n=7jF!zyogsFbEa~r0B}8#|2nv|qGu}CUyIt%^ zUeq3gd;6WRRBTQ+5p3n>R%Mm<d$@G!dr@wdaO!D{F)Gh=yrW4 zg8il%Vk%`r^Bnrj%pe$m$w$YUevnA_l45Yp4I=(Q41?*>40s(xv5zho zU9kM4(>zyzEI)sdclF^UnO2b1?J@iPMVwpX(AHKPnms#-j?Njf1eX~b5cUm9MI<#psut?~bTy2FzhV@>hY ze*cIqy?MJ#s51*X;Bmh#f(9u;xa47m3jI$Q3{WCm9$xI?zpcCZx4`|vcrVP7f4evg zqHW3wq1V0W_5Y|`Co5asm3nvH=YBa@Wz@e29iW2QVlM2=%GvDf=DsJl4zeA5&A$K`I-{nn*Vyy?EltuKUr(*c`%f+aXfMK;`p+tfgIX6xW# zsI1%RSlZHnJvL6@D{p&sdJe}a8BIBXFC3-WNQpn$hdoJAScY@e&_9x)Mz@RQc!96` znftTzi_#s8zIF11zv)Z;Gnay;A;v!DTah~E!10ppuf**R+qK)-)6+3tPI|@w4rF=g zNl^^>GmMV9_(k_0i|!RW9`}KsZY`zhCen40IT{1+65~AkALg*=pl;&xM8F-Xp%H5P zK%zA~gt9=vHCalaz6Y?fo#%AY; zi!i#QA-;}i)J+;4B)?NJW3hzB_GW_@qF((QoN3@GdNkkSNfU;vbGyH*s`}+bBYwBd zq84&)ev~|(3}T#YEBk?62!nA|lDvxuC@W8DGQh8vHSHvHkOGc%>^cRaYw{R`(j9s4 zS)jB1AhcN$B_j=#J%lSpJY;r)@CsIq&o}FrY(DJ?mk` zEJGQR3aVIlm-8*b?Ut)$tE0R%?pZQ9E4hqjoQ0wb@_#lf4oX+G15)piN9B5?G5t~7 zYa#xI>SKf^#zqd9t}#HLrcQRu&vJrBq%M_Fi=;>_hZ5#?Q|?7toZ(57g^cz`J2l@( zpm|_S+JA*GX@@n&W>RgLmyEA3($-hfPU?;{qu|_wB~$N-g0_!cj1s-`^Qw9T1vP$n z4OXGEkqs@Js0e02ptC6s|l$@Ea5*mcoGQEE2nqulsFkSquvmZ z(1f}#GDt05n-hR0mUq9;E++gcgSurJ>cU)#!lglA7L?f_Af7SGUwhDYOA(juKBFq~ z(S{~E&2BANXoNGB-Mc7!OZcQf1X$Z#MuiUumY2XDb7e>K)w8A`@z|HAHC@7CMHhAw&i)@|>Z?zV4{lp1KxQvw2A+!x8ErM>H(ZvTQPKW83UoO)JO zn!gx9U4Z;Dv>y1IN$8COEFzb3dG#~uL}Pr47{x6Ma?w${TqLQna|-3EGIH0~=v(Wq zkmBfn-eY^hik?y=dpkn1G7$E=!>*8-(>!u-*NUJ0@l1|8qS7B77}!Ii(&f~g+tX~| zVntl4nBU08nGYF<*PlF#b8uY80V>js4h~kT3UZtQt&UO2tm~!i9ecW z-a$1Pw~L72-MuIFSVTBM)}Qwn=!d>{+DQYj=eTh2Z@8)O*XIBDYdV*B%YaIC)YVvj zx)+g8+|dxpRWMDuaMR9setDhD$~tg2gV;R0yjV?5EjP6=QxeQJ;Voq9y{P>`ffEEz zQAzY^%6gn6C7UbW{+Zhum8Rj4E<$ZAh%r%0LLqV`_tDuOt4x$V#xOy8X^C} zklzqOd4@q(zYY4herzZ&pdwf{w@(gNw`|Dpu`si6ia~prwa;VlAQVAfH~4@>2~?5w z*qE3K!?%w;(3XbG%+)|{{G=|mmJlb194Gj^anoLzbUEXML9=EVS8UJ;c;(Nb9&K*uR}k zRYNPL;2_J5t;!3{jE(Fxf8l2;EE4_S(C{c?3&r9@r+cyR2cea)0^f0>1_5AG{Rzew zw&Jx!+j)9{(YNm+exznZ=072L<>#>=$ljQsRT}{<5CO({bNh(fOL5L>%W?1E@N%s= z*Gu2^)Dx7Jt-h#;VFiTGnljvAzQ$eZy*@{=Sfh5$hDx5zSVd8UvOG6xHN}D8Z)|Ou zg7H4C?(T=6F!W|@HIi9d8~#c8otyRMHa*MeK>Unyc;)brTl#8Mfx0?xSk11l@RfIN zy}?93U~ohMuVR_Fq7LW>_%4b?wwu2S<^e_k%_G*VVAQSi{ra?ivq!Ym_p!_*7|`Sv zUnE4Hgol-HUQlIxX%5ycWXcGH0myK0xc3=Iv$JLRPSdT|x?tRu!*ToA?U_Zbjebrc zjl`oP6MosryS^Nnj4BIVLL(DiOoaZ_8Uez38&UH}ksrzyPSykFKZdsbFPonU9S`J1Y}CN?e!kAoa$c=j)*M=;D->-ZZt z6((pEfTlG&jE|@+Hq>OK#<2xLgvS1bt7~`5E3QPbc^zQ+ujDo;xQT#Z%W|>bR1kI2 zyPp}rcXJh7YX0@vo56wHSAMTJnwx{Hd3*hnHN`dk6tg47YqxXin?opBl;jm%_Xnff zy({j_bhHvOs;rs`;UJ9)E$#NU%U)V7nX&+0$FbXu{W-o^53AkI;*9y0l`9VR1#~dq$BEoC7>6gRcss62OE2Oml(k)rZ#s~|U_UnGUywb@ zDdfhMjKoLVqI*Z{NQ6V}$mpodmoEZdXS2O#Ox3EjPl-~!CCFp*R)gT|_d`G# zzP-KNA=2%QnU34R@5X`OXhb12B`%@&?(z~J&i>Y$fF>kLZme50{y-5(@c$J<{Xasg z|8r?h{)=$wF|J(p@i}G^Ua)TBvoB3;?@N>j$2rlz2|4f!=7H@m+6rMLfnE!e+p8Ef zv$C?)Wse8Gq9_pliX&ql(lazRw;th!yZ+-2j{b&C)xMhT&n%ltJ(ChT-TuTI{T!)I z>-QI<4Qz!lZh!5tqcB-F=Es$$23`N_9xB>IqV9SneT0cU>Bg&B)1-(7JTBR8I`mY+ zR|Fs{`kyicTmJuiS&d2ZieR&0Fs#PG@Ya|EdrJ+`K=rc26J#)HTN_!g{FXwgj6lo@ z>9*rN6c`-|mCf; \ No newline at end of file From ab79cca16b10916e49dec098dbc0f945a30f72c1 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 4 Apr 2026 10:34:56 +0200 Subject: [PATCH 035/190] Collapse video demos --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 84519fa1..fbd11370 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,9 @@ To achieve this, Serena offers multiple levels of (composable) configuration: ## Serena in Action +
+Demonstrations + #### Demonstration 1: Efficient Operation in Claude Code A demonstration of Serena efficiently retrieving and editing code within Claude Code, thereby saving tokens and time. Efficient operations are not only useful for saving costs, but also for generally improving the generated code's quality. This effect may be less pronounced in very small projects, but often becomes of crucial importance in larger ones. @@ -149,6 +152,7 @@ A demonstration of Serena implementing a small feature for itself (a better log Note how Serena's tools enable Claude to find and edit the right symbols. https://github.com/user-attachments/assets/6eaa9aa1-610d-4723-a2d6-bf1e487ba753 +
## Quick Start From df16c9f9f5d5a3656a7166d63b78e95da4c95432 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 4 Apr 2026 10:52:47 +0200 Subject: [PATCH 036/190] Restore old section on language support separate from features, collapse features section --- README.md | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fbd11370..49ef30ea 100644 --- a/README.md +++ b/README.md @@ -36,25 +36,29 @@ To connect the Serena MCP server to your client, you typically See the [Quick Start](#quick-start) section below for information on how to get started. -## Features +## Programming Language Support & Semantic Analysis Capabilities -Serena supports two code intelligence backends: +Serena provides a set of versatile code querying and editing functionalities +based on symbolic understanding of the code. +Equipped with these capabilities, Serena discovers and edits code just like a seasoned developer +making use of an IDE's capabilities would. +Serena can efficiently find the right context and do the right thing even in very large and +complex projects! -* **Language servers** implementing the language server protocol (LSP) — the free/open-source alternative. -* **The Serena JetBrains Plugin**, which leverages the capabilities of your JetBrains IDE. +There are two alternative technologies powering these capabilities: -Language Servers, while very powerful, have inherent limitations. -The JetBrains variant is the more advanced solution, as detailed below. It is also more memory-efficient, especially in -multi-agent scenarios. +* **Language servers** implementing the language server Protocol (LSP) — the free/open-source alternative. +* **The Serena JetBrains Plugin**, which leverages the powerful code analysis and editing + capabilities of your JetBrains IDE. -### Language Support +You can choose either of these backends depending on your preferences and requirements. -#### Language Servers +### Language Servers When using Serena's language server backend, we provide **support for over 40 programming languages**, including AL, Ansible, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, F# (currently with some bugs), GLSL, Go, Groovy (partial support), Haskell, HLSL, Java, Javascript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. -#### The Serena JetBrains Plugin +### The Serena JetBrains Plugin The [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) leverages the powerful code analysis capabilities of your JetBrains IDE. @@ -65,6 +69,16 @@ including IntelliJ IDEA, PyCharm, Android Studio, WebStorm, PhpStorm, RubyMine, See our [documentation page](https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html) for further details and instructions on how to apply the plugin. +## Features + +Serena provides a wide range of tools for efficient code retrieval, editing and refactoring, as well as +a memory system for long-lived agent workflows. + +Given its large scope, Serena adapts to your needs by offering a multi-layered configuration system. + +
+Details + ### Retrieval Serena's retrieval tools allow agents to explore codebases at the symbol level, understanding structure and relationships @@ -135,6 +149,8 @@ To achieve this, Serena offers multiple levels of (composable) configuration: * execution context-specific configuration (e.g. for particular clients) * dynamically composable configuration fragments (modes) +
+ ## Serena in Action
From 749bb815ae153977ab950bbfb828c7149f7ec71a Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 4 Apr 2026 11:17:10 +0200 Subject: [PATCH 037/190] Adapt docs generation to new integrated README content --- docs/01-about/.gitignore | 2 +- docs/01-about/010_llm-integration.md | 17 ----------------- docs/01-about/020_programming-languages.md | 4 ++-- docs/autogen_docs.py | 8 ++++++-- 4 files changed, 9 insertions(+), 22 deletions(-) delete mode 100644 docs/01-about/010_llm-integration.md diff --git a/docs/01-about/.gitignore b/docs/01-about/.gitignore index 463af074..11bfb993 100644 --- a/docs/01-about/.gitignore +++ b/docs/01-about/.gitignore @@ -1,4 +1,4 @@ # auto-generated files /000_intro.md -/005_features.md +/025_features.md /035_tools.md diff --git a/docs/01-about/010_llm-integration.md b/docs/01-about/010_llm-integration.md deleted file mode 100644 index 0f9c75aa..00000000 --- a/docs/01-about/010_llm-integration.md +++ /dev/null @@ -1,17 +0,0 @@ -# LLM Integration - -Serena provides the necessary [tools](035_tools) for coding workflows, but an LLM is required to do the actual work, -orchestrating tool use. - -In general, Serena can be integrated with an LLM in several ways: - -* by using the **model context protocol (MCP)**. - Serena provides an MCP server which integrates with - * Claude Code and Claude Desktop, - * Terminal-based clients like Codex, Gemini-CLI, Qwen3-Coder, rovodev, OpenHands CLI and others, - * IDEs like VSCode, Cursor or IntelliJ, - * Extensions like Cline or Roo Code - * Local clients like [OpenWebUI](https://docs.openwebui.com/openapi-servers/mcp), [Jan](https://jan.ai/docs/mcp-examples/browser/browserbase#enable-mcp), [Agno](https://docs.agno.com/introduction/playground) and others -* by using [mcpo to connect it to ChatGPT](../03-special-guides/serena_on_chatgpt.md) or other clients that don't support MCP but do support tool calling via OpenAPI. -* by incorporating Serena's tools into an agent framework of your choice, as illustrated [here](../03-special-guides/custom_agent). - Serena's tool implementation is decoupled from the framework-specific code and can thus easily be adapted to any agent framework. diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index 11585ea6..ceb1f86d 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -13,7 +13,7 @@ There are two alternative technologies powering these capabilities: * **The Serena JetBrains Plugin**, which leverages the powerful code analysis and editing capabilities of your JetBrains IDE. -See the [Features](005_features) section for a detailed comparison of the capabilities provided by the JetBrains Plugin vs. language servers. +See the [Features](025_features) section for a detailed comparison of the capabilities provided by the JetBrains Plugin vs. language servers. ## Language Servers @@ -121,7 +121,7 @@ work in a JetBrains IDE, this means Serena seamlessly integrates with the IDE in requiring no additional setup or configuration beyond the plugin itself. * See the [JetBrains Plugin documentation](../02-usage/025_jetbrains_plugin) for a high-level overview of its benefits and usage details. -* See the [Features](005_features) section for a detailed comparison of the capabilities provided by the JetBrains Plugin vs. language servers. +* See the [Features](025_features) section for a detailed comparison of the capabilities provided by the JetBrains Plugin vs. language servers. ```{raw} html

diff --git a/docs/autogen_docs.py b/docs/autogen_docs.py index 6dfd37f0..5e1dae72 100644 --- a/docs/autogen_docs.py +++ b/docs/autogen_docs.py @@ -181,7 +181,7 @@ def autogen_about_intro_features(): with open(readme_path, "r", encoding="utf-8") as f: readme_contents = f.read() - match = re.search(r"(.*?)(.*?)^(## Features.*?)^## ", readme_contents, re.DOTALL | re.MULTILINE) + match = re.search(r"(.*?)(.*?)^## Programming.*?^(## Features.*?)^## ", readme_contents, re.DOTALL | re.MULTILINE) assert match, f"Failed to extract about texts from README.md. {__file__} probably needs to be updated." tagline = match.group(1).strip() @@ -194,6 +194,8 @@ def autogen_about_intro_features(): f.write(autogen_info) f.write("# About Serena\n\n") f.write(f"**{tagline}**\n\n") + about_text = about_text.replace("resources/serena-block-diagram.png", "https://raw.githubusercontent.com/oraios/serena/main/resources/serena-block-diagram.svg") + about_text = re.subn(r"^See the \[Quick Start.*?$", r"", about_text, flags=re.MULTILINE)[0] f.write(f"{about_text}\n\n") jetbrains_marketplace_link = ('```{raw} html\n' @@ -201,9 +203,11 @@ def autogen_about_intro_features(): '' '

\n```') - with open(Path(__file__).parent / "01-about" / "005_features.md", "w", encoding="utf-8") as f: + with open(Path(__file__).parent / "01-about" / "025_features.md", "w", encoding="utf-8") as f: f.write(autogen_info) features_text = re.subn(r"^#", r"", features_text, flags=re.MULTILINE)[0] + features_text = re.subn(r"", "", features_text, flags=re.MULTILINE)[0] + features_text = re.subn(r".*?", "", features_text, flags=re.MULTILINE)[0] features_text = re.sub(r' Date: Sat, 4 Apr 2026 13:11:07 +0200 Subject: [PATCH 038/190] README refinements --- README.md | 21 ++++++++++++--------- docs/autogen_docs.py | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 49ef30ea..218e2e6c 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,12 @@ Serena can extend the functionality of your existing AI client via the **model c Most modern AI chat clients directly support MCP, including * terminal-based clients like Claude Code, Codex, OpenCode, or Gemini-CLI, * IDEs and IDE assistant plugins for VSCode, Cursor and JetBrains IDEs, -* desktop and local web clients like Claude Desktop or OpenWebUI +* desktop and web clients like Claude Desktop or OpenWebUI. -To connect the Serena MCP server to your client, you typically - * provide the client with a launch command that starts the MCP server, or +To connect the Serena MCP server to your client, you either + * provide the client with a launch command that allows it to start the MCP server, or * start the Serena MCP server yourself in HTTP mode and provide the client with the URL. See the [Quick Start](#quick-start) section below for information on how to get started. @@ -40,7 +40,7 @@ See the [Quick Start](#quick-start) section below for information on how to get Serena provides a set of versatile code querying and editing functionalities based on symbolic understanding of the code. -Equipped with these capabilities, Serena discovers and edits code just like a seasoned developer +Equipped with these capabilities, your agent discovers and edits code just like a seasoned developer making use of an IDE's capabilities would. Serena can efficiently find the right context and do the right thing even in very large and complex projects! @@ -48,19 +48,22 @@ complex projects! There are two alternative technologies powering these capabilities: * **Language servers** implementing the language server Protocol (LSP) — the free/open-source alternative. -* **The Serena JetBrains Plugin**, which leverages the powerful code analysis and editing - capabilities of your JetBrains IDE. +* The **Serena JetBrains Plugin**, which leverages the powerful code analysis and editing + capabilities of your JetBrains IDE (paid plugin; free trial available). You can choose either of these backends depending on your preferences and requirements. ### Language Servers +Serena incorporates a powerful abstraction layer for the integration of language servers that implement the language server protocol (LSP). +The underlying language servers are typically open-source projects or at least freely available for use. + When using Serena's language server backend, we provide **support for over 40 programming languages**, including -AL, Ansible, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, F# (currently with some bugs), GLSL, Go, Groovy (partial support), Haskell, HLSL, Java, Javascript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. +AL, Ansible, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, HLSL, Java, JavaScript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. ### The Serena JetBrains Plugin -The [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) +The paid Serena JetBrains Plugin (free trial available) leverages the powerful code analysis capabilities of your JetBrains IDE. The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs, including IntelliJ IDEA, PyCharm, Android Studio, WebStorm, PhpStorm, RubyMine, GoLand, and potentially others (Rider and CLion are unsupported though). @@ -177,7 +180,7 @@ https://github.com/user-attachments/assets/6eaa9aa1-610d-4723-a2d6-bf1e487ba753 > [!NOTE] > Some language servers require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details. -**Starting the MCP Server**. The easiest way to start the Serena MCP server is by running the latest version from GitHub using uvx. +**Starting the MCP Server**. The easiest way to start the Serena MCP server is by running the latest version from GitHub using uvx. Issue this command to see available options: ```bash diff --git a/docs/autogen_docs.py b/docs/autogen_docs.py index 5e1dae72..5372dd09 100644 --- a/docs/autogen_docs.py +++ b/docs/autogen_docs.py @@ -194,7 +194,7 @@ def autogen_about_intro_features(): f.write(autogen_info) f.write("# About Serena\n\n") f.write(f"**{tagline}**\n\n") - about_text = about_text.replace("resources/serena-block-diagram.png", "https://raw.githubusercontent.com/oraios/serena/main/resources/serena-block-diagram.svg") + about_text = about_text.replace("resources/serena-block-diagram.svg", "https://raw.githubusercontent.com/oraios/serena/main/resources/serena-block-diagram.svg") about_text = re.subn(r"^See the \[Quick Start.*?$", r"", about_text, flags=re.MULTILINE)[0] f.write(f"{about_text}\n\n") From d958f7010330900116c6e33d357f1e31d3e2235b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 4 Apr 2026 13:14:57 +0200 Subject: [PATCH 039/190] Improve language server docs --- README.md | 2 +- docs/01-about/020_programming-languages.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 218e2e6c..3102dc65 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ https://github.com/user-attachments/assets/6eaa9aa1-610d-4723-a2d6-bf1e487ba753 **Prerequisites**. Serena is managed by *uv*. If you don’t already have it, you need to [install uv](https://docs.astral.sh/uv/getting-started/installation/) before proceeding. > [!NOTE] -> Some language servers require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details. +> When using the language server backend, some additional dependencies to be installed for certain languages; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details. **Starting the MCP Server**. The easiest way to start the Serena MCP server is by running the latest version from GitHub using uvx. Issue this command to see available options: diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index ceb1f86d..bc556a43 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -28,7 +28,7 @@ We currently provide direct, out-of-the-box support for the programming language Some languages require additional installations or setup steps, as noted. * **AL** -* **Ansible** +* **Ansible** (experimental; requires Node.js and npm; automatically installs `@ansible/ansible-language-server`; must be explicitly specified in the `languages` entry in the `project.yml`; requires `ansible` in PATH for full functionality) the upstream `@ansible/ansible-language-server@1.2.3` supports hover, completion, definition, @@ -58,9 +58,9 @@ Some languages require additional installations or setup steps, as noted. (requires installation of `gopls`) * **Groovy** (requires local groovy-language-server.jar setup via `GROOVY_LS_JAR_PATH` or configuration) -* **Haskell** +* **Haskell** (automatically locates HLS via ghcup, stack, or system PATH; supports Stack and Cabal projects) -* **HLSL / GLSL / WGSL** +* **HLSL / GLSL / WGSL** (uses [shader-language-server](https://github.com/antaalt/shader-sense) (language `hlsl`); automatically downloaded; on macOS, requires Rust toolchain for building from source; note: reference search is not supported by this language server) @@ -97,7 +97,7 @@ Some languages require additional installations or setup steps, as noted. (requires [rustup](https://rustup.rs/) - uses rust-analyzer from your toolchain) * **Scala** (requires some [manual setup](../03-special-guides/scala_setup_guide_for_serena); uses Metals LSP) -* **Solidity** +* **Solidity** (experimental; requires Node.js and npm; automatically installs `@nomicfoundation/solidity-language-server`; works best with a `foundry.toml` or `hardhat.config.js` in the project root) * **Swift** From 66cb41a66a73f5f6d639c3b8a487903a11996afa Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 4 Apr 2026 13:39:11 +0200 Subject: [PATCH 040/190] Add callout making clear which purpose the JetBrains plugin serves --- docs/02-usage/025_jetbrains_plugin.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/02-usage/025_jetbrains_plugin.md b/docs/02-usage/025_jetbrains_plugin.md index c8af4a14..13fb7697 100644 --- a/docs/02-usage/025_jetbrains_plugin.md +++ b/docs/02-usage/025_jetbrains_plugin.md @@ -14,7 +14,15 @@ leverage the powerful code analysis and editing capabilities of your JetBrains I We recommend the JetBrains plugin as the preferred way of using Serena, especially for users of JetBrains IDEs. -How it works: +```{admonition} *Note:* The plugin is a language intelligence backend for the Serena MCP server. +:class: note +It is *not* a UI extension for direct agent interaction (like Copilot) or anything of the sort. +You still interact with your regular client – be it external to your IDE (like Claude Code) or internal (like JetBrains AI Assistant) – +and connect it to the Serena MCP server. +The plugin simply enables the Serena MCP server to directly leverage capabilities of your JetBrains IDE! +``` + +**How it works:** 1. Install the plugin in your JetBrains IDE 2. Configure Serena to use the JetBrains language backend (see [below](configure-jetbrains)) 3. Open the project you want to work on in your JetBrains IDE and activate it in Serena (see [below](jetbrains-workflow)) From 0e229586dc1795fd962cc3f2b3c4b2018d87aab8 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 4 Apr 2026 15:18:33 +0200 Subject: [PATCH 041/190] Readme, docs: clarified config file creation and JB plugin role Also added a sentence on using with the Codex app --- README.md | 14 ++++++++++---- docs/02-usage/025_jetbrains_plugin.md | 19 ++++++++++++++++--- docs/02-usage/030_clients.md | 8 +++++++- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3102dc65..be17cde6 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ complex projects! There are two alternative technologies powering these capabilities: -* **Language servers** implementing the language server Protocol (LSP) — the free/open-source alternative. +* **Language servers** implementing the language server Protocol (LSP) — the free/open-source alternative + which is used by default. * The **Serena JetBrains Plugin**, which leverages the powerful code analysis and editing capabilities of your JetBrains IDE (paid plugin; free trial available). @@ -180,16 +181,21 @@ https://github.com/user-attachments/assets/6eaa9aa1-610d-4723-a2d6-bf1e487ba753 > [!NOTE] > When using the language server backend, some additional dependencies to be installed for certain languages; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details. -**Starting the MCP Server**. The easiest way to start the Serena MCP server is by running the latest version from GitHub using uvx. -Issue this command to see available options: +**Starting the MCP Server**. The easiest way to start the Serena MCP server is by running the latest version from GitHub using uvx: ```bash -uvx -p 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --help +uvx -p 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server ``` +If this is the first time you are starting Serena, the necessary configuration files will be created, and +you should see the Serena dashboard appear. You can terminate the server by pressing Ctrl+C (multiple times, if necessary). + **Configuring Your Client**. To connect Serena to your preferred MCP client, you typically need to [configure a launch command in your client](https://oraios.github.io/serena/02-usage/030_clients.html). Follow the link for specific instructions on how to set up Serena for Claude Code, Codex, Claude Desktop, MCP-enabled IDEs and other clients (such as local and web-based GUIs). +**Switching to the JetBrains Backend**. The default code intelligence backend for Serena is the free LSP backend. +For using the more powerful JetBrains backend, follow the instructions on the [JetBrains Plugin page](https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html). + > [!TIP] > While getting started quickly is easy, Serena is a powerful toolkit with many configuration options. > We highly recommend reading through the [user guide](https://oraios.github.io/serena/02-usage/000_intro.html) to get the most out of Serena. diff --git a/docs/02-usage/025_jetbrains_plugin.md b/docs/02-usage/025_jetbrains_plugin.md index 13fb7697..89007a6a 100644 --- a/docs/02-usage/025_jetbrains_plugin.md +++ b/docs/02-usage/025_jetbrains_plugin.md @@ -1,7 +1,11 @@ # The Serena JetBrains Plugin -The [JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) allows Serena to +The [JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) allows the Serena MCP server to leverage the powerful code analysis and editing capabilities of your JetBrains IDE. +This page explains how to install the plugin and how to configure Serena for using it. +You will still need to set up the Serena MCP server +itself, so make sure to follow the [installation instructions](020_running.md) and connect the MCP server to your +LLM-based client as described in [client setup](030_clients.md) in addition to following the instructions below. ```{raw} html

@@ -61,9 +65,18 @@ After installing the plugin, you need to configure Serena to use it. **Central Configuration**. -Edit the global Serena configuration file located at `~/.serena/serena_config.yml` +If you ever executed Serena before, you will have the global Serena configuration file located at `~/.serena/serena_config.yml` (`%USERPROFILE%\.serena\serena_config.yml` on Windows). -Change the `language_backend` setting as follows: + +If the configuration file does not exist, run + +```shell +uvx -p 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server +``` + +(see [running Serena](020_running.md) for alternative options) to install all dependencies and create configuration files. Terminate the server with `Ctrl+C` after it has started. + +In your configuration, change the `language_backend` setting as follows: ```yaml language_backend: JetBrains diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index e0c13e73..117a77ce 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -147,7 +147,7 @@ While serena can be directly installed from the GitHub MCP server registry, we r ## Codex -Serena works with OpenAI's Codex CLI out of the box, but you have to use the `codex` context for it to work properly. (The technical reason is that Codex doesn't fully support the MCP specifications, so some massaging of tools is required.). +Serena works with OpenAI's Codex CLI and app out of the box, but you have to use the `codex` context for it to work properly. (The technical reason is that Codex doesn't fully support the MCP specifications, so some massaging of tools is required.). Add a [run command](020_running) to `~/.codex/config.toml` to configure Serena for all Codex sessions; create the file if it does not exist. @@ -162,6 +162,12 @@ args = ["-p", "3.13", "--from", "git+https://github.com/oraios/serena", "serena" The larger startup timeout is to permit uvx to download the necessary dependencies. Once downloaded, the startup time is much faster. +:::{note} +The codex app does not start a session in the project's directory, so when using the app, you will have to +ask Codex to "Activate the current dir as project using serena" at the start of each session. +This is not necessary when using the codex CLI. +::: + :::{attention} Codex currently ignores the [instructions](https://blog.modelcontextprotocol.io/posts/2025-11-03-using-server-instructions/) property of MCP servers, so it is recommended to prompt the agent to From c3a90d2a0769795b7dd8b5d6194fd7e7994ee900 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 4 Apr 2026 21:17:54 +0200 Subject: [PATCH 042/190] Fix reactivation of the same project restarting language servers Reactivation should do nothing at all. Fixes #1280 --- CHANGELOG.md | 7 ++++++- src/serena/agent.py | 21 +++++++++++++++------ src/serena/tools/config_tools.py | 4 +++- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 701cfce7..8d92b384 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ -# Latest +# Unreleased (main) Status of the `main` branch. Changes prior to the next official version change will appear here. +* Fixes: + - Fix reactivation of the same project restarting language servers #1280 + +# 1.0.0 + * Memories: * Add `ignored_memory_patterns` configuration option (regex-based) to completely exclude matching memories from listing, reading, and writing. Configurable at both global diff --git a/src/serena/agent.py b/src/serena/agent.py index f036e908..7b1369e4 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -783,10 +783,17 @@ class SerenaAgent: """ return self._language_backend == LanguageBackend.LSP - def _activate_project(self, project: Project, update_active_modes: bool = True, update_active_tools: bool = True) -> None: + def _activate_project(self, project: Project, update_active_modes: bool = True, update_active_tools: bool = True) -> bool: + """ + :return: True if the project was newly activated, False if it was already active + """ + # check if the project is already active + if self._active_project is not None and self._active_project.project_root == project.project_root: + return False + log.info(f"Activating {project.project_name} at {project.project_root}") - # Check if the project requires a different language backend than the one initialized at startup + # check if the project requires a different language backend than the one initialized at startup project_backend = project.project_config.language_backend if project_backend is not None and project_backend != self._language_backend: raise ValueError( @@ -822,14 +829,18 @@ class SerenaAgent: if self._project_activation_callback is not None: self._project_activation_callback() + return True + def activate_project_from_path_or_name( self, project_root_or_name: str, update_active_modes: bool = True, update_active_tools: bool = True - ) -> Project: + ) -> bool: """ Activate a project from a path or a name. If the project was already registered, it will just be activated. If the argument is a path at which no Serena project previously existed, the project will be created beforehand. Raises ProjectNotFoundError if the project could neither be found nor created. + + :return: True if the project was newly activated, False if it was already active """ project_instance: Project | None = self.serena_config.get_project(project_root_or_name) if project_instance is not None: @@ -844,9 +855,7 @@ class SerenaAgent: f"Existing project names: {self.serena_config.project_names}" ) - self._activate_project(project_instance, update_active_modes=update_active_modes, update_active_tools=update_active_tools) - - return project_instance + return self._activate_project(project_instance, update_active_modes=update_active_modes, update_active_tools=update_active_tools) def get_active_tool_names(self) -> list[str]: """ diff --git a/src/serena/tools/config_tools.py b/src/serena/tools/config_tools.py index bdab4706..0e92924a 100644 --- a/src/serena/tools/config_tools.py +++ b/src/serena/tools/config_tools.py @@ -28,7 +28,9 @@ class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject): :param project: the name of a registered project to activate or a path to a project directory """ - self.agent.activate_project_from_path_or_name(project) + is_new_activation = self.agent.activate_project_from_path_or_name(project) + if not is_new_activation: + return "Project was already active." result = self.agent.get_project_activation_message() result += "\nIMPORTANT: If you have not yet read the 'Serena Instructions Manual', do it now before continuing!" return result From 599266ec5f74a4cfce04f1fcf9b81ac3dace6f81 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 4 Apr 2026 21:29:19 +0200 Subject: [PATCH 043/190] Revise changelog for v1.0.0 --- CHANGELOG.md | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d92b384..c3db50aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,20 +7,6 @@ Status of the `main` branch. Changes prior to the next official version change w # 1.0.0 -* Memories: - * Add `ignored_memory_patterns` configuration option (regex-based) to completely exclude - matching memories from listing, reading, and writing. Configurable at both global - (`serena_config.yml`) and project (`project.yml`) level, merged additively. - Useful for projects with large numbers of archived memory files that inflate - `activate_project` output. - -* New language support: - * Add Solidity language server support (`Language.SOLIDITY`) using the - Nomic Foundation `@nomicfoundation/solidity-language-server`. Automatically - installed via npm. Supports `.sol` files with go-to-definition, find references, - document symbols, hover, and diagnostics. Works best with a `foundry.toml` or - `hardhat.config.js` in the project root. - * General: * Add monorepo/multi-language support * Project configuration files (`project.yml`) can now define multiple languages. @@ -43,9 +29,9 @@ Status of the `main` branch. Changes prior to the next official version change w * Log page now has save (downloads a snapshot) and clear (resets log view) buttons alongside the existing copy button * Language server backend: * New two-tier caching of language server document symbols and considerable performance improvements surrounding symbol retrieval/indexing - * Allow passing language server specific settings through `ls_specific_settings` field (in `serena_config.yml`) + * Allow passing language server-specific settings through `ls_specific_settings` field (in `serena_config.yml`) * Add the JetBrains language backend as an alternative to language servers - * Improve management of the Serena projects + * Improve management of Serena projects * Facilitate project activation based on the current directory (through the `--project-from-cwd` parameter) * Add notion of a "single-project context" (flag `single_project`), allowing user-defined contexts to behave like the built-in `ide-assistant` context (where the available tools are restricted to ones required by the active @@ -54,16 +40,19 @@ Status of the `main` branch. Changes prior to the next official version change w locations outside of the project folder, thus improving support for read-only projects. * Add support for `project.local.yml` for local overrides that should not be versioned * Various fixes related to indexing, special paths and determination of ignored paths - -* Client support: - * New mode `oaicompat-agent` and extensions enhancing OpenAI tool compatibility, permitting Serena to work with llama.cpp + * Memories: + * Add support for global memories (shared across projects) + * Add `read_only_memory_patterns` configuration option + * Add `ignored_memory_patterns` configuration option + * Improved client support, e.g. new mode `oaicompat-agent` and extensions enhancing OpenAI tool compatibility * Tools: - * Symbol information (hover, docstring, quick-info) is now provided as part of `find_symbol` and related tool responses. + * Additional symbol meta-information (hover, docstring, quick-info) is now provided as part of `find_symbol` and related tool responses. * Added `QueryProjectTool` and `ListQueryableProjectTool` (see above) * Added `RenameSymbolTool` for renaming symbols across the codebase (if LS supports this operation). * Replaced `ReplaceRegexTool` with `ReplaceContentTool`, which supports both plain text and regex-based replacements - (and which requires no escaping in the replacement text, making it more robust) + (and which requires no escaping in the replacement text, making it more robust) + * Add JetBrains tools which leverage the corresponding JetBrains language backend through our plugin * Decreased `TOOL_DEFAULT_MAX_ANSWER_LENGTH` to be in accordance with (below) typical max-tokens configurations * Language support: @@ -95,7 +84,7 @@ Status of the `main` branch. Changes prior to the next official version change w * **Add support for MATLAB** via the official MathWorks MATLAB Language Server. Requires MATLAB R2021b or later and Node.js. Set `MATLAB_PATH` environment variable or configure `matlab_path` in `ls_specific_settings`. Supports .m, .mlx, and .mlapp files with code completion, diagnostics, go-to-definition, find references, document symbols, formatting, and rename. * **Add support for Pascal** via the official Pascal Language Server. * **C/C++ alternate LS (ccls)**: Add experimental, opt-in support for ccls as an alternative backend to clangd. Enable via `cpp_ccls` in project configuration. Requires `ccls` installed and ideally a `compile_commands.json` at repo root. - + * **Add support for Solidity** via the Nomic Foundation `@nomicfoundation/solidity-language-server` (automatically installed via npm) # 0.1.4 From 98c7572801f19a5686e3a1848b248a15e83a3229 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 5 Apr 2026 10:11:19 +0200 Subject: [PATCH 044/190] Hotfix: disable pywebview dashboard until memory leak is resolved --- src/serena/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 257b8540..c06ecd74 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -715,7 +715,7 @@ class SerenaDashboardViewer: # The dashboard viewer (with system tray) is technically supported only on Windows and macOS. # Linux support is problematic; see https://github.com/oraios/serena/pull/1117#issuecomment-4128753943 supported_platforms = [ - "win32", + # "win32", # NOTE: Disabling macOS support for now, because the tray behaviour is suboptimal (too many icons when # subagents are spawned, etc.) # "darwin" From 7d9e0508d6a636c33806d15794b7082feb257449 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 5 Apr 2026 12:05:42 +0200 Subject: [PATCH 045/190] Revert "Hotfix: disable pywebview dashboard until memory leak is resolved" This reverts commit 98c7572801f19a5686e3a1848b248a15e83a3229. The issue that was supposed to be fixed was already resolved by 87accc6. --- src/serena/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index c06ecd74..257b8540 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -715,7 +715,7 @@ class SerenaDashboardViewer: # The dashboard viewer (with system tray) is technically supported only on Windows and macOS. # Linux support is problematic; see https://github.com/oraios/serena/pull/1117#issuecomment-4128753943 supported_platforms = [ - # "win32", + "win32", # NOTE: Disabling macOS support for now, because the tray behaviour is suboptimal (too many icons when # subagents are spawned, etc.) # "darwin" From 554be852017f35d04403e3068840758c99b0eb8e Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 5 Apr 2026 12:53:42 +0200 Subject: [PATCH 046/190] Show Serena version in dashboard #1078 --- src/serena/agent.py | 6 ++++-- src/serena/dashboard.py | 2 ++ src/serena/resources/dashboard/dashboard.js | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 7b1369e4..4461d682 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -269,6 +269,8 @@ class SerenaAgent: :param memory_log_handler: a MemoryLogHandler instance from which to read log messages; if None, a new one will be created if necessary. """ + self.version = serena_version() + # obtain serena configuration using the decoupled factory function self.serena_config = serena_config or SerenaConfig.from_config_file() @@ -334,7 +336,7 @@ class SerenaAgent: # log fundamental information log.info( - f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()}; " + f"Starting Serena server (version={self.version}, process id={os.getpid()}, parent process id={os.getppid()}; " f"language backend={self.serena_config.language_backend.name}); Python version={platform.python_version()}, platform={platform.platform()}" ) log.info("Configuration file: %s", self.serena_config.config_file_path) @@ -875,7 +877,7 @@ class SerenaAgent: :return: a string overview of the current configuration, including the active and available configuration options """ result_str = "Current configuration:\n" - result_str += f"Serena version: {serena_version()}\n" + result_str += f"Serena version: {self.version}\n" result_str += f"Loglevel: {self.serena_config.log_level}, trace_lsp_communication={self.serena_config.trace_lsp_communication}\n" if self._active_project is not None: result_str += f"Active project: {self._active_project.project_name}\n" diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 257b8540..76a32b65 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -60,6 +60,7 @@ class ResponseConfigOverview(BaseModel): languages: list[str] encoding: str | None current_client: str | None + serena_version: str class ResponseAvailableLanguages(BaseModel): @@ -537,6 +538,7 @@ class SerenaDashboardAPI: languages=languages, encoding=encoding, current_client=Tool.get_last_tool_call_client_str(), + serena_version=self._agent.version, ) def _shutdown(self) -> None: diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index 5819cec3..dffd10ef 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -637,6 +637,8 @@ class Dashboard { let html = '

'; + html += '
Version:
' + config.serena_version + '
'; + // Project info html += '
Active Project:
'; if (config.active_project.name && config.active_project.path) { From 8856d825903b21590c5190ff768c883eabb8503a Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 5 Apr 2026 13:12:23 +0200 Subject: [PATCH 047/190] Adjust instructions for Claude Code: Newer versions interpret `-p` parameter Even though the parameter is after the separator `--`, newer versions (>2.1.76) fail to add the MCP server and interpret the arguments directly. Fixes #1279 --- docs/02-usage/030_clients.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index 117a77ce..4a1fd4db 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -79,7 +79,7 @@ Serena is a great way to make Claude Code both cheaper and more powerful! use this command: ```shell -claude mcp add serena -- uvx -p 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --context claude-code --project "$(pwd)" +claude mcp add serena -- uvx --python 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --context claude-code --project "$(pwd)" ``` Note: From 8189784f78623b346809bf81ab60d80b7f3af2c8 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 5 Apr 2026 14:09:12 +0200 Subject: [PATCH 048/190] Fix test_activate_project_rejects_backend_mismatch (must use different project path for second activation) --- test/serena/config/test_serena_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/serena/config/test_serena_config.py b/test/serena/config/test_serena_config.py index 8a2eedbb..b3632b5c 100644 --- a/test/serena/config/test_serena_config.py +++ b/test/serena/config/test_serena_config.py @@ -270,10 +270,10 @@ class TestEffectiveLanguageBackend: # Add a second project that requires JetBrains jb_project = Project( - project_root=str(Path(__file__).parent.parent / "resources" / "repos" / "python" / "test_repo"), + project_root=str(Path(__file__).parent.parent / "resources" / "repos" / "java" / "test_repo"), project_config=ProjectConfig( project_name="jb_proj", - languages=[Language.PYTHON], + languages=[Language.JAVA], language_backend=LanguageBackend.JETBRAINS, ), serena_config=config, From d1275627a8863734a0927a50a4e74b3c56412ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20H=E1=BB=A9a?= Date: Mon, 6 Apr 2026 17:31:28 +0700 Subject: [PATCH 049/190] Support mise, asdf and rvm for Ruby version managers (#1254) Backwards compatible since rbenv is used first if it is found. Then shutil.which is used to check availability of alternative ruby version managers --- src/solidlsp/language_servers/ruby_lsp.py | 61 +++++++++++++++-------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/src/solidlsp/language_servers/ruby_lsp.py b/src/solidlsp/language_servers/ruby_lsp.py index 4a3d41b8..7bd56256 100644 --- a/src/solidlsp/language_servers/ruby_lsp.py +++ b/src/solidlsp/language_servers/ruby_lsp.py @@ -109,39 +109,57 @@ class RubyLsp(SolidLanguageServer): """ ls_specific_settings = solidlsp_settings.get_ls_specific_settings(Language.RUBY) ruby_lsp_version = ls_specific_settings.get("ruby_lsp_version", RUBY_LSP_VERSION) - # Detect rbenv-managed Ruby environment - # When .ruby-version exists, it indicates the project uses rbenv for version management. - # rbenv automatically reads .ruby-version to determine which Ruby version to use. - # Using "rbenv exec" ensures commands run with the correct Ruby version and its gems. + # Detect Ruby version manager environment + # Using the version manager's exec wrapper ensures commands run with the correct Ruby version + # and its gems. # - # Why rbenv is preferred over system Ruby: - # - Respects project-specific Ruby versions - # - Avoids bundler version mismatches between system and project - # - Ensures consistent environment across developers - # - # Fallback behavior: - # If .ruby-version doesn't exist or rbenv isn't installed, we fall back to system Ruby. - # This may cause issues if: - # - System Ruby version differs from what the project expects - # - System bundler version is incompatible with Gemfile.lock - # - Project gems aren't installed in system Ruby + # Priority order: + # 1. rbenv - .ruby-version + rbenv binary; uses "rbenv exec " + # 2. mise - .ruby-version + mise binary; uses "mise exec ruby -- " (scoped to ruby + # tool only, avoids activating unrelated tools in .tool-versions) + # 3. asdf - .tool-versions + asdf binary; uses "asdf exec " + # 4. RVM - .ruby-version + rvm-exec binary; uses "rvm-exec ruby-X.Y.Z " + # rvm-exec requires the version string as first arg (e.g. "ruby-3.2.0"); + # may live at ~/.rvm/bin/rvm-exec if not on PATH + # 5. system Ruby - fallback, may cause version/gem mismatches ruby_version_file = os.path.join(repository_root_path, ".ruby-version") + tool_versions_file = os.path.join(repository_root_path, ".tool-versions") + rvm_exec_path = shutil.which("rvm-exec") or os.path.join(os.path.expanduser("~"), ".rvm", "bin", "rvm-exec") + use_rbenv = os.path.exists(ruby_version_file) and shutil.which("rbenv") is not None + use_mise = os.path.exists(ruby_version_file) and not use_rbenv and shutil.which("mise") is not None + use_asdf = os.path.exists(tool_versions_file) and not use_rbenv and not use_mise and shutil.which("asdf") is not None + use_rvm = os.path.exists(ruby_version_file) and not use_rbenv and not use_mise and not use_asdf and os.path.exists(rvm_exec_path) if use_rbenv: ruby_cmd = ["rbenv", "exec", "ruby"] bundle_cmd = ["rbenv", "exec", "bundle"] log.info(f"Using rbenv-managed Ruby (found {ruby_version_file})") + elif use_mise: + ruby_cmd = ["mise", "exec", "ruby", "--", "ruby"] + bundle_cmd = ["mise", "exec", "ruby", "--", "bundle"] + log.info(f"Using mise-managed Ruby (found {ruby_version_file})") + elif use_asdf: + ruby_cmd = ["asdf", "exec", "ruby"] + bundle_cmd = ["asdf", "exec", "bundle"] + log.info(f"Using asdf-managed Ruby (found {tool_versions_file})") + elif use_rvm: + with open(ruby_version_file) as _f: + raw_version = _f.read().strip() + rvm_ruby_version = raw_version if raw_version.startswith("ruby-") else f"ruby-{raw_version}" + ruby_cmd = [rvm_exec_path, rvm_ruby_version, "ruby"] + bundle_cmd = [rvm_exec_path, rvm_ruby_version, "bundle"] + log.info(f"Using RVM-managed Ruby (found {ruby_version_file}, version={rvm_ruby_version})") else: ruby_cmd = ["ruby"] bundle_cmd = ["bundle"] - if os.path.exists(ruby_version_file): + if os.path.exists(ruby_version_file) or os.path.exists(tool_versions_file): log.warning( - f"Found {ruby_version_file} but rbenv is not installed. " - "Using system Ruby. Consider installing rbenv for better version management: https://github.com/rbenv/rbenv", + "Found Ruby version file but no supported version manager (rbenv, mise, asdf, rvm) detected. " + "Using system Ruby. Consider installing mise: https://mise.jdx.dev", ) else: - log.info("No .ruby-version file found, using system Ruby") + log.info("No Ruby version file found, using system Ruby") # Check if Ruby is installed try: @@ -166,9 +184,10 @@ class RubyLsp(SolidLanguageServer): except FileNotFoundError as e: raise RuntimeError( "Ruby is not installed or not found in PATH. Please install Ruby using one of these methods:\n" + " - Using mise: mise install ruby && mise use ruby (https://mise.jdx.dev)\n" " - Using rbenv: rbenv install 3.0.0 && rbenv global 3.0.0\n" - " - Using RVM: rvm install 3.0.0 && rvm use 3.0.0 --default\n" - " - Using asdf: asdf install ruby 3.0.0 && asdf global ruby 3.0.0\n" + " - Using asdf: asdf install ruby 3.0.0 && asdf global ruby 3.0.0\n" + " - Using RVM: rvm install 3.0.0 && rvm use 3.0.0 --default\n" " - System package manager (brew install ruby, apt install ruby, etc.)" ) from e From 854507524a41ee9eeb1b6212c861c5ef135ffd10 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 6 Apr 2026 12:55:10 +0200 Subject: [PATCH 050/190] Tests: improve tests for vue (inspired by #1199) --- test/solidlsp/vue/test_vue_error_cases.py | 71 +++-------------------- 1 file changed, 7 insertions(+), 64 deletions(-) diff --git a/test/solidlsp/vue/test_vue_error_cases.py b/test/solidlsp/vue/test_vue_error_cases.py index ee6a5496..3a6aac8b 100644 --- a/test/solidlsp/vue/test_vue_error_cases.py +++ b/test/solidlsp/vue/test_vue_error_cases.py @@ -116,76 +116,19 @@ class TestVueNonExistentFiles: """Tests for handling non-existent files.""" @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True) - def test_document_symbols_nonexistent_file(self, language_server: SolidLanguageServer) -> None: - """Test requesting document symbols from non-existent file. - - Expected behavior: Should raise FileNotFoundError or return empty result. - """ - nonexistent_file = os.path.join("src", "components", "NonExistent.vue") - - # Should raise an appropriate exception or return empty result - try: - result = language_server.request_document_symbols(nonexistent_file) - # If no exception, verify result is empty or indicates file not found - symbols = result.get_all_symbols_and_roots() - assert len(symbols[0]) == 0, f"Non-existent file should return empty symbols, got {len(symbols[0])} symbols" - except (FileNotFoundError, Exception) as e: - # Expected - file doesn't exist - assert True, f"Appropriately raised exception for non-existent file: {e}" - - @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True) - def test_containing_symbol_nonexistent_file(self, language_server: SolidLanguageServer) -> None: - """Test requesting containing symbol from non-existent file. - - Expected behavior: Should raise FileNotFoundError or return None. - """ - nonexistent_file = os.path.join("src", "components", "NonExistent.vue") - - # Should raise an appropriate exception or return None - try: - result = language_server.request_containing_symbol(nonexistent_file, 10, 10) - # If no exception, verify result indicates file not found - assert result is None or result == {}, f"Non-existent file should return None or empty dict, got: {result}" - except (FileNotFoundError, Exception) as e: - # Expected - file doesn't exist - assert True, f"Appropriately raised exception for non-existent file: {e}" - - @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True) - def test_references_nonexistent_file(self, language_server: SolidLanguageServer) -> None: + def test_requesting_on_nonexistent_file(self, language_server: SolidLanguageServer) -> None: """Test requesting references from non-existent file. Expected behavior: Should raise FileNotFoundError or return empty list. """ nonexistent_file = os.path.join("src", "components", "NonExistent.vue") - # Should raise an appropriate exception or return empty list - try: - result = language_server.request_references(nonexistent_file, 10, 10) - # If no exception, verify result is empty - assert result is None or isinstance(result, list), f"Non-existent file should return None or list, got: {result}" - if isinstance(result, list): - assert len(result) == 0, f"Non-existent file should return empty list, got {len(result)} references" - except (FileNotFoundError, Exception) as e: - # Expected - file doesn't exist - assert True, f"Appropriately raised exception for non-existent file: {e}" - - @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True) - def test_definition_nonexistent_file(self, language_server: SolidLanguageServer) -> None: - """Test requesting definition from non-existent file. - - Expected behavior: Should raise FileNotFoundError or return empty list. - """ - nonexistent_file = os.path.join("src", "components", "NonExistent.vue") - - # Should raise an appropriate exception or return empty list - try: - result = language_server.request_definition(nonexistent_file, 10, 10) - # If no exception, verify result is empty - assert isinstance(result, list), f"request_definition should return a list, got: {type(result)}" - assert len(result) == 0, f"Non-existent file should return empty list, got {len(result)} definitions" - except (FileNotFoundError, Exception) as e: - # Expected - file doesn't exist - assert True, f"Appropriately raised exception for non-existent file: {e}" + with pytest.raises(FileNotFoundError): + language_server.request_references(nonexistent_file, 10, 10) + with pytest.raises(FileNotFoundError): + language_server.request_definition(nonexistent_file, 10, 10) + with pytest.raises(FileNotFoundError): + language_server.request_document_symbols(nonexistent_file) class TestVueUndefinedSymbols: From ece96a164999e96a80b64115809b4df414bebf49 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 6 Apr 2026 13:06:02 +0200 Subject: [PATCH 051/190] Tests: improve tests for python (inspired by #1199) --- test/solidlsp/python/test_symbol_retrieval.py | 48 +++---------------- 1 file changed, 6 insertions(+), 42 deletions(-) diff --git a/test/solidlsp/python/test_symbol_retrieval.py b/test/solidlsp/python/test_symbol_retrieval.py index 3baff0a0..b95155d2 100644 --- a/test/solidlsp/python/test_symbol_retrieval.py +++ b/test/solidlsp/python/test_symbol_retrieval.py @@ -224,22 +224,11 @@ class TestLanguageServerSymbols: # Find the line number where create_user is called # This could vary, so we'll use a relative position that makes sense - defining_symbol = language_server.request_defining_symbol(examples_file_path, 10, 30) - - # Verify that we found the defining symbol - should be the create_user method - # Because this might fail if the structure isn't exactly as expected, we'll use try-except - try: - assert defining_symbol is not None - assert defining_symbol.get("name") == "create_user" - # The defining symbol should be in the services.py file - if "location" in defining_symbol and "uri" in defining_symbol["location"]: - assert "services.py" in defining_symbol["location"]["uri"] - except AssertionError: - # If the file structure doesn't match what we expect, we can't guarantee this test - # will pass, so we'll consider it a warning rather than a failure - import warnings - - warnings.warn("Could not verify method call definition - file structure may differ from expected") + defining_symbol = language_server.request_defining_symbol(examples_file_path, 46, 29) + assert defining_symbol is not None + assert defining_symbol.get("name") == "create_user" + # The defining symbol should be in the services.py file + assert "services.py" in defining_symbol["location"]["uri"] @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None: @@ -331,32 +320,7 @@ class TestLanguageServerSymbols: # Step 3: Verify that they refer to the same symbol assert defining_symbol["kind"] == containing_symbol["kind"] - if "location" in defining_symbol and "location" in containing_symbol: - assert defining_symbol["location"]["uri"] == containing_symbol["location"]["uri"] - - # The integration test is successful if we've gotten this far, - # as it demonstrates the integration between request_containing_symbol and request_defining_symbol - - # Try to get the container information for our method, but be flexible - # since implementations may vary - container_name = defining_symbol.get("containerName", None) - if container_name and "UserService" in container_name: - # If containerName contains UserService, that's a valid implementation - pass - else: - # Try an alternative approach - looking for the containing class - try: - # Look for the class symbol in the file - for line in range(5, 12): # Approximate range where UserService class should be defined - symbol = language_server.request_containing_symbol(file_path, line, 5) # column 5 should be within class definition - if symbol and symbol.get("name") == "UserService" and symbol.get("kind") == SymbolKind.Class.value: - # Found the class - this is also a valid implementation - break - except Exception: - # Just log a warning - this is an alternative verification and not essential - import warnings - - warnings.warn("Could not verify container hierarchy - implementation detail") + assert defining_symbol["location"]["uri"] == containing_symbol["location"]["uri"] @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None: From 4ff2993bc0e1c71c33db4422bc8bb24641a329f2 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 6 Apr 2026 13:41:09 +0200 Subject: [PATCH 052/190] Docs: recommend user-level setup for claude code by default Also added a note on startup timeouts --- docs/02-usage/030_clients.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index 4a1fd4db..05610861 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -75,8 +75,23 @@ object, e.g. Serena is a great way to make Claude Code both cheaper and more powerful! -**Per-Project Configuration.** To add the Serena MCP server to the current project in the current directory, -use this command: +:::{note} +Serena might take some time to start up, especially on the first run. +To make sure that enough time is available for the server to start, +set `MCP_TIMEOUT` to a sufficiently high value (e.g. by adding `export MCP_TIMEOUT=60000` to your shell profile) +before starting Claude Code. + +Confirm that Claude Code is connected to Serena by running the `/mcp` command and by reconnecting, if necessary. +::: + +**Global Configuration**. To add the Serena MCP server for all your projects, use the user-level configuration of claude code and the `--project-from-cwd` flag: + +```shell +claude mcp add --scope user serena -- uvx --python 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --context=claude-code --project-from-cwd +``` + +**Per-Project Configuration.** Alternatively, to add Serena only for the current project in the current directory, +use the command: ```shell claude mcp add serena -- uvx --python 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --context claude-code --project "$(pwd)" @@ -89,12 +104,6 @@ Note: that Serena is configured to work on the current project from the get-go, following Claude Code's mode of operation. -**Global Configuration**. Alternatively, use `--project-from-cwd` for user-level configuration that works across all projects: - -```shell -claude mcp add --scope user serena -- uvx -p 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --context=claude-code --project-from-cwd -``` - Whenever you start Claude Code, Serena will search up from the current directory for `.serena/project.yml` or `.git` markers, activating the containing directory as the project (if any). This mechanism makes it suitable for a single global MCP configuration. From dcc49f19f9caa22a8bd32c3c99cf4299dae3763f Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 6 Apr 2026 14:42:33 +0200 Subject: [PATCH 053/190] Fix git commit id in version not being computed from Serena directory --- CHANGELOG.md | 4 ++++ src/serena/util/git.py | 10 ++++++---- src/serena/util/shell.py | 6 ++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3db50aa..44e37a72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,12 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* Dashboard: + - Add version display + * Fixes: - Fix reactivation of the same project restarting language servers #1280 + - Fix git commit id in version # 1.0.0 diff --git a/src/serena/util/git.py b/src/serena/util/git.py index 865ae59e..ec88179f 100644 --- a/src/serena/util/git.py +++ b/src/serena/util/git.py @@ -2,6 +2,7 @@ import logging from sensai.util.git import GitStatus +from ..constants import REPO_ROOT from .shell import subprocess_check_output log = logging.getLogger(__name__) @@ -9,10 +10,11 @@ log = logging.getLogger(__name__) def get_git_status() -> GitStatus | None: try: - commit_hash = subprocess_check_output(["git", "rev-parse", "HEAD"]) - unstaged = bool(subprocess_check_output(["git", "diff", "--name-only"])) - staged = bool(subprocess_check_output(["git", "diff", "--staged", "--name-only"])) - untracked = bool(subprocess_check_output(["git", "ls-files", "--others", "--exclude-standard"])) + cwd = REPO_ROOT + commit_hash = subprocess_check_output(["git", "rev-parse", "HEAD"], cwd=cwd) + unstaged = bool(subprocess_check_output(["git", "diff", "--name-only"], cwd=cwd)) + staged = bool(subprocess_check_output(["git", "diff", "--staged", "--name-only"], cwd=cwd)) + untracked = bool(subprocess_check_output(["git", "ls-files", "--others", "--exclude-standard"], cwd=cwd)) return GitStatus( commit=commit_hash, has_unstaged_changes=unstaged, has_staged_uncommitted_changes=staged, has_untracked_files=untracked ) diff --git a/src/serena/util/shell.py b/src/serena/util/shell.py index ce10cdbf..a8fad3b1 100644 --- a/src/serena/util/shell.py +++ b/src/serena/util/shell.py @@ -42,9 +42,11 @@ def execute_shell_command(command: str, cwd: str | None = None, capture_stderr: return ShellCommandResult(stdout=stdout, stderr=stderr, return_code=process.returncode, cwd=cwd) -def subprocess_check_output(args: list[str], encoding: str = "utf-8", strip: bool = True, timeout: float | None = None) -> str: +def subprocess_check_output( + args: list[str], encoding: str = "utf-8", strip: bool = True, timeout: float | None = None, cwd: str | None = None +) -> str: output = subprocess.check_output( - args, stdin=subprocess.DEVNULL, stderr=subprocess.PIPE, timeout=timeout, env=os.environ.copy(), **subprocess_kwargs() + args, stdin=subprocess.DEVNULL, stderr=subprocess.PIPE, timeout=timeout, env=os.environ.copy(), cwd=cwd, **subprocess_kwargs() ).decode(encoding) # type: ignore if strip: output = output.strip() From cda321e3184baf91a2b295591b539516a1dd3dfd Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 6 Apr 2026 16:01:35 +0200 Subject: [PATCH 054/190] Fix manual shutdown triggered by GUI tool/dashboard not cleaning everything up. Subprocesses were not cleaned up, in particular. * Rename SerenaAgent.shutdown to on_shutdown, because it is really a shutdown handler; it did not perform a shutdown. * Add new method shutdown which is now called from both sites (and which performs the full shutdown) * Manage the process instance used for the dashboard viewer and shut it down explicitly. * Initialise all variables used during shutdown immeditaly (avoiding errors when shutdown is triggered during init) --- CHANGELOG.md | 1 + scripts/demo_progressive_tool_shortening.py | 4 +- src/serena/agent.py | 46 +++++++++++++++------ src/serena/dashboard.py | 14 +------ src/serena/gui_log_viewer.py | 10 ++--- src/serena/mcp.py | 2 +- test/serena/config/test_serena_config.py | 12 +++--- test/serena/test_dashboard.py | 8 +--- test/serena/test_serena_agent.py | 2 +- 9 files changed, 51 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44e37a72..9601b47c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Status of the `main` branch. Changes prior to the next official version change w * Fixes: - Fix reactivation of the same project restarting language servers #1280 - Fix git commit id in version + - Fix manual shutdown triggered by GUI tool/dashboard not cleaning everything up. # 1.0.0 diff --git a/scripts/demo_progressive_tool_shortening.py b/scripts/demo_progressive_tool_shortening.py index f54d1946..3786021f 100644 --- a/scripts/demo_progressive_tool_shortening.py +++ b/scripts/demo_progressive_tool_shortening.py @@ -178,7 +178,7 @@ if __name__ == "__main__": run_lsp_tools(lsp_agent) run_backend_independent_tools(lsp_agent) finally: - lsp_agent.shutdown() + lsp_agent.on_shutdown() # JetBrains backend (requires a running IDE) try: @@ -186,6 +186,6 @@ if __name__ == "__main__": try: run_jb_tools(jb_agent) finally: - jb_agent.shutdown() + jb_agent.on_shutdown() except Exception as e: print(f"\nJetBrains backend not available, skipping: {e}") diff --git a/src/serena/agent.py b/src/serena/agent.py index 4461d682..31267135 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -6,6 +6,7 @@ import json import multiprocessing import os import platform +import signal import subprocess import sys from collections.abc import Callable, Iterator, Sequence @@ -269,6 +270,10 @@ class SerenaAgent: :param memory_log_handler: a MemoryLogHandler instance from which to read log messages; if None, a new one will be created if necessary. """ + self._active_project: Project | None = None + self._gui_log_viewer: Optional["GuiLogViewer"] = None + self._dashboard_viewer_process: multiprocessing.Process | None = None + self.version = serena_version() # obtain serena configuration using the decoupled factory function @@ -277,9 +282,6 @@ class SerenaAgent: # propagate configuration to other components self.serena_config.propagate_settings() - # project-specific instances, which will be initialized upon project activation - self._active_project: Project | None = None - # determine registered project to be activated (if any) registered_project_to_activate: RegisteredProject | None = ( self.serena_config.get_registered_project(project, autoregister=True) if project is not None else None @@ -302,7 +304,6 @@ class SerenaAgent: return memory_log_handler # open GUI log window if enabled - self._gui_log_viewer: Optional["GuiLogViewer"] = None if self.serena_config.gui_log_window: log.info("Opening GUI window") if platform.system() == "Darwin": @@ -312,7 +313,12 @@ class SerenaAgent: # which uv used as a base, unfortunately) from serena.gui_log_viewer import GuiLogViewer - self._gui_log_viewer = GuiLogViewer("dashboard", title="Serena Logs", memory_log_handler=get_memory_log_handler()) + self._gui_log_viewer = GuiLogViewer( + "dashboard", + title="Serena Logs", + memory_log_handler=get_memory_log_handler(), + shutdown_handler=lambda: self.shutdown(), + ) self._gui_log_viewer.start() else: log.debug("GUI window is disabled") @@ -579,8 +585,10 @@ class SerenaAgent: 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() + self._dashboard_viewer_process = multiprocessing.Process( + target=self._start_dashboard_viewer_process_function, args=(url, minimized), daemon=True + ) + self._dashboard_viewer_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: @@ -952,23 +960,35 @@ class SerenaAgent: ToolRegistry().print_tool_overview(self._active_tools.tools) def __del__(self) -> None: - self.shutdown() + self.on_shutdown() - def shutdown(self, timeout: float = 2.0) -> None: + def on_shutdown(self, timeout: float = 2.0) -> None: """ - Shuts down the agent, freeing resources and stopping background tasks. + Shutdown handler of the agent, freeing resources and stopping background tasks. """ - # guard against __del__ being called on a partially constructed instance - if not hasattr(self, "_active_project"): - return log.info("SerenaAgent is shutting down ...") if self._active_project is not None: + log.info(f"Shutting down active project '{self._active_project.project_name}' ...") self._active_project.shutdown(timeout=timeout) self._active_project = None if self._gui_log_viewer: log.info("Stopping the GUI log window ...") self._gui_log_viewer.stop() self._gui_log_viewer = None + if self._dashboard_viewer_process: + log.info("Stopping the dashboard viewer process ...") + self._dashboard_viewer_process.terminate() + self._dashboard_viewer_process = None + + def shutdown(self) -> None: + """ + Triggers a hard shutdown of the agent, freeing resources and signalling the process to terminate + """ + # perform clean-up right away, because kill does not result in normal deletion of the object + self.on_shutdown() + + # signal process termination + os.kill(os.getpid(), signal.SIGTERM) def get_tool_by_name(self, tool_name: str) -> Tool: tool_class = ToolRegistry().get_tool_class_by_name(tool_name) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 76a32b65..11a9739d 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -2,7 +2,6 @@ import os import socket import sys import threading -from collections.abc import Callable from pathlib import Path from typing import TYPE_CHECKING, Any, Self @@ -136,13 +135,11 @@ class SerenaDashboardAPI: memory_log_handler: MemoryLogHandler, tool_names: list[str], agent: "SerenaAgent", - shutdown_callback: Callable[[], None] | None = None, tool_usage_stats: ToolUsageStats | None = None, ) -> None: self._memory_log_handler = memory_log_handler self._tool_names = tool_names self._agent = agent - self._shutdown_callback = shutdown_callback self._app = Flask(__name__) self._tool_usage_stats = tool_usage_stats self._setup_routes() @@ -214,7 +211,7 @@ class SerenaDashboardAPI: @self._app.route("/shutdown", methods=["PUT"]) def shutdown() -> dict[str, str]: - self._shutdown() + self._agent.shutdown() return {"status": "shutting down"} @self._app.route("/get_available_languages", methods=["GET"]) @@ -541,15 +538,6 @@ class SerenaDashboardAPI: serena_version=self._agent.version, ) - def _shutdown(self) -> None: - log.info("Shutting down Serena") - if self._shutdown_callback: - self._shutdown_callback() - else: - # noinspection PyProtectedMember - # noinspection PyUnresolvedReferences - os._exit(0) - def _get_available_languages(self) -> ResponseAvailableLanguages: from solidlsp.ls_config import Language diff --git a/src/serena/gui_log_viewer.py b/src/serena/gui_log_viewer.py index 4ee16a86..57d3ac08 100644 --- a/src/serena/gui_log_viewer.py +++ b/src/serena/gui_log_viewer.py @@ -1,11 +1,11 @@ # mypy: ignore-errors import logging -import os import queue import sys import threading import tkinter as tk import traceback +from collections.abc import Callable from enum import Enum, auto from pathlib import Path from typing import Literal @@ -38,6 +38,7 @@ class GuiLogViewer: memory_log_handler: MemoryLogHandler | None = None, width=800, height=600, + shutdown_handler: Callable[[], None] | None = None, ): """ :param mode: the mode; if "dashboard", run a dashboard with logs and some control options; if "error", run @@ -57,6 +58,7 @@ class GuiLogViewer: self.log_thread = None self.menubar: tk.Menu | None = None self.tool_names = [] # List to store tool names for highlighting + self.shutdown_handler = shutdown_handler # Define colors for different log levels self.log_colors = { @@ -320,10 +322,8 @@ class GuiLogViewer: self.running = False def _shutdown_server(self) -> None: - log.info("Shutting down Serena") - # noinspection PyUnresolvedReferences - # noinspection PyProtectedMember - os._exit(0) + if self.shutdown_handler is not None: + self.shutdown_handler() class GuiLogViewerHandler(logging.Handler): diff --git a/src/serena/mcp.py b/src/serena/mcp.py index f9374030..75325668 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -346,7 +346,7 @@ class SerenaMCPFactory: finally: log.info("MCP server shutting down") if self.agent is not None: - self.agent.shutdown() + self.agent.on_shutdown() def _get_initial_instructions(self) -> str: assert self.agent is not None diff --git a/test/serena/config/test_serena_config.py b/test/serena/config/test_serena_config.py index b3632b5c..3f2e7ac3 100644 --- a/test/serena/config/test_serena_config.py +++ b/test/serena/config/test_serena_config.py @@ -236,7 +236,7 @@ class TestEffectiveLanguageBackend: try: assert agent.get_language_backend().is_lsp() finally: - agent.shutdown(timeout=5) + agent.on_shutdown(timeout=5) def test_project_overrides_global_backend(self): """When startup project has language_backend set, it overrides the global.""" @@ -247,7 +247,7 @@ class TestEffectiveLanguageBackend: try: assert agent.get_language_backend().is_jetbrains() finally: - agent.shutdown(timeout=5) + agent.on_shutdown(timeout=5) def test_no_project_uses_global_backend(self): """When no startup project is provided, effective backend is the global one.""" @@ -261,7 +261,7 @@ class TestEffectiveLanguageBackend: try: assert agent.get_language_backend() == LanguageBackend.LSP finally: - agent.shutdown(timeout=5) + agent.on_shutdown(timeout=5) def test_activate_project_rejects_backend_mismatch(self): """Post-init activation of a project with mismatched backend raises ValueError.""" @@ -285,7 +285,7 @@ class TestEffectiveLanguageBackend: with pytest.raises(ValueError, match="Cannot activate project"): agent.activate_project_from_path_or_name("jb_proj") finally: - agent.shutdown(timeout=5) + agent.on_shutdown(timeout=5) def test_activate_project_allows_matching_backend(self): """Post-init activation of a project with matching backend succeeds.""" @@ -308,7 +308,7 @@ class TestEffectiveLanguageBackend: # Should not raise agent.activate_project_from_path_or_name("lsp_proj2") finally: - agent.shutdown(timeout=5) + agent.on_shutdown(timeout=5) def test_activate_project_allows_none_backend(self): """Post-init activation of a project with no backend override succeeds.""" @@ -331,7 +331,7 @@ class TestEffectiveLanguageBackend: # Should not raise — None means "inherit session backend" agent.activate_project_from_path_or_name("proj2") finally: - agent.shutdown(timeout=5) + agent.on_shutdown(timeout=5) class TestGetConfiguredProjectSerenaFolder: diff --git a/test/serena/test_dashboard.py b/test/serena/test_dashboard.py index 8de6e8eb..37e68e01 100644 --- a/test/serena/test_dashboard.py +++ b/test/serena/test_dashboard.py @@ -29,13 +29,7 @@ def _make_dashboard(project_languages: list[Language] | None) -> SerenaDashboard if project_languages is not None: project = SimpleNamespace(project_config=SimpleNamespace(languages=project_languages)) agent = _DummyAgent(project) - return SerenaDashboardAPI( - memory_log_handler=_DummyMemoryLogHandler(), - tool_names=[], - agent=agent, - shutdown_callback=None, - tool_usage_stats=None, - ) + return SerenaDashboardAPI(memory_log_handler=_DummyMemoryLogHandler(), tool_names=[], agent=agent, tool_usage_stats=None) def test_available_languages_include_experimental_when_no_active_project(): diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 51a0227c..c2c1e61e 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -110,7 +110,7 @@ def serena_agent(request: pytest.FixtureRequest, serena_config) -> Iterator[Sere yield agent # explicitly shut down to free resources - agent.shutdown(timeout=5) + agent.on_shutdown(timeout=5) class TestSerenaAgent: From eb453976e2b06f42b9597b61e53cd7978a860291 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 6 Apr 2026 19:17:23 +0200 Subject: [PATCH 055/190] CLI: removed deprecated commands --- pyproject.toml | 2 -- scripts/mcp_server.py | 4 ++-- src/serena/cli.py | 23 ++++++++--------------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 680d8599..667214c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,8 +61,6 @@ explicit = true [project.scripts] serena = "serena.cli:top_level" -serena-mcp-server = "serena.cli:start_mcp_server" -index-project = "serena.cli:index_project" # deprecated [project.license] text = "MIT" diff --git a/scripts/mcp_server.py b/scripts/mcp_server.py index 74d2ae52..9b5c3f5a 100644 --- a/scripts/mcp_server.py +++ b/scripts/mcp_server.py @@ -1,4 +1,4 @@ -from serena.cli import start_mcp_server +from serena.cli import top_level if __name__ == "__main__": - start_mcp_server() + top_level.start_mcp_server() diff --git a/src/serena/cli.py b/src/serena/cli.py index 7ad3fb2d..666815ee 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -1109,23 +1109,16 @@ class PromptCommands(AutoRegisteringGroup): click.echo(f"Deleted override file '{prompt_yaml_name}'.") -# Expose groups so we can reference them in pyproject.toml -mode = ModeCommands() -context = ContextCommands() -project = ProjectCommands() -config = SerenaConfigCommands() -tools = ToolCommands() -prompts = PromptCommands() +_mode = ModeCommands() +_context = ContextCommands() +_project = ProjectCommands() +_config = SerenaConfigCommands() +_tools = ToolCommands() +_prompts = PromptCommands() -# Expose toplevel commands for the same reason +# Expose so we can use this as an entrypoint top_level = TopLevelCommands() -start_mcp_server = top_level.start_mcp_server # needed for the help script to work - register all subcommands to the top-level group -for subgroup in (mode, context, project, config, tools, prompts): +for subgroup in (_mode, _context, _project, _config, _tools, _prompts): top_level.add_command(subgroup) - - -def get_help() -> str: - """Retrieve the help text for the top-level Serena CLI.""" - return top_level.get_help(click.Context(top_level, info_name="serena")) From d525db7c012cccb87339e2fde3a99777dc5f0852 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 6 Apr 2026 19:32:14 +0200 Subject: [PATCH 056/190] Added init command to CLI --- src/serena/cli.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/serena/cli.py b/src/serena/cli.py index 666815ee..480821c9 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -45,7 +45,7 @@ from solidlsp.util.subprocess_util import subprocess_kwargs log = logging.getLogger(__name__) -_MAX_CONTENT_WIDTH = 100 +_MAX_CONTENT_WIDTH = 200 _MODES_EXPLANATION = f"""\b\nBuilt-in mode names or paths to custom mode YAMLs with which to override the default modes defined in the global Serena configuration or the active project. @@ -155,6 +155,28 @@ class TopLevelCommands(AutoRegisteringGroup): def __init__(self) -> None: super().__init__(name="serena", help="Serena CLI commands. You can run ` --help` for more info on each command.") + @staticmethod + @click.command( + "init", + help="Initialize Serena by creating a global config file with the specified default language backend.", + context_settings={"max_content_width": _MAX_CONTENT_WIDTH}, + ) + @click.option( + "--language-backend", + "-b", + type=click.Choice([b.value for b in LanguageBackend]), + default=LanguageBackend.LSP.value, + show_default=True, + help="Default code intelligence backend (can be overridden in the project config).", + ) + def init(language_backend: Literal["LSP", "JetBrains"] = "LSP"): + serena_config = SerenaConfig.from_config_file() + serena_config.language_backend = LanguageBackend(language_backend) + serena_config.save() + click.echo( + f"Initialized Serena with default backend: {language_backend}.\nFind the global Serena config in:\n {serena_config.config_file_path}" + ) + @staticmethod @click.command("start-mcp-server", help="Starts the Serena MCP server.", context_settings={"max_content_width": _MAX_CONTENT_WIDTH}) @click.option("--project", "project", type=PROJECT_TYPE, default=None, help="Path or name of project to activate at startup.") From 318d2f4204656ff1c489b95281515a65c07e6862 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 6 Apr 2026 20:20:18 +0200 Subject: [PATCH 057/190] Readme, docs: recommend init command instead of start-mcp-server --- CHANGELOG.md | 2 ++ README.md | 22 ++++++++++++---------- docs/02-usage/025_jetbrains_plugin.md | 14 ++++++-------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3db50aa..699c509a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* General: + - Added `serena init` command * Fixes: - Fix reactivation of the same project restarting language servers #1280 diff --git a/README.md b/README.md index be17cde6..d69520d5 100644 --- a/README.md +++ b/README.md @@ -178,24 +178,26 @@ https://github.com/user-attachments/assets/6eaa9aa1-610d-4723-a2d6-bf1e487ba753 **Prerequisites**. Serena is managed by *uv*. If you don’t already have it, you need to [install uv](https://docs.astral.sh/uv/getting-started/installation/) before proceeding. -> [!NOTE] -> When using the language server backend, some additional dependencies to be installed for certain languages; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details. - -**Starting the MCP Server**. The easiest way to start the Serena MCP server is by running the latest version from GitHub using uvx: +**Initialize Serena**. To initialize Serena, simply run: ```bash -uvx -p 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server +uvx -p 3.13 --from git+https://github.com/oraios/serena serena init ``` -If this is the first time you are starting Serena, the necessary configuration files will be created, and -you should see the Serena dashboard appear. You can terminate the server by pressing Ctrl+C (multiple times, if necessary). +This will create the global configuration file `~/.serena/config.yaml`. + +The default code intelligence backend for Serena is the free LSP backend. You can add `-b JetBrains` to the `init` command +to set up the JetBrains as the default code intelligence backend instead, see [JetBrains Plugin page](https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html) for more details. + + +> [!NOTE] +> When using the language server backend, some additional dependencies to be installed for certain languages; +> see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details. + **Configuring Your Client**. To connect Serena to your preferred MCP client, you typically need to [configure a launch command in your client](https://oraios.github.io/serena/02-usage/030_clients.html). Follow the link for specific instructions on how to set up Serena for Claude Code, Codex, Claude Desktop, MCP-enabled IDEs and other clients (such as local and web-based GUIs). -**Switching to the JetBrains Backend**. The default code intelligence backend for Serena is the free LSP backend. -For using the more powerful JetBrains backend, follow the instructions on the [JetBrains Plugin page](https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html). - > [!TIP] > While getting started quickly is easy, Serena is a powerful toolkit with many configuration options. > We highly recommend reading through the [user guide](https://oraios.github.io/serena/02-usage/000_intro.html) to get the most out of Serena. diff --git a/docs/02-usage/025_jetbrains_plugin.md b/docs/02-usage/025_jetbrains_plugin.md index 89007a6a..e7f425a6 100644 --- a/docs/02-usage/025_jetbrains_plugin.md +++ b/docs/02-usage/025_jetbrains_plugin.md @@ -65,24 +65,22 @@ After installing the plugin, you need to configure Serena to use it. **Central Configuration**. -If you ever executed Serena before, you will have the global Serena configuration file located at `~/.serena/serena_config.yml` -(`%USERPROFILE%\.serena\serena_config.yml` on Windows). - -If the configuration file does not exist, run +You can run ```shell -uvx -p 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server +uvx -p 3.13 --from git+https://github.com/oraios/serena serena init -b JetBrains ``` -(see [running Serena](020_running.md) for alternative options) to install all dependencies and create configuration files. Terminate the server with `Ctrl+C` after it has started. +to set the default code intelligence backend to JetBrains in the global Serena configuration file. -In your configuration, change the `language_backend` setting as follows: +Alternatively, you can also manually edit the configuration file `~/.serena/serena_config.yml` +(`%USERPROFILE%\.serena\serena_config.yml` on Windows) and set ```yaml language_backend: JetBrains ``` -*Note*: you can also use the button `Edit Global Serena Config` in the Serena MCP dashboard to open the config file in your default editor. +Note that the file might not exist yet if you never executed Serena before. **Per-Instance Configuration**. The configuration setting in the global config file can be overridden on a From d1652727feab061205d1a9f570a4566570fbe653 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 6 Apr 2026 20:29:12 +0200 Subject: [PATCH 058/190] Improve init command: Report version, add final success message Improve README: * Move LS deps back to prerequisites * Remove incorrect config path (redundant anyway, because user sees it upon init) * Some wording changes --- README.md | 21 +++++++++------------ src/serena/cli.py | 13 ++++++------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d69520d5..8fdd463d 100644 --- a/README.md +++ b/README.md @@ -176,24 +176,21 @@ https://github.com/user-attachments/assets/6eaa9aa1-610d-4723-a2d6-bf1e487ba753 ## Quick Start -**Prerequisites**. Serena is managed by *uv*. If you don’t already have it, you need to [install uv](https://docs.astral.sh/uv/getting-started/installation/) before proceeding. +**Prerequisites**. Serena is managed by *uv*, and [installing uv](https://docs.astral.sh/uv/getting-started/installation/) is the only required prerequisite for running Serena. -**Initialize Serena**. To initialize Serena, simply run: +> [!NOTE] +> When using the language server backend, some additional dependencies may need to be installed to support certain languages; +> see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details. + +**Initialise Serena**. To initialise Serena and verify that your setup works correctly, simply run: ```bash uvx -p 3.13 --from git+https://github.com/oraios/serena serena init ``` -This will create the global configuration file `~/.serena/config.yaml`. - -The default code intelligence backend for Serena is the free LSP backend. You can add `-b JetBrains` to the `init` command -to set up the JetBrains as the default code intelligence backend instead, see [JetBrains Plugin page](https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html) for more details. - - -> [!NOTE] -> When using the language server backend, some additional dependencies to be installed for certain languages; -> see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details. - +By default, this will set up Serena to use the language server backend. To use the JetBrains backend instead, add the parameters `-b JetBrains` +(see the [JetBrains Plugin documentation page](https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html) for additional usage details). +Either way, you should receive a success message indicating that Serena has been initialised successfully. **Configuring Your Client**. To connect Serena to your preferred MCP client, you typically need to [configure a launch command in your client](https://oraios.github.io/serena/02-usage/030_clients.html). Follow the link for specific instructions on how to set up Serena for Claude Code, Codex, Claude Desktop, MCP-enabled IDEs and other clients (such as local and web-based GUIs). diff --git a/src/serena/cli.py b/src/serena/cli.py index 480821c9..8e1b3093 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -17,6 +17,7 @@ from sensai.util.logging import FileLoggerContext, datetime_tag from sensai.util.string import dict_string from tqdm import tqdm +from serena import serena_version from serena.agent import SerenaAgent from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode from serena.config.serena_config import ( @@ -90,9 +91,6 @@ def find_project_root(root: str | Path | None = None) -> str | None: return None -# --------------------- Utilities ------------------------------------- - - def _open_in_editor(path: str) -> None: """Open the given file in the system's default editor or viewer.""" editor = os.environ.get("EDITOR") @@ -169,13 +167,14 @@ class TopLevelCommands(AutoRegisteringGroup): show_default=True, help="Default code intelligence backend (can be overridden in the project config).", ) - def init(language_backend: Literal["LSP", "JetBrains"] = "LSP"): + def init(language_backend: Literal["LSP", "JetBrains"] = "LSP") -> None: + click.echo(f"\nSerena version: {serena_version()}\n") serena_config = SerenaConfig.from_config_file() serena_config.language_backend = LanguageBackend(language_backend) serena_config.save() - click.echo( - f"Initialized Serena with default backend: {language_backend}.\nFind the global Serena config in:\n {serena_config.config_file_path}" - ) + click.echo(f"Configuration file: {serena_config.config_file_path}") + click.echo(f"Language backend: {language_backend}") + click.echo("\nSerena has been initialised successfully.\n") @staticmethod @click.command("start-mcp-server", help="Starts the Serena MCP server.", context_settings={"max_content_width": _MAX_CONTENT_WIDTH}) From cd1ce8dc600c78cec9b51c61811e3e5e3d211925 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 7 Apr 2026 01:54:03 +0200 Subject: [PATCH 059/190] Pin sphinx-book-theme to 1.1.4 The newer version 1.2.0 does not have the auto-expanding contents (in display on right) - at least not by default --- pyproject.toml | 1 + uv.lock | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 667214c7..0cc71524 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,7 @@ dev = [ # docs "sphinx==7.4.7", "sphinx_rtd_theme==2.0.0", + "sphinx_book_theme==1.1.4", "sphinx-toolbox==3.7.0", "jupyter-book==1.0.4.post1", "nbsphinx==0.9.7", diff --git a/uv.lock b/uv.lock index ec3f4bd7..b79d5624 100644 --- a/uv.lock +++ b/uv.lock @@ -2104,20 +2104,21 @@ wheels = [ [[package]] name = "pydata-sphinx-theme" -version = "0.16.1" +version = "0.15.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accessible-pygments" }, { name = "babel" }, { name = "beautifulsoup4" }, { name = "docutils" }, + { name = "packaging" }, { name = "pygments" }, { name = "sphinx" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693, upload-time = "2024-12-17T10:53:39.537Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/ea/3ab478cccacc2e8ef69892c42c44ae547bae089f356c4b47caf61730958d/pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d", size = 2400673, upload-time = "2024-06-25T19:28:45.041Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/0d/8ba33fa83a7dcde13eb3c1c2a0c1cc29950a048bfed6d9b0d8b6bd710b4c/pydata_sphinx_theme-0.16.1-py3-none-any.whl", hash = "sha256:225331e8ac4b32682c18fcac5a57a6f717c4e632cea5dd0e247b55155faeccde", size = 6723264, upload-time = "2024-12-17T10:53:35.645Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6", size = 4640157, upload-time = "2024-06-25T19:28:42.383Z" }, ] [[package]] @@ -2963,6 +2964,7 @@ dev = [ { name = "pytest-xdist" }, { name = "ruff" }, { name = "sphinx" }, + { name = "sphinx-book-theme" }, { name = "sphinx-rtd-theme" }, { name = "sphinx-toolbox" }, { name = "syrupy" }, @@ -3020,6 +3022,7 @@ requires-dist = [ { name = "ruff", marker = "extra == 'dev'", specifier = "==0.12.5" }, { name = "sensai-utils", specifier = "==1.5.0" }, { name = "sphinx", marker = "extra == 'dev'", specifier = "==7.4.7" }, + { name = "sphinx-book-theme", marker = "extra == 'dev'", specifier = "==1.1.4" }, { name = "sphinx-rtd-theme", marker = "extra == 'dev'", specifier = "==2.0.0" }, { name = "sphinx-toolbox", marker = "extra == 'dev'", specifier = "==3.7.0" }, { name = "sqlalchemy", marker = "extra == 'agno'", specifier = "==2.0.41" }, @@ -3142,15 +3145,15 @@ wheels = [ [[package]] name = "sphinx-book-theme" -version = "1.2.0" +version = "1.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydata-sphinx-theme" }, { name = "sphinx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/f7/154786f3cfb7692cd7acc24b6dfe4dcd1146b66f376b17df9e47125555e9/sphinx_book_theme-1.2.0.tar.gz", hash = "sha256:4a7ebfc7da4395309ac942ddfc38fbec5c5254c3be22195e99ad12586fbda9e3", size = 443962, upload-time = "2026-03-09T23:20:30.442Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/19/d002ed96bdc7738c15847c730e1e88282d738263deac705d5713b4d8fa94/sphinx_book_theme-1.1.4.tar.gz", hash = "sha256:73efe28af871d0a89bd05856d300e61edce0d5b2fbb7984e84454be0fedfe9ed", size = 439188, upload-time = "2025-02-20T16:32:32.581Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/bf/6f506a37c7f8ecc4576caf9486e303c7af249f6d70447bb51dde9d78cb99/sphinx_book_theme-1.2.0-py3-none-any.whl", hash = "sha256:709605d308e1991c5ef0cf19c481dbe9084b62852e317fafab74382a0ee7ccfa", size = 455936, upload-time = "2026-03-09T23:20:28.788Z" }, + { url = "https://files.pythonhosted.org/packages/51/9e/c41d68be04eef5b6202b468e0f90faf0c469f3a03353f2a218fd78279710/sphinx_book_theme-1.1.4-py3-none-any.whl", hash = "sha256:843b3f5c8684640f4a2d01abd298beb66452d1b2394cd9ef5be5ebd5640ea0e1", size = 433952, upload-time = "2025-02-20T16:32:31.009Z" }, ] [[package]] From e2696f9f54950d699d8efaf50cbf04ad68765eeb Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 7 Apr 2026 11:03:11 +0200 Subject: [PATCH 060/190] Added minimal AGENTS.md --- AGENTS.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..2b07df32 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +Relevant information about the project is in .serena/memories. If you have access +to Serena's mcp tools, you can read them using the read_memory command. Otherwise +you can just read them using normal file reading tools. From 4a692eed44e5cea825cd989f9a6f3a609fea747c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 7 Apr 2026 12:16:58 +0200 Subject: [PATCH 061/190] News: switched mechanism to fetch news from github This allows news to be updated without updating the installed version of Serena itself. --- .../dashboard/news => news}/20260111.html | 0 .../dashboard/news => news}/20260303.html | 0 .../dashboard/news => news}/20260321.html | 0 .../dashboard/news => news}/20260330.html | 0 .../dashboard/news => news}/20260404.html | 0 news/news.json | 7 ++ scripts/build_news_json.py | 44 ++++++++ src/serena/config/serena_config.py | 12 ++ src/serena/dashboard.py | 103 +++++++++++++++--- src/serena/resources/dashboard/dashboard.js | 79 +++++++------- 10 files changed, 186 insertions(+), 59 deletions(-) rename {src/serena/resources/dashboard/news => news}/20260111.html (100%) rename {src/serena/resources/dashboard/news => news}/20260303.html (100%) rename {src/serena/resources/dashboard/news => news}/20260321.html (100%) rename {src/serena/resources/dashboard/news => news}/20260330.html (100%) rename {src/serena/resources/dashboard/news => news}/20260404.html (100%) create mode 100644 news/news.json create mode 100644 scripts/build_news_json.py diff --git a/src/serena/resources/dashboard/news/20260111.html b/news/20260111.html similarity index 100% rename from src/serena/resources/dashboard/news/20260111.html rename to news/20260111.html diff --git a/src/serena/resources/dashboard/news/20260303.html b/news/20260303.html similarity index 100% rename from src/serena/resources/dashboard/news/20260303.html rename to news/20260303.html diff --git a/src/serena/resources/dashboard/news/20260321.html b/news/20260321.html similarity index 100% rename from src/serena/resources/dashboard/news/20260321.html rename to news/20260321.html diff --git a/src/serena/resources/dashboard/news/20260330.html b/news/20260330.html similarity index 100% rename from src/serena/resources/dashboard/news/20260330.html rename to news/20260330.html diff --git a/src/serena/resources/dashboard/news/20260404.html b/news/20260404.html similarity index 100% rename from src/serena/resources/dashboard/news/20260404.html rename to news/20260404.html diff --git a/news/news.json b/news/news.json new file mode 100644 index 00000000..11eb1d97 --- /dev/null +++ b/news/news.json @@ -0,0 +1,7 @@ +{ + "20260111": "
\n

Extended Symbol Information, Type Hierarchy and Compact Overviews

\n

January 11, 2026

\n

\n Recent commits and a new plugin release provide major new features!\n

\n
\n

\n For more detailed information, see our changelog.\n

\n
", + "20260303": "
\n

Nested and Global Memories

\n

March 03, 2026

\n

\n Serena's memory system has been significantly extended in functionality.\n It keeps the simplicity that made it popular but now supports structuring memories\n into topics and sharing them across projects.\n

\n

\n For more detailed information, see the documentation.\n

\n
", + "20260321": "
\n

Progressive Shortening and General Improvements in Tool Responses

\n

March 21, 2026

\n

\n Tools that may deliver too long results now output informative shorter\n messages instead of just stating \"result is too long\". The mechanism uses\n progressive shortening so that the contained information is optimized\n for the length limit.\n

\n

\n In addition, several tool responses have been optimized without losing\n content, making Serena even more token efficient in general.\n

\n
", + "20260330": "
\n

Querying of External Projects

\n

March 30, 2026

\n

\n We added a new tool for querying external projects that are\n known to Serena: From the current project, use Serena's tools on another project to retrieve information.\n This is useful in situations where the current project builds on or is otherwise related\n to other projects.\n

\n

\n Find out more about this feature in our documentation.\n

\n
", + "20260404": "
\n

Serena v1.0 Is Here!

\n

April 3, 2026

\n

\n After a full year of work since the first version of Serena, we are excited to announce the release of Serena v1.0!\n

\n

\n Since our last update, we have now added support for exciting new retrieval and refactoring tools, especially for users of the JetBrains backend.\n

\n
    \n
  • safe_delete, jet_brains_safe_delete (beta):
    Safely deletes a symbol, checking for remaining usages first
  • \n
  • jet_brains_move (beta):
    Moves a symbol, file or directory to a new location, updating all references
  • \n
  • jet_brains_find_declaration:
    Finds the declaration of a symbol using the JetBrains backend
  • \n
  • jet_brains_find_implementations:
    Finds the implementations of a symbol (e.g. abstract method)
  • \n
  • jet_brains_inline_symbol (beta):
    Inlines a symbol, replacing all call sites with the symbol’s body
  • \n
\n

\n We are far from done. Many new features will be coming your way – for both the LSP and JetBrains backends.\n

\n

\n Thank you to our contributors, users and customers!\n

\n
" +} \ No newline at end of file diff --git a/scripts/build_news_json.py b/scripts/build_news_json.py new file mode 100644 index 00000000..10a86b67 --- /dev/null +++ b/scripts/build_news_json.py @@ -0,0 +1,44 @@ +""" +Script to build a single JSON file from all individual news HTML files. + +Usage: + uv run python scripts/build_news_json.py + +This reads all .html files from the `news/` directory and creates +`news/news.json` containing a mapping of news IDs to HTML content strings. +""" + +import json +import sys +from pathlib import Path + +from serena.config.serena_config import SerenaPaths + + +def build_news_json() -> None: + news_dir = Path(SerenaPaths().news_dir) + + if not news_dir.exists(): + print(f"Error: News directory not found at {news_dir}", file=sys.stderr) + sys.exit(1) + + news_files = sorted(news_dir.glob("*.html")) + if not news_files: + print("Warning: No HTML news files found in news/", file=sys.stderr) + + news_data: dict[str, str] = {} + for news_file in news_files: + news_id = news_file.stem # e.g. "20260111" + html_content = news_file.read_text(encoding="utf-8").strip() + news_data[news_id] = html_content + print(f" Added news {news_id} ({len(html_content)} chars)") + + output_file = news_dir / "news.json" + with open(output_file, "w", encoding="utf-8") as f: + json.dump(news_data, f, ensure_ascii=False, indent=2) + + print(f"\nBuilt {output_file} with {len(news_data)} news entries.") + + +if __name__ == "__main__": + build_news_json() diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index ae5bbd1d..987d8cf6 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -87,6 +87,18 @@ class SerenaPaths: """ file containing the ID of the last read news snippet """ + self.news_etag_file: str = os.path.join(self.serena_user_home_dir, "news_etag.txt") + """ + file containing the ETag of the last fetched remote news JSON + """ + self.news_file: str = os.path.join(self.serena_user_home_dir, "news.json") + """ + local cache of the remote news JSON file + """ + self.news_dir: str = os.path.join(REPO_ROOT, "news") + """ + repository news directory containing the source HTML snippets and generated news.json + """ global_memories_path = Path(os.path.join(self.serena_user_home_dir, "memories", "global")) global_memories_path.mkdir(parents=True, exist_ok=True) self.global_memories_path = global_memories_path diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 11a9739d..dbd46504 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -1,7 +1,10 @@ +import json import os import socket import sys import threading +import urllib.error +import urllib.request from pathlib import Path from typing import TYPE_CHECKING, Any, Self @@ -142,7 +145,11 @@ class SerenaDashboardAPI: self._agent = agent self._app = Flask(__name__) self._tool_usage_stats = tool_usage_stats + self._loaded_news: dict[str, str] = {} + self._news_ready = threading.Event() self._setup_routes() + # Fetch remote news in background on startup (non-blocking) + threading.Thread(target=self._fetch_news, daemon=True).start() @property def memory_log_handler(self) -> MemoryLogHandler: @@ -346,12 +353,12 @@ class SerenaDashboardAPI: except Exception as e: return {"status": "error", "message": str(e)} - @self._app.route("/news_snippet_ids", methods=["GET"]) - def get_news_snippet_ids() -> dict[str, str | list[int]]: - def _get_unread_news_ids() -> list[int]: - all_news_files = (Path(SERENA_DASHBOARD_DIR) / "news").glob("*.html") - all_news_ids = [int(f.stem) for f in all_news_files] - """News ids are ints of format YYYYMMDD (publication dates)""" + @self._app.route("/fetch_unread_news", methods=["GET"]) + def fetch_unread_news() -> dict[str, dict[str, str] | str]: + def _fetch_unread_news() -> dict[str, str]: + """News ids are strings of format YYYYMMDD (publication dates)""" + self._news_ready.wait() + all_news = self._loaded_news # Filter news items by installation date serena_config_creation_date = SerenaConfig.get_config_file_creation_date() @@ -359,23 +366,23 @@ class SerenaDashboardAPI: # should not normally happen, since config file should exist when the dashboard is started # We assume a fresh installation in this case log.error("Serena config file not found when starting the dashboard") - return [] - serena_config_creation_date_int = int(serena_config_creation_date.strftime("%Y%m%d")) + return {} + serena_config_creation_date = serena_config_creation_date.strftime("%Y%m%d") # Only include news items published on or after the installation date - post_installation_news_ids = [news_id for news_id in all_news_ids if news_id >= serena_config_creation_date_int] + post_installation_news = {k: v for k, v in all_news.items() if k >= serena_config_creation_date} news_snippet_id_file = SerenaPaths().news_snippet_id_file if not os.path.exists(news_snippet_id_file): - return post_installation_news_ids + return post_installation_news with open(news_snippet_id_file, encoding="utf-8") as f: - last_read_news_id = int(f.read().strip()) - if last_read_news_id == 20262103: - last_read_news_id = 20260321 # fix originally misnamed file - return [news_id for news_id in post_installation_news_ids if news_id > last_read_news_id] + last_read_news_id = f.read().strip() + if last_read_news_id == "20262103": + last_read_news_id = "20260321" # fix originally misnamed news id + return {k: v for k, v in post_installation_news.items() if k > last_read_news_id} try: - unread_news_ids = _get_unread_news_ids() - return {"news_snippet_ids": unread_news_ids, "status": "success"} + unread_news = _fetch_unread_news() + return {"news": unread_news, "status": "success"} except Exception as e: return {"status": "error", "message": str(e)} @@ -383,10 +390,10 @@ class SerenaDashboardAPI: def mark_news_snippet_as_read() -> dict[str, str]: try: request_data = request.get_json() - news_snippet_id = int(request_data.get("news_snippet_id")) + news_snippet_id = str(request_data.get("news_snippet_id")) news_snippet_id_file = SerenaPaths().news_snippet_id_file with open(news_snippet_id_file, "w", encoding="utf-8") as f: - f.write(str(news_snippet_id)) + f.write(news_snippet_id) return {"status": "success", "message": f"Marked news snippet {news_snippet_id} as read"} except Exception as e: return {"status": "error", "message": str(e)} @@ -618,6 +625,66 @@ class SerenaDashboardAPI: self._agent.execute_task(run, logged=True, name="SaveSerenaConfig") + # ===== Remote News Methods ===== + + # The branch from which news are fetched. Change to a feature branch for testing. + _NEWS_JSON_URL = "https://raw.githubusercontent.com/oraios/serena/main/news/news.json" + + def _fetch_news(self) -> None: + """Fetch news.json from GitHub using ETag-based caching and store in memory. Silently ignores network errors.""" + paths = SerenaPaths() + + headers: dict[str, str] = {} + # Load stored ETag if available + if os.path.exists(paths.news_etag_file) and os.path.exists(paths.news_file): + try: + with open(paths.news_etag_file, encoding="utf-8") as f: + stored_etag = f.read().strip() + if stored_etag: + headers["If-None-Match"] = stored_etag + except Exception: + log.warning("Failed to read stored news ETag at %s, proceeding without it", paths.news_etag_file, exc_info=True) + + fetched_news_dict = None + try: + req = urllib.request.Request(self._NEWS_JSON_URL, headers=headers) + with urllib.request.urlopen(req, timeout=10) as response: + etag = response.headers.get("ETag", "") + body = response.read().decode("utf-8") + # Validate JSON + fetched_news_dict = json.loads(body) + # Store news content and ETag + with open(paths.news_file, "w", encoding="utf-8") as f: + f.write(body) + if etag: + with open(paths.news_etag_file, "w", encoding="utf-8") as f: + f.write(etag) + log.info("Remote news updated from %s", self._NEWS_JSON_URL) + except urllib.error.HTTPError as e: + if e.code == 304: + log.debug("Remote news unchanged (304 Not Modified)") + else: + log.warning("Failed to fetch remote news (HTTP %d): %s", e.code, e.reason) + except Exception as e: + log.warning("Failed to fetch remote news: %s", e) + if fetched_news_dict is None: + fetched_news_dict = self._load_previously_fetched_news_data() + self._loaded_news = fetched_news_dict + self._news_ready.set() + + @staticmethod + def _load_previously_fetched_news_data() -> dict[str, str]: + """Return the news data dict. Uses local cache if available, otherwise falls back to local news files.""" + paths = SerenaPaths() + + if os.path.exists(paths.news_file): + try: + with open(paths.news_file, encoding="utf-8") as f: + return json.loads(f.read()) + except Exception: + log.warning("Failed to read cached news data from %s", paths.news_file) + return {} + def _add_language(self, request_add_language: RequestAddLanguage) -> None: from solidlsp.ls_config import Language diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index dffd10ef..784d8238 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -2075,31 +2075,36 @@ class Dashboard { let self = this; console.log('Loading news...'); $.ajax({ - url: '/news_snippet_ids', + url: '/fetch_unread_news', type: 'GET', success: function(response) { - console.log('News snippet IDs response:', response); - if (response.status === 'success' && response.news_snippet_ids && response.news_snippet_ids.length > 0) { - console.log('Displaying news with IDs:', response.news_snippet_ids); - self.displayNews(response.news_snippet_ids); + console.log('Unread news response:', response); + if (response.status === 'success' && response.news && Object.keys(response.news).length > 0) { + const newsIds = Object.keys(response.news); + self.displayNews(newsIds, response.news); } else { console.log('No unread news, hiding section'); self.$newsSection.hide(); } }, error: function(xhr, status, error) { - console.error('Error loading news snippet IDs:', error); + console.error('Error loading news:', error); self.$newsSection.hide(); } }); } - displayNews(newsIds) { + /** + * Display news items given unread IDs and the full news data mapping. + * @param {number[]} newsIds - array of unread news IDs + * @param {Object} newsData - mapping of news ID strings to HTML content + */ + displayNews(newsIds, newsData) { let self = this; console.log('displayNews called with:', newsIds); // Sort newest first (descending order) newsIds.sort((a, b) => b - a); - + if (newsIds.length === 0) { console.log('No news items to display.'); self.$newsSection.hide(); @@ -2108,40 +2113,32 @@ class Dashboard { self.$newsSection.show(); self.$newsDisplay.empty(); console.log('Displaying ' + newsIds.length + ' news items.'); - // Load each news snippet HTML - let loadedCount = 0; - newsIds.forEach(function(newsId) { - $.ajax({ - url: '/dashboard/news/' + newsId + '.html', - type: 'GET', - success: function(html) { - // Wrap the HTML in a container with a button - let $newsContainer = $('
').attr('data-news-id', newsId); - let $newsContent = $(html); - - // Add button for marking as read - let $markRead = $('
'); - let $button = $('
'; // Active tools - collapsible From 2c4174f3bd705661335faad99eec1214e1a59c4f Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 7 Apr 2026 17:52:15 +0200 Subject: [PATCH 067/190] Add ClientSetupHandler and setup CLI command Add first implementation: ClaudeCode --- src/serena/cli.py | 27 ++++++++++++++++++ src/serena/config/client_setup.py | 46 +++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/serena/config/client_setup.py diff --git a/src/serena/cli.py b/src/serena/cli.py index 8e1b3093..90ea178b 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -19,6 +19,7 @@ from tqdm import tqdm from serena import serena_version from serena.agent import SerenaAgent +from serena.config.client_setup import client_setup_handlers from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode from serena.config.serena_config import ( LanguageBackend, @@ -176,6 +177,32 @@ class TopLevelCommands(AutoRegisteringGroup): click.echo(f"Language backend: {language_backend}") click.echo("\nSerena has been initialised successfully.\n") + @staticmethod + @click.command( + "setup", + help="Set up Serena for use with a specific client by registering it as an MCP server.", + context_settings={"max_content_width": _MAX_CONTENT_WIDTH}, + ) + @click.argument( + "client", + type=click.Choice([h.name for h in client_setup_handlers]), + ) + def setup(client: str) -> None: + # find the matching handler + handler = next(h for h in client_setup_handlers if h.name == client) + + # check applicability + if not handler.is_applicable(): + click.echo(f"\nCannot apply setup for client '{client}' (not found or not functional).\n") + raise SystemExit(1) + + # apply the setup + if handler.apply(): + click.echo(f"\nSerena has been successfully set up for {client}.\n") + else: + click.echo(f"\nFailed to set up Serena for {client}.\n") + raise SystemExit(1) + @staticmethod @click.command("start-mcp-server", help="Starts the Serena MCP server.", context_settings={"max_content_width": _MAX_CONTENT_WIDTH}) @click.option("--project", "project", type=PROJECT_TYPE, default=None, help="Path or name of project to activate at startup.") diff --git a/src/serena/config/client_setup.py b/src/serena/config/client_setup.py new file mode 100644 index 00000000..5d125257 --- /dev/null +++ b/src/serena/config/client_setup.py @@ -0,0 +1,46 @@ +from abc import ABC, abstractmethod + +from serena.util.shell import execute_shell_command + + +class ClientSetupHandler(ABC): + def __init__(self, name: str) -> None: + self.name = name + + @abstractmethod + def is_applicable(self) -> bool: + """ + :return: whether the client setup can applied (respective client is available) + """ + + @abstractmethod + def get_mcp_server_options(self) -> list[str]: + pass + + def get_mcp_server_command(self) -> str: + return f"serena start-mcp-server {' '.join(self.get_mcp_server_options())}" + + @abstractmethod + def apply(self) -> bool: + """ + Applies the client setup + """ + + +class ClientSetupHandlerClaudeCode(ClientSetupHandler): + def __init__(self) -> None: + super().__init__("ClaudeCode") + + def is_applicable(self) -> bool: + result = execute_shell_command("claude --version") + return result.return_code == 0 and "Claude" in result.stdout + + def get_mcp_server_options(self) -> list[str]: + return ["--context=claude-code", "--project-from-cwd"] + + def apply(self) -> bool: + result = execute_shell_command(f"claude mcp add --scope user serena -- {self.get_mcp_server_command()}") + return result.return_code == 0 + + +client_setup_handlers = [ClientSetupHandlerClaudeCode()] From 4f7cd8895018bc24c4ce1f75f615de64090132e4 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 7 Apr 2026 18:38:09 +0200 Subject: [PATCH 068/190] Add ClientSetupHandlerCodexCLI --- src/serena/config/client_setup.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/serena/config/client_setup.py b/src/serena/config/client_setup.py index 5d125257..c76f321e 100644 --- a/src/serena/config/client_setup.py +++ b/src/serena/config/client_setup.py @@ -43,4 +43,20 @@ class ClientSetupHandlerClaudeCode(ClientSetupHandler): return result.return_code == 0 -client_setup_handlers = [ClientSetupHandlerClaudeCode()] +class ClientSetupHandlerCodexCLI(ClientSetupHandler): + def __init__(self) -> None: + super().__init__("CodexCLI") + + def is_applicable(self) -> bool: + result = execute_shell_command("codex --version") + return result.return_code == 0 and "codex-cli" in result.stdout + + def get_mcp_server_options(self) -> list[str]: + return ["--context=codex", "--project-from-cwd"] + + def apply(self) -> bool: + result = execute_shell_command(f"codex mcp add serena -- {self.get_mcp_server_command()}") + return result.return_code == 0 + + +client_setup_handlers = [ClientSetupHandlerClaudeCode(), ClientSetupHandlerCodexCLI()] From 73ca8ef20f243977a1a104aa72ec79c5496fa029 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 7 Apr 2026 21:28:32 +0200 Subject: [PATCH 069/190] Report list of setup commands upon init, change setup handler names --- src/serena/cli.py | 13 +++++++++++++ src/serena/config/client_setup.py | 12 ++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/serena/cli.py b/src/serena/cli.py index 90ea178b..6e297cac 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -175,6 +175,19 @@ class TopLevelCommands(AutoRegisteringGroup): serena_config.save() click.echo(f"Configuration file: {serena_config.config_file_path}") click.echo(f"Language backend: {language_backend}") + + # check for auto-configurable clients + applicable_setup_handlers = [] + for setup_handler in client_setup_handlers: + if setup_handler.is_applicable(): + applicable_setup_handlers.append(setup_handler) + if len(applicable_setup_handlers) > 0: + click.echo( + "\nAuto-configurable clients detected.\nApply the following commands to configure the Serena MCP server (in a default configuration):" + ) + for setup_handler in applicable_setup_handlers: + click.echo(f" serena setup {setup_handler.name}") + click.echo("\nSerena has been initialised successfully.\n") @staticmethod diff --git a/src/serena/config/client_setup.py b/src/serena/config/client_setup.py index c76f321e..ba0fc23d 100644 --- a/src/serena/config/client_setup.py +++ b/src/serena/config/client_setup.py @@ -29,7 +29,7 @@ class ClientSetupHandler(ABC): class ClientSetupHandlerClaudeCode(ClientSetupHandler): def __init__(self) -> None: - super().__init__("ClaudeCode") + super().__init__("claude-code") def is_applicable(self) -> bool: result = execute_shell_command("claude --version") @@ -43,9 +43,13 @@ class ClientSetupHandlerClaudeCode(ClientSetupHandler): return result.return_code == 0 -class ClientSetupHandlerCodexCLI(ClientSetupHandler): +class ClientSetupHandlerCodex(ClientSetupHandler): + """ + Setup for Codex CLI and Codex App (shared config) + """ + def __init__(self) -> None: - super().__init__("CodexCLI") + super().__init__("codex") def is_applicable(self) -> bool: result = execute_shell_command("codex --version") @@ -59,4 +63,4 @@ class ClientSetupHandlerCodexCLI(ClientSetupHandler): return result.return_code == 0 -client_setup_handlers = [ClientSetupHandlerClaudeCode(), ClientSetupHandlerCodexCLI()] +client_setup_handlers = [ClientSetupHandlerClaudeCode(), ClientSetupHandlerCodex()] From 35268e87b7aa10ff77707c1e18e240fd83a1ec2c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 8 Apr 2026 09:57:11 +0200 Subject: [PATCH 070/190] Minor: added metadata (name, website_url) to FastMCP initialization [ci skip] --- src/serena/mcp.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 75325668..a8252846 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -332,7 +332,14 @@ class SerenaMCPFactory: # retain only FASTMCP_ prefix for already set environment variables. Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") instructions = self._get_initial_instructions() - mcp = FastMCP(lifespan=self.server_lifespan, host=host, port=port, instructions=instructions) + mcp = FastMCP( + name="Serena", + lifespan=self.server_lifespan, + website_url="https://oraios.github.io/serena", + host=host, + port=port, + instructions=instructions, + ) return mcp @asynccontextmanager From 23b81d4a223666da26c690b7b4221dae16c0a45e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 8 Apr 2026 13:16:09 +0200 Subject: [PATCH 071/190] Prompting: better descriptions for refactoring tools --- src/serena/tools/jetbrains_tools.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 00ae3a51..59ab3be9 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -126,9 +126,13 @@ class JetBrainsMoveTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, ToolMa target_parent_name_path: str | None = None, ) -> str: """ - Moves a symbol, file or directory to a different location. The target location is the new parent - of the symbol, i.e. the moved entity is never renamed by the operation, only moved. - References to affected symbols are automatically updated. + Moves a symbol, file or directory to a different location and automatically update all references to affected symbols. + **Important**: this tool should always be preferred to naive moving (e.g. via file system operations or edits) + as it is much more reliable and efficient. It is always safe to use this tool. For some symbols, moving may not be applicable, + and will result in no edits and a suitable error message. + The target location is the new parent of the symbol, + i.e. the moved entity is never renamed by the operation, only moved. + Valid moves: - Symbol: @@ -168,12 +172,16 @@ class JetBrainsSafeDeleteTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, propagate: bool = False, ) -> str: """ - Safely deletes a symbol, checking for usages first. It is also - possible to request deleting of usages and cleaning up of unused code. + Safely deletes a symbol, file, or directory, checking for usages first and propagating deletion, if desired. + Propagation means it is possible to request deleting of usages and cleaning up of unused code. + Propagation is powerful for cleaning up code but should be used with care, and only when you are sure that + **Important**: this tool should always be preferred to naive deleting (e.g. via file system operations or edits). + When using it, you don't have to search for usages first, as the tool will do it for you. :param relative_path: the relative path to the file containing the symbol to delete. :param name_path: the name path of the symbol to delete. A name path identifies a symbol within a source file, e.g. "MyClass/my_method". + Omit for deleting a file or directory. :param delete_even_if_used: whether to force deletion even if the symbol still has usages. Default is False (safe mode: will report usages instead of deleting). :param propagate: whether to propagate the deletion to usages of the symbol and also @@ -204,6 +212,8 @@ class JetBrainsInlineSymbol(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, To Inlines a symbol (usually a method/function, but also classes may be amenable to inlining, which turns invocation into anonymous class creation), replacing all call sites with the symbol's body. + **Important**: this tool should always be preferred to naive inlining (e.g. via searching for references and + editing them). :param name_path: the name path of the symbol to inline. :param relative_path: the relative path to the file containing the symbol to inline. From 19d1459f68e93f2d562f89b536b08af6dde9994f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 8 Apr 2026 13:16:33 +0200 Subject: [PATCH 072/190] Prompting: more focus on refactoring tools in system prompt --- src/serena/resources/config/modes/editing.yml | 23 +++++++++++-------- .../config/prompt_templates/system_prompt.yml | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/serena/resources/config/modes/editing.yml b/src/serena/resources/config/modes/editing.yml index 4de188b2..57d800b5 100644 --- a/src/serena/resources/config/modes/editing.yml +++ b/src/serena/resources/config/modes/editing.yml @@ -1,15 +1,18 @@ description: All tools, with detailed instructions for code editing -prompt: | - You are operating in editing mode. You can edit files with the provided tools. - You adhere to the project's code style and patterns. - +prompt: | Use symbolic editing tools whenever possible for precise code modifications. - If no explicit editing task has yet been provided, wait for the user to provide one. Do not be overly eager. - - When writing new code, think about where it belongs best. Don't generate new files if you don't plan on actually - properly integrating them into the codebase. - - You have two main approaches for editing code: (a) editing at the symbol level and (b) file-based editing. + + {% if 'jet_brains_move' in available_tools -%} + **Refactoring tools** + You also have access to Serena's refactoring tools (move, inline, safe_delete) + which should ALWAYS be preferred over naive and primitive approaches when applicable. + When you need to move, inline or delete a symbol, you should always try using the corresponding tool first. + {%- endif %} + + {% if 'jet_brains_move' in available_tools -%} + Apart from refactoring tools + {%- endif %} + you have two main approaches for editing code: (a) editing at the symbol level and (b) file-based editing. The symbol-based approach is appropriate if you need to adjust an entire symbol, e.g. a method, a class, a function, etc. It is not appropriate if you need to adjust just a few lines of code within a larger symbol. diff --git a/src/serena/resources/config/prompt_templates/system_prompt.yml b/src/serena/resources/config/prompt_templates/system_prompt.yml index de648821..debefcd2 100644 --- a/src/serena/resources/config/prompt_templates/system_prompt.yml +++ b/src/serena/resources/config/prompt_templates/system_prompt.yml @@ -33,7 +33,7 @@ prompts: to read the desired methods with `include_body=True`. You can understand relationships between symbols by using the `find_referencing_symbols` tool. {% endif %} - + {% if 'read_memory' in available_tools -%} You generally have access to memories and it may be useful for you to read them. You infer whether memories are relevant based on their names. From fe2e2ce0ccb1c0af6dab50b901b13a507ecc498c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 8 Apr 2026 13:17:09 +0200 Subject: [PATCH 073/190] Stripped CLAUDE.md of details --- CLAUDE.md | 117 ++---------------------------------------------------- 1 file changed, 3 insertions(+), 114 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 53ddc373..2b07df32 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,114 +1,3 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Development Commands - -**Essential Commands (use these exact commands):** -- `uv run poe format` - Format code (RUFF) - ONLY allowed formatting command -- `uv run poe type-check` - Run mypy type checking - ONLY allowed type checking command -- `uv run poe test` - Run tests with default markers (excludes java/rust by default) -- `uv run poe test -m "python or go"` - Run specific language tests -- `uv run poe test -m vue` - Run Vue tests -- `uv run poe lint` - Check code style without fixing - -**Test Markers:** -Available pytest markers for selective testing: -- `python`, `go`, `java`, `rust`, `typescript`, `vue`, `php`, `perl`, `powershell`, `csharp`, `elixir`, `terraform`, `clojure`, `swift`, `bash`, `ruby`, `ruby_solargraph` -- `snapshot` - for symbolic editing operation tests - -**Project Management:** -- `uv run serena-mcp-server` - Start MCP server from project root -- `uv run index-project` - Index project for faster tool performance - -**Always run format, type-check, and test before completing any task.** - -## Architecture Overview - -Serena is a dual-layer coding agent toolkit: - -### Core Components - -**1. SerenaAgent (`src/serena/agent.py`)** -- Central orchestrator managing projects, tools, and user interactions -- Coordinates language servers, memory persistence, and MCP server interface -- Manages tool registry and context/mode configurations - -**2. SolidLanguageServer (`src/solidlsp/ls.py`)** -- Unified wrapper around Language Server Protocol (LSP) implementations -- Provides language-agnostic interface for symbol operations -- Handles caching, error recovery, and multiple language server lifecycle - -**3. Tool System (`src/serena/tools/`)** -- **file_tools.py** - File system operations, search, regex replacements -- **symbol_tools.py** - Language-aware symbol finding, navigation, editing -- **memory_tools.py** - Project knowledge persistence and retrieval -- **config_tools.py** - Project activation, mode switching -- **workflow_tools.py** - Onboarding and meta-operations - -**4. Configuration System (`src/serena/config/`)** -- **Contexts** - Define tool sets for different environments (desktop-app, agent, ide-assistant) -- **Modes** - Operational patterns (planning, editing, interactive, one-shot) -- **Projects** - Per-project settings and language server configs - -### Language Support Architecture - -Each supported language has: -1. **Language Server Implementation** in `src/solidlsp/language_servers/` -2. **Runtime Dependencies** - Automatic language server downloads when needed -3. **Test Repository** in `test/resources/repos//` -4. **Test Suite** in `test/solidlsp//` - -### Memory & Knowledge System - -- **Markdown-based storage** in `.serena/memories/` directories -- **Project-specific knowledge** persistence across sessions -- **Contextual retrieval** based on relevance -- **Onboarding support** for new projects - -## Development Patterns - -### Adding New Languages -1. Create language server class in `src/solidlsp/language_servers/` -2. Add to Language enum in `src/solidlsp/ls_config.py` -3. Update factory method in `src/solidlsp/ls.py` -4. Create test repository in `test/resources/repos//` -5. Write test suite in `test/solidlsp//` -6. Add pytest marker to `pyproject.toml` - -### Adding New Tools -1. Inherit from `Tool` base class in `src/serena/tools/tools_base.py` -2. Implement required methods and parameter validation -3. Register in appropriate tool registry -4. Add to context/mode configurations - -### Testing Strategy -- Language-specific tests use pytest markers -- Symbolic editing operations have snapshot tests -- Integration tests in `test_serena_agent.py` -- Test repositories provide realistic symbol structures - -## Configuration Hierarchy - -Configuration is loaded from (in order of precedence): -1. Command-line arguments to `serena-mcp-server` -2. Project-specific `.serena/project.yml` -3. User config `~/.serena/serena_config.yml` -4. Active modes and contexts - -## Key Implementation Notes - -- **Symbol-based editing** - Uses LSP for precise code manipulation -- **Caching strategy** - Reduces language server overhead -- **Error recovery** - Automatic language server restart on crashes -- **Multi-language support** - 19 languages with LSP integration (including Vue) -- **MCP protocol** - Exposes tools to AI agents via Model Context Protocol -- **Async operation** - Non-blocking language server interactions - -## Working with the Codebase - -- Project uses Python 3.11 with `uv` for dependency management -- Strict typing with mypy, formatted with ruff -- Language servers run as separate processes with LSP communication -- Memory system enables persistent project knowledge -- Context/mode system allows workflow customization \ No newline at end of file +Relevant information about the project is in .serena/memories. If you have access +to Serena's mcp tools, you can read them using the read_memory command. Otherwise +you can just read them using normal file reading tools. From c1aedec189a4132cc83e124d06d1858b98764343 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 8 Apr 2026 13:39:05 +0200 Subject: [PATCH 074/190] Deleted obsolete tools. Essentially backwards compatible Enhanced tool registry check to log a warning instead of crashing when a deleted tool name is passed --- .serena/project.yml | 48 ++++++------- docs/02-usage/050_configuration.md | 4 -- src/serena/agent.py | 12 ++-- .../resources/config/contexts/chatgpt.yml | 3 +- .../resources/config/contexts/claude-code.yml | 1 - .../resources/config/contexts/codex.yml | 1 - .../resources/config/contexts/desktop-app.yml | 3 +- src/serena/resources/config/contexts/ide.yml | 1 - .../prompt_templates/simple_tool_outputs.yml | 39 ----------- src/serena/resources/project.template.yml | 47 +++++++------ src/serena/tools/config_tools.py | 21 ------ src/serena/tools/tools_base.py | 21 ++++++ src/serena/tools/workflow_tools.py | 70 +------------------ 13 files changed, 76 insertions(+), 195 deletions(-) diff --git a/.serena/project.yml b/.serena/project.yml index 0170423a..84b6fcf8 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -48,41 +48,41 @@ read_only: false # Below is the complete list of tools for convenience. # To make sure you have the latest list of tools, and to view their descriptions, # execute `uv run scripts/print_tool_overview.py`. -# -# * `activate_project`: Activates a project by name. +# * `activate_project`: Activates a project based on the project name or path. # * `check_onboarding_performed`: Checks whether project onboarding was already performed. # * `create_text_file`: Creates/overwrites a file in the project directory. -# * `delete_lines`: Deletes a range of lines within a file. -# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly, +# for example by saying that the information retrieved from a memory file is no longer correct +# or no longer relevant for the project. +# * `edit_memory`: Replaces content matching a regular expression in a memory. # * `execute_shell_command`: Executes a shell command. -# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. -# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). -# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `find_file`: Finds files in the given relative paths +# * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend +# * `find_symbol`: Performs a global (or local) search using the language server backend. # * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. # * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. -# * `initial_instructions`: Gets the initial instructions for the current project. -# Should only be used in settings where the system prompt cannot be set, -# e.g. in clients you have no control over, like Claude Desktop. +# * `initial_instructions`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual') +# for clients that do not read the initial instructions when the MCP server is connected. # * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. -# * `insert_at_line`: Inserts content at a given line in a file. # * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. # * `list_dir`: Lists files and directories in the given directory (optionally with recursion). -# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `list_memories`: List available memories. Any memory can be read using the `read_memory` tool. # * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). -# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). # * `read_file`: Reads a file within the project directory. -# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. -# * `remove_project`: Removes a project from the Serena configuration. -# * `replace_lines`: Replaces a range of lines within a file with new content. -# * `replace_symbol_body`: Replaces the full definition of a symbol. -# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `read_memory`: Read the content of a memory file. This tool should only be used if the information +# is relevant to the current task. You can infer whether the information +# is relevant from the memory file name. +# You should not read the same memory file multiple times in the same conversation. +# * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported +# (e.g., renaming "global/foo" to "bar" moves it from global to project scope). +# * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities. +# For JB, we use a separate tool. +# * `replace_content`: Replaces content in a file (optionally using regular expressions). +# * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend. +# * `safe_delete_symbol`: # * `search_for_pattern`: Performs a search for a pattern in the project. -# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. -# * `switch_modes`: Activates modes by providing a list of their names -# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. -# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. -# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. -# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +# * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format. +# The memory name should be meaningful. excluded_tools: [] # list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default). diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 5f5f3ad3..798daa3c 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -145,10 +145,6 @@ If you want to keep certain modes as always active, regardless of command-line p define them as *base modes* in the global or project configuration. ::: -Modes can also be _switched dynamically_ during a session. -You can instruct the LLM to use the `switch_modes` tool to activate a different set of modes (e.g., "Switch to planning and one-shot modes"). -Like command-line parameters, this only affects default modes, not base modes (which remain active). - :::{note} **Mode Compatibility**: While you can combine modes, some may be semantically incompatible (e.g., `interactive` and `one-shot`). Serena currently does not prevent incompatible combinations; it is up to the user to choose sensible mode configurations. diff --git a/src/serena/agent.py b/src/serena/agent.py index 31267135..cd616343 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -146,24 +146,20 @@ class ToolSet: tool_names = set() for fixed_tool in definition.fixed_tools: fixed_tool = get_updated_tool_name(fixed_tool) - if not registry.is_valid_tool_name(fixed_tool): - raise ValueError(f"Invalid tool name '{fixed_tool}' provided for fixed tool set") - tool_names.add(fixed_tool) + if registry.check_valid_tool_name(fixed_tool, " (in fixed tools set)"): + tool_names.add(fixed_tool) log.info(f"{definition} defined a fixed tool set with {len(tool_names)} tools: {', '.join(tool_names)}") else: included_tools = [] excluded_tools = [] for included_tool in definition.included_optional_tools: included_tool = get_updated_tool_name(included_tool) - if not registry.is_valid_tool_name(included_tool): - raise ValueError(f"Invalid tool name '{included_tool}' provided for inclusion") - if included_tool not in tool_names: + if registry.check_valid_tool_name(included_tool, " (in included optional tools)") and included_tool not in tool_names: tool_names.add(included_tool) included_tools.append(included_tool) for excluded_tool in definition.excluded_tools: excluded_tool = get_updated_tool_name(excluded_tool) - if not registry.is_valid_tool_name(excluded_tool): - raise ValueError(f"Invalid tool name '{excluded_tool}' provided for exclusion") + registry.check_valid_tool_name(excluded_tool, " (in excluded tools)") if excluded_tool in tool_names: tool_names.remove(excluded_tool) excluded_tools.append(excluded_tool) diff --git a/src/serena/resources/config/contexts/chatgpt.yml b/src/serena/resources/config/contexts/chatgpt.yml index ae1c3db6..494e9c42 100644 --- a/src/serena/resources/config/contexts/chatgpt.yml +++ b/src/serena/resources/config/contexts/chatgpt.yml @@ -11,8 +11,7 @@ prompt: | a diagram in addition to your text-based communication. Note that in the chat interface you have various rendering options for text, html, and mermaid diagrams, as has been explained to you in your initial instructions. excluded_tools: [] -included_optional_tools: - - switch_modes +included_optional_tools: [] tool_description_overrides: find_symbol: | diff --git a/src/serena/resources/config/contexts/claude-code.yml b/src/serena/resources/config/contexts/claude-code.yml index 118ed8b4..c7da899e 100644 --- a/src/serena/resources/config/contexts/claude-code.yml +++ b/src/serena/resources/config/contexts/claude-code.yml @@ -12,7 +12,6 @@ excluded_tools: - create_text_file - read_file - execute_shell_command - - prepare_for_new_conversation - replace_content tool_description_overrides: {} diff --git a/src/serena/resources/config/contexts/codex.yml b/src/serena/resources/config/contexts/codex.yml index a492e8f9..ccad93b5 100644 --- a/src/serena/resources/config/contexts/codex.yml +++ b/src/serena/resources/config/contexts/codex.yml @@ -13,7 +13,6 @@ excluded_tools: - create_text_file - read_file - execute_shell_command - - prepare_for_new_conversation - replace_content diff --git a/src/serena/resources/config/contexts/desktop-app.yml b/src/serena/resources/config/contexts/desktop-app.yml index 8d49cec5..aad51ac8 100644 --- a/src/serena/resources/config/contexts/desktop-app.yml +++ b/src/serena/resources/config/contexts/desktop-app.yml @@ -9,7 +9,6 @@ prompt: | To illustrate complex relationships, consider creating diagrams in addition to your text-based communication (depending on the options for text, html, mermaid diagrams, etc. that you are provided with in your initial instructions). excluded_tools: [] -included_optional_tools: - - switch_modes +included_optional_tools: [] tool_description_overrides: {} diff --git a/src/serena/resources/config/contexts/ide.yml b/src/serena/resources/config/contexts/ide.yml index 4a917a56..e8806f4e 100644 --- a/src/serena/resources/config/contexts/ide.yml +++ b/src/serena/resources/config/contexts/ide.yml @@ -12,7 +12,6 @@ excluded_tools: - create_text_file - read_file - execute_shell_command - - prepare_for_new_conversation tool_description_overrides: {} diff --git a/src/serena/resources/config/prompt_templates/simple_tool_outputs.yml b/src/serena/resources/config/prompt_templates/simple_tool_outputs.yml index 41291165..c6f2c619 100644 --- a/src/serena/resources/config/prompt_templates/simple_tool_outputs.yml +++ b/src/serena/resources/config/prompt_templates/simple_tool_outputs.yml @@ -34,42 +34,3 @@ prompts: Moreover, you should create memory files for the style and conventions and a dedicated memory file for what should be done when a task is completed. **Important**: after done with the onboarding task, remember to call the `write_memory` to save the collected information! - - think_about_collected_information: | - Have you collected all the information you need for solving the current task? If not, can the missing information be acquired by using the available tools, - in particular the tools related to symbol discovery? Or do you need to ask the user for more information? - Think about it step by step and give a summary of the missing information and how it could be acquired. - - think_about_task_adherence: | - Are you deviating from the task at hand? Do you need any additional information to proceed? - Have you loaded all relevant memory files to see whether your implementation is fully aligned with the - code style, conventions, and guidelines of the project? If not, adjust your implementation accordingly - before modifying any code into the codebase. - Note that it is better to stop and ask the user for clarification - than to perform large changes which might not be aligned with the user's intentions. - If you feel like the conversation is deviating too much from the original task, apologize and suggest to the user - how to proceed. If the conversation became too long, create a summary of the current progress and suggest to the user - to start a new conversation based on that summary. - - think_about_whether_you_are_done: | - Have you already performed all the steps required by the task? Is it appropriate to run tests and linting, and if so, - have you done that already? Is it appropriate to adjust non-code files like documentation and config and have you done that already? - Should new tests be written to cover the changes? - Note that a task that is just about exploring the codebase does not require running tests or linting. - Read the corresponding memory files to see what should be done when a task is completed. - - summarize_changes: | - Summarize all the changes you have made to the codebase over the course of the conversation. - Explore the diff if needed (e.g. by using `git diff`) to ensure that you have not missed anything. - Explain whether and how the changes are covered by tests. Explain how to best use the new code, how to understand it, - which existing code it affects and interacts with. Are there any dangers (like potential breaking changes or potential new problems) - that the user should be aware of? Should any new documentation be written or existing documentation updated? - You can use tools to explore the codebase prior to writing the summary, but don't write any new code in this step until - the summary is complete. - - prepare_for_new_conversation: | - You have not yet completed the current task but we are running out of context. - {mode_prepare_for_new_conversation} - Imagine that you are handing over the task to another person who has access to the - same tools and memory files as you do, but has not been part of the conversation so far. - Write a summary that can be used in the next conversation to a memory file using the `write_memory` tool. diff --git a/src/serena/resources/project.template.yml b/src/serena/resources/project.template.yml index f6ba8cb1..9a2c92e5 100644 --- a/src/serena/resources/project.template.yml +++ b/src/serena/resources/project.template.yml @@ -69,40 +69,41 @@ read_only: false # To make sure you have the latest list of tools, and to view their descriptions, # execute `uv run scripts/print_tool_overview.py`. # -# * `activate_project`: Activates a project by name. +# * `activate_project`: Activates a project based on the project name or path. # * `check_onboarding_performed`: Checks whether project onboarding was already performed. # * `create_text_file`: Creates/overwrites a file in the project directory. -# * `delete_lines`: Deletes a range of lines within a file. -# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly, +# for example by saying that the information retrieved from a memory file is no longer correct +# or no longer relevant for the project. +# * `edit_memory`: Replaces content matching a regular expression in a memory. # * `execute_shell_command`: Executes a shell command. -# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. -# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). -# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `find_file`: Finds files in the given relative paths +# * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend +# * `find_symbol`: Performs a global (or local) search using the language server backend. # * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. # * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. -# * `initial_instructions`: Gets the initial instructions for the current project. -# Should only be used in settings where the system prompt cannot be set, -# e.g. in clients you have no control over, like Claude Desktop. +# * `initial_instructions`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual') +# for clients that do not read the initial instructions when the MCP server is connected. # * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. -# * `insert_at_line`: Inserts content at a given line in a file. # * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. # * `list_dir`: Lists files and directories in the given directory (optionally with recursion). -# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `list_memories`: List available memories. Any memory can be read using the `read_memory` tool. # * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). -# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). # * `read_file`: Reads a file within the project directory. -# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. -# * `remove_project`: Removes a project from the Serena configuration. -# * `replace_lines`: Replaces a range of lines within a file with new content. -# * `replace_symbol_body`: Replaces the full definition of a symbol. -# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `read_memory`: Read the content of a memory file. This tool should only be used if the information +# is relevant to the current task. You can infer whether the information +# is relevant from the memory file name. +# You should not read the same memory file multiple times in the same conversation. +# * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported +# (e.g., renaming "global/foo" to "bar" moves it from global to project scope). +# * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities. +# For JB, we use a separate tool. +# * `replace_content`: Replaces content in a file (optionally using regular expressions). +# * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend. +# * `safe_delete_symbol`: # * `search_for_pattern`: Performs a search for a pattern in the project. -# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. -# * `switch_modes`: Activates modes by providing a list of their names -# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. -# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. -# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. -# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +# * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format. +# The memory name should be meaningful. excluded_tools: [] # list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default). diff --git a/src/serena/tools/config_tools.py b/src/serena/tools/config_tools.py index 0e92924a..17ffe74b 100644 --- a/src/serena/tools/config_tools.py +++ b/src/serena/tools/config_tools.py @@ -51,27 +51,6 @@ class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerO return f"Successfully removed project '{project_name}' from configuration." -class SwitchModesTool(Tool, ToolMarkerOptional): - """ - Activates modes by providing a list of their names - """ - - def apply(self, modes: list[str]) -> str: - """ - Activates the desired modes, like ["editing", "interactive"] or ["planning", "one-shot"] - - :param modes: the names of the modes to activate - """ - self.agent.set_modes(modes) - - # Inform the Agent about the activated modes and the currently active tools - mode_instances = self.agent.get_active_modes() - result_str = f"Active modes: {', '.join([mode.name for mode in mode_instances])}" + "\n" - result_str += "\n".join([mode_instance.prompt for mode_instance in mode_instances]) + "\n" - result_str += f"Currently active tools: {', '.join(self.agent.get_active_tool_names())}" - return result_str - - class GetCurrentConfigTool(Tool): """ Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index cb2b299d..bb474349 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -429,6 +429,15 @@ tool_packages = ["serena.tools"] @singleton class ToolRegistry: + _deleted_tools: list[str] = [ + "think_about_collected_information", + "think_about_collected_information", + "prepare_for_new_conversation", + "summarize_changes", + "think_about_whether_you_are_done", + "switch_modes", + ] + def __init__(self) -> None: self._tool_dict: dict[str, RegisteredTool] = {} inclusion_predicate = lambda c: "apply" in c.__dict__ # include only concrete tool classes that implement apply @@ -518,3 +527,15 @@ class ToolRegistry: def is_valid_tool_name(self, tool_name: str) -> bool: return tool_name in self._tool_dict + + def check_valid_tool_name(self, tool_name: str, caller_context_for_logging: str = "") -> bool: + """Returns True if the tool name is valid, False if it is deleted, and raises ValueError if it is invalid.""" + if self.is_deleted_tool_name(tool_name): + log.warning(f"Tool name is deleted: {tool_name}{caller_context_for_logging}") + return False + if not self.is_valid_tool_name(tool_name): + raise ValueError(f"Invalid tool name: {tool_name}{caller_context_for_logging}") + return True + + def is_deleted_tool_name(self, tool_name: str) -> bool: + return tool_name in self._deleted_tools diff --git a/src/serena/tools/workflow_tools.py b/src/serena/tools/workflow_tools.py index 343f26e2..e8bc67b4 100644 --- a/src/serena/tools/workflow_tools.py +++ b/src/serena/tools/workflow_tools.py @@ -4,7 +4,7 @@ Tools supporting the general workflow of the agent import platform -from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional +from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject class CheckOnboardingPerformedTool(Tool): @@ -50,74 +50,6 @@ class OnboardingTool(Tool): return self.prompt_factory.create_onboarding_prompt(system=system) -class ThinkAboutCollectedInformationTool(Tool, ToolMarkerOptional): - """ - Thinking tool for pondering the completeness of collected information. - """ - - def apply(self) -> str: - """ - Think about the collected information and whether it is sufficient and relevant. - This tool should ALWAYS be called after you have completed a non-trivial sequence of searching steps like - find_symbol, find_referencing_symbols, search_files_for_pattern, read_file, etc. - """ - return self.prompt_factory.create_think_about_collected_information() - - -class ThinkAboutTaskAdherenceTool(Tool, ToolMarkerOptional): - """ - Thinking tool for determining whether the agent is still on track with the current task. - """ - - def apply(self) -> str: - """ - Think about the task at hand and whether you are still on track. - Especially important if the conversation has been going on for a while and there - has been a lot of back and forth. - - This tool should ALWAYS be called before you insert, replace, or delete code. - """ - return self.prompt_factory.create_think_about_task_adherence() - - -class ThinkAboutWhetherYouAreDoneTool(Tool, ToolMarkerOptional): - """ - Thinking tool for determining whether the task is truly completed. - """ - - def apply(self) -> str: - """ - Whenever you feel that you are done with what the user has asked for, it is important to call this tool. - """ - return self.prompt_factory.create_think_about_whether_you_are_done() - - -class SummarizeChangesTool(Tool, ToolMarkerOptional): - """ - Provides instructions for summarizing the changes made to the codebase. - """ - - def apply(self) -> str: - """ - Summarize the changes you have made to the codebase. - This tool should always be called after you have fully completed any non-trivial coding task, - but only after the think_about_whether_you_are_done call. - """ - return self.prompt_factory.create_summarize_changes() - - -class PrepareForNewConversationTool(Tool): - """ - Provides instructions for preparing for a new conversation (in order to continue with the necessary context). - """ - - def apply(self) -> str: - """ - Instructions for preparing for a new conversation. This tool should only be called on explicit user request. - """ - return self.prompt_factory.create_prepare_for_new_conversation() - - class InitialInstructionsTool(Tool, ToolMarkerDoesNotRequireActiveProject): """ Provides instructions Serena usage (i.e. the 'Serena Instructions Manual') From 2aa8cf4caeb609633fd37ca7cab68ee2c5512132 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 8 Apr 2026 16:30:52 +0200 Subject: [PATCH 075/190] Change release mechanism Release creation and publishing to PyPI is now triggered by push of tag v* --- .github/workflows/publish.yml | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8a0145c4..04127f48 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,22 +1,33 @@ name: Publish Python Package on: - release: - types: [created] - - workflow_dispatch: - inputs: - tag: - description: 'Tag name for the release (e.g., v0.1.0)' - required: true - default: 'v0.1.0' + push: + # Sequence of patterns matched against refs/tags + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 env: - # Set this to true manually in the GitHub workflow UI if you want to publish to PyPI - # Will always publish to testpypi PUBLISH_TO_PYPI: true jobs: + build: + name: Create GitHub Release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + body: | + See [change log](https://github.com/oraios/serena/blob/main/CHANGELOG.md) + draft: false + prerelease: false publish: name: Publish the serena-agent package runs-on: ubuntu-latest From 527abc2ed0bba05a85c6afc4a21e2f1d443b1ebb Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 8 Apr 2026 17:04:43 +0200 Subject: [PATCH 076/190] Add commit and tag in bump_version script, add developer instructions --- CONTRIBUTING.md | 2 +- README-dev.md | 19 +++++++++++++++++++ scripts/bump_version.py | 9 +++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 README-dev.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a59b63be..a52b439c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ See the corresponding [memory](.serena/memories/adding_new_language_support_guid You can install a virtual environment with the required as follows -1. Create a new virtual environment: `uv venv` +1. Create a new virtual environment: `uv venv -p 3.13` 2. Activate the environment: * On Linux/Unix/macOS or Windows with Git Bash: `source .venv/bin/activate` * On Windows outside of Git Bash: `.venv\Scripts\activate.bat` (in cmd/ps) or `source .venv/Scripts/activate` (in git-bash) diff --git a/README-dev.md b/README-dev.md new file mode 100644 index 00000000..958e4893 --- /dev/null +++ b/README-dev.md @@ -0,0 +1,19 @@ +# Developer Instructions + +## Python Environment & Development Tools + +See [the contributing guide](CONTRIBUTING.md) for instructions on setting up your development environment +and tools for formatting and type checking. + +## Release Process + +1. Ensure clean git status +2. Set version for release, e.g. + + python scripts/bump_version.py --patch + python scripts/bump_version.py --minor + + This also creates the git tag. +3. Push to GitHub, triggering the release process. + + git push --tags \ No newline at end of file diff --git a/scripts/bump_version.py b/scripts/bump_version.py index ec4de118..9a54e70b 100644 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import os import re from pathlib import Path from typing import Literal @@ -8,6 +9,7 @@ from typing import Literal import click from serena.constants import REPO_ROOT +from serena.util.git import get_git_status log = logging.getLogger(__name__) @@ -27,6 +29,10 @@ _UNRELEASED_HEADER = "# Unreleased (main)\n" @click.option("--version", "-v", "target_version", metavar="X.Y.Z", help="Set an explicit version instead of bumping.") @click.option("--dry-run", is_flag=True, help="Show what would change without writing any files.") def bump_version(major: bool, minor: bool, patch: bool, target_version: str | None, dry_run: bool) -> None: + git_status = get_git_status() + if not git_status.is_clean: + raise click.ClickException("Working directory is not clean. Please commit or stash your changes first.") + log.info("bump_version called: major=%s, minor=%s, patch=%s, target_version=%s", major, minor, patch, target_version) version_part = resolve_version_selection(major=major, minor=minor, patch=patch, target_version=target_version) log.info("Resolved version_part=%s", version_part) @@ -39,6 +45,9 @@ def bump_version(major: bool, minor: bool, patch: bool, target_version: str | No click.echo(f"Dry run complete. Version would be bumped to {new_version}") else: click.echo(f"Bumped version to {new_version}") + os.system("git add -u") + os.system(f'git commit -m "Release v{new_version}"') + os.system(f"git tag v{new_version}") def find_repo_root() -> Path: From b0fd89579403bf4a165e4691d8e1d9f778b73ec0 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 8 Apr 2026 17:12:25 +0200 Subject: [PATCH 077/190] Do not assume any default for version component to bump --- scripts/bump_version.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/bump_version.py b/scripts/bump_version.py index 9a54e70b..2fdd213d 100644 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -25,7 +25,7 @@ _UNRELEASED_HEADER = "# Unreleased (main)\n" @click.command() @click.option("--major", "major", is_flag=True, help="Bump the major version and reset minor and patch to 0.") @click.option("--minor", "minor", is_flag=True, help="Bump the minor version and reset patch to 0.") -@click.option("--patch", "patch", is_flag=True, help="Bump the patch version (default).") +@click.option("--patch", "patch", is_flag=True, help="Bump the patch version.") @click.option("--version", "-v", "target_version", metavar="X.Y.Z", help="Set an explicit version instead of bumping.") @click.option("--dry-run", is_flag=True, help="Show what would change without writing any files.") def bump_version(major: bool, minor: bool, patch: bool, target_version: str | None, dry_run: bool) -> None: @@ -67,7 +67,9 @@ def resolve_version_selection(*, major: bool, minor: bool, patch: bool, target_v return "major" if minor: return "minor" - return "patch" + if patch: + return "patch" + raise click.ClickException("No version bump selected. Use --major, --minor, --patch or --version.") def bump_repo_version(repo_root: Path, *, version_part: VersionPart | None, target_version: str | None, dry_run: bool = False) -> str: From aafad5c2b027192a4aab2eb6fe7b5e56b93aacfb Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 8 Apr 2026 14:44:29 +0200 Subject: [PATCH 078/190] Added context file for copilot plugin in JB IDEs --- .../config/contexts/jb-copilot-plugin.yml | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/serena/resources/config/contexts/jb-copilot-plugin.yml diff --git a/src/serena/resources/config/contexts/jb-copilot-plugin.yml b/src/serena/resources/config/contexts/jb-copilot-plugin.yml new file mode 100644 index 00000000..9cbae115 --- /dev/null +++ b/src/serena/resources/config/contexts/jb-copilot-plugin.yml @@ -0,0 +1,27 @@ +description: Generic IDE coding agent context (basic file operations and shell operations assumed to be covered; single project mode) +prompt: | + In addition to Serena's tools, you have access to internal tools, including for, basic (line-based) edits and reads, + and shell commands. + + If Serena's tools can be used to achieve your task, you should prioritize them. + In particular, it is important that you avoid reading entire source code files unless it is strictly necessary! + Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools. + For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool. + + Make sure to first activate the working directory as using Serena's `activate_project` tool and to read initial + instructions (if not done so already) with the `initial_instructions` tool. + +excluded_tools: + - create_text_file + - read_file + - execute_shell_command + +tool_description_overrides: {} + +# whether to assume that Serena shall only work on a single project in this context (provided that a project is given +# when Serena is started). +# If set to true and a project is provided at startup, the set of tools is limited to those required by the project's +# concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal. +# Tools explicitly disabled by the project will not be available at all. +# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed. +single_project: true From e35c09842f54cc0e15faef2c920f2cb0b48fd8a0 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 8 Apr 2026 20:49:09 +0200 Subject: [PATCH 079/190] ActivateProjectTool: Return instructions to read manual also if project is already activated --- src/serena/tools/config_tools.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/serena/tools/config_tools.py b/src/serena/tools/config_tools.py index 17ffe74b..2008012b 100644 --- a/src/serena/tools/config_tools.py +++ b/src/serena/tools/config_tools.py @@ -30,8 +30,9 @@ class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject): """ is_new_activation = self.agent.activate_project_from_path_or_name(project) if not is_new_activation: - return "Project was already active." - result = self.agent.get_project_activation_message() + result = "Project was already active." + else: + result = self.agent.get_project_activation_message() result += "\nIMPORTANT: If you have not yet read the 'Serena Instructions Manual', do it now before continuing!" return result From c25deb845de2aa1115439c58a2649958a3b95a75 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 8 Apr 2026 21:13:51 +0200 Subject: [PATCH 080/190] JetBrainsFindReferencingSymbolsTool: Add computation of context lines based on line number now returned by plugin --- src/serena/jetbrains/jetbrains_types.py | 44 +++++++++++++++++++++++-- src/serena/tools/jetbrains_tools.py | 16 +++++++-- src/serena/tools/symbol_tools.py | 2 +- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/serena/jetbrains/jetbrains_types.py b/src/serena/jetbrains/jetbrains_types.py index fd72bc60..ead4eb55 100644 --- a/src/serena/jetbrains/jetbrains_types.py +++ b/src/serena/jetbrains/jetbrains_types.py @@ -1,5 +1,10 @@ from typing import Literal, NotRequired, TypedDict +JB_EXTERNAL_FILE_PREFIX = " bool: + """ + Checks if a symbol is an external symbol (i.e., from a library) based on its relative path. + """ + return symbol_dto["relative_path"].startswith(JB_EXTERNAL_FILE_PREFIX) class SymbolCollectionResponse(TypedDict): diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 59ab3be9..6db26e0b 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -5,7 +5,7 @@ from typing import Any, Literal import serena.jetbrains.jetbrains_types as jb from serena.code_editor import JetBrainsCodeEditor from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient -from serena.jetbrains.jetbrains_types import SymbolDTO +from serena.jetbrains.jetbrains_types import SymbolDTO, SymbolDTOUtil from serena.symbol import JetBrainsSymbolDictGrouper from serena.tools import Tool, ToolMarkerBeta, ToolMarkerOptional, ToolMarkerSymbolicEdit, ToolMarkerSymbolicRead from serena.util.text_utils import find_text_coordinates @@ -259,7 +259,19 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMark ) symbol_dicts = response_dict["symbols"] - # capture file paths before grouping, which mutates the dicts + # replace reference line number (if present) by actual line/context + for symbol_dict in symbol_dicts: + if "reference_line_no" in symbol_dict: + ref_line = symbol_dict["reference_line_no"] + ref_relative_path = symbol_dict["relative_path"] + if not SymbolDTOUtil.is_external_symbol(symbol_dict) and ref_line is not None and ref_line >= 0: + content_around_ref = self.project.retrieve_content_around_line( + relative_file_path=ref_relative_path, line=ref_line, context_lines_before=1, context_lines_after=1 + ) + symbol_dict["context"] = content_around_ref.to_display_string() + del symbol_dict["reference_line_no"] + + # capture file paths before grouping ref_paths = [s.get("relative_path", "unknown") for s in symbol_dicts] result = self.symbol_dict_grouper.group(symbol_dicts) diff --git a/src/serena/tools/symbol_tools.py b/src/serena/tools/symbol_tools.py index bff6f2fd..972703a1 100644 --- a/src/serena/tools/symbol_tools.py +++ b/src/serena/tools/symbol_tools.py @@ -290,7 +290,7 @@ class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead): ref_dict["content_around_reference"] = content_around_ref.to_display_string() reference_dicts.append(ref_dict) - # capture lightweight reference data before grouping, which mutates the dicts + # capture lightweight reference data before grouping ref_summaries = [] for ref, d in zip(references_in_symbols, reference_dicts, strict=True): ref_summaries.append( From a0519d183d3664ec89955204311a8c6505b5af6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:58:26 +0000 Subject: [PATCH 081/190] Bump cryptography in the uv group across 1 directory Bumps the uv group with 1 update in the / directory: [cryptography](https://github.com/pyca/cryptography). Updates `cryptography` from 46.0.6 to 46.0.7 - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/46.0.6...46.0.7) --- updated-dependencies: - dependency-name: cryptography dependency-version: 46.0.7 dependency-type: direct:production dependency-group: uv ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- uv.lock | 102 ++++++++++++++++++++++++------------------------- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 66e4cf76..fedccc91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dependencies = [ "starlette==1.0.0", "python-multipart==0.0.22", "filelock==3.25.2", - "cryptography==46.0.6", + "cryptography==46.0.7", "regex==2026.2.28", "pythonnet==3.1.0-rc0 ; sys_platform == 'win32'", ] diff --git a/uv.lock b/uv.lock index b79d5624..edc0249c 100644 --- a/uv.lock +++ b/uv.lock @@ -455,61 +455,61 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.6" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, - { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, - { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, - { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, - { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, - { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, - { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, - { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, - { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, - { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, - { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, - { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, - { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, - { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, - { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, - { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, - { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, - { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, - { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, - { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, - { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, - { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, - { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, - { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, - { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, - { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, - { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, - { url = "https://files.pythonhosted.org/packages/2e/84/7ccff00ced5bac74b775ce0beb7d1be4e8637536b522b5df9b73ada42da2/cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead", size = 3475444, upload-time = "2026-03-25T23:34:38.944Z" }, - { url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" }, - { url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" }, - { url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" }, - { url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, ] [[package]] @@ -2983,7 +2983,7 @@ requires-dist = [ { name = "agno", marker = "extra == 'agno'", specifier = "==2.5.10" }, { name = "anthropic", specifier = "==0.59.0" }, { name = "beautifulsoup4", specifier = "==4.14.2" }, - { name = "cryptography", specifier = "==46.0.6" }, + { name = "cryptography", specifier = "==46.0.7" }, { name = "docstring-parser", specifier = "==0.17.0" }, { name = "dotenv", specifier = "==0.9.9" }, { name = "filelock", specifier = "==3.25.2" }, From 1b299830fdac73b0f0ea986ae3213cf91d066613 Mon Sep 17 00:00:00 2001 From: zerone0x Date: Thu, 9 Apr 2026 04:37:23 +0200 Subject: [PATCH 082/190] fix: avoid ignoring root path in gitignore checks Fixes #1054 Co-Authored-By: Claude --- src/serena/util/file_system.py | 3 +++ test/serena/util/test_file_system.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/serena/util/file_system.py b/src/serena/util/file_system.py index d7d9f956..318cb571 100644 --- a/src/serena/util/file_system.py +++ b/src/serena/util/file_system.py @@ -341,6 +341,9 @@ def match_path(relative_path: str, path_spec: PathSpec, root_path: str = "") -> :param root_path: the root path from which the relative path is derived :return: """ + if str(relative_path) in {"", "."}: + return False + normalized_path = str(relative_path).replace(os.path.sep, "/") # We can have patterns like /src/..., which would only match corresponding paths from the repo root diff --git a/test/serena/util/test_file_system.py b/test/serena/util/test_file_system.py index b47dd34c..95ce9e61 100644 --- a/test/serena/util/test_file_system.py +++ b/test/serena/util/test_file_system.py @@ -4,7 +4,9 @@ import tempfile from pathlib import Path # Assuming the gitignore parser code is in a module named 'gitignore_parser' -from serena.util.file_system import GitignoreParser, GitignoreSpec +from pathspec import PathSpec + +from serena.util.file_system import GitignoreParser, GitignoreSpec, match_path class TestGitignoreParser: @@ -191,6 +193,13 @@ test.log assert not parser.should_ignore("file1.txt") assert not parser.should_ignore("src/main.py") + def test_match_path_root_directory(self): + """Root directory should never be ignored by pathspec patterns.""" + spec = PathSpec.from_lines("gitwildmatch", ["/.*/"]) + + assert not match_path(".", spec, root_path=str(self.repo_path)) + assert not match_path("", spec, root_path=str(self.repo_path)) + def test_should_ignore_subdirectory_patterns(self): """Test ignoring files based on subdirectory .gitignore files.""" parser = GitignoreParser(str(self.repo_path)) From 6c94f34fac472338af7c840f5327fbc521a8046a Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 9 Apr 2026 15:28:07 +0200 Subject: [PATCH 083/190] Update change log --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb3c8397..c1fd6386 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 * General: - Added `serena init` command - Added Crystal language support (uses [Crystalline](https://github.com/elbywan/crystalline) language server) + - `JetBrainsFindReferencingSymbolTool`: Include context lines (when using plugin version 2023.2.15+) * Dashboard: - Add version display @@ -12,7 +13,7 @@ Status of the `main` branch. Changes prior to the next official version change w * Fixes: - Fix reactivation of the same project restarting language servers #1280 - Fix git commit id in version - - Fix manual shutdown triggered by GUI tool/dashboard not cleaning everything up. + - Fix manual server shutdown triggered by GUI tool/dashboard not cleaning everything up. # 1.0.0 From b32718a0135a49cf27c496c83073b15e3e5d33a5 Mon Sep 17 00:00:00 2001 From: TheSmallKiwi Date: Thu, 9 Apr 2026 15:44:03 -0400 Subject: [PATCH 084/190] Fix HLSL language server crash on CRLF files pulled in via #include shader-language-server (antaalt/shader-sense) v1.3.0 panics with "byte index N is out of bounds" in symbol_parser.rs when a file is first parsed as an include dependency (raw CRLF bytes via std::fs::read_to_string) and then later opened as a main document. watch_main_file replaces the cached module's content with the client-provided didOpen text but does not re-parse the tree-sitter tree, so if the client normalized line endings (CRLF -> LF) the stored tree references byte offsets past the new content length and the next documentSymbol query slices out of bounds. Python's default text-mode open performs universal-newlines translation, which is what was shrinking the text Serena sent in didOpen. Override open_file in HlslLanguageServer to preload LSPFileBuffer contents with newline="" so the bytes sent to the server match what it reads from disk, sidestepping the stale-tree condition entirely. The fix is scoped to hlsl_language_server.py; no core files are touched. Verified with a 28-shader sweep over a Unity HDRP workspace where the crash was 100% reproducible (TerrainRayMarch.shader -> TerrainSDF.hlsl); all files now return symbols cleanly. The upstream bug should still be fixed in shader-sense (watch_main_file should call update_module rather than assigning .content), but this workaround unblocks Windows users immediately. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../language_servers/hlsl_language_server.py | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/src/solidlsp/language_servers/hlsl_language_server.py b/src/solidlsp/language_servers/hlsl_language_server.py index 0a3203eb..2857055b 100644 --- a/src/solidlsp/language_servers/hlsl_language_server.py +++ b/src/solidlsp/language_servers/hlsl_language_server.py @@ -7,13 +7,22 @@ import logging import os import pathlib import shutil +from collections.abc import Iterator +from contextlib import contextmanager +from pathlib import Path from typing import Any, cast import psutil from overrides import override -from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer +from solidlsp.ls import ( + LanguageServerDependencyProvider, + LanguageServerDependencyProviderSinglePath, + LSPFileBuffer, + SolidLanguageServer, +) from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_exceptions import SolidLSPException from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.settings import SolidLSPSettings @@ -268,6 +277,92 @@ class HlslLanguageServer(SolidLanguageServer): log.debug(f"Error cleaning up shader-language-server process tree: {e}") super().stop(shutdown_timeout) + @contextmanager + def open_file(self, relative_file_path: str, open_in_ls: bool = True) -> Iterator[LSPFileBuffer]: + """Open a file for LSP, preserving on-disk CRLF line endings. + + Workaround for an upstream bug in shader-language-server + (antaalt/shader-sense) where `watch_main_file` replaces an already-cached + module's content without re-parsing the tree-sitter tree. When a file is + first pulled into the server's cache via an `#include` from another + shader (where it's read via `std::fs::read_to_string`, preserving CRLF), + and then later opened directly via `textDocument/didOpen` with the + client-normalized LF text, the stored tree still references byte offsets + into the longer CRLF content. The next symbol query slices the new + (shorter) content with stale offsets and panics with + `byte index N is out of bounds` in `shader-sense/src/symbols/symbol_parser.rs`. + + The root-cause fix belongs upstream (the server should call + `update_module` instead of assigning `content` raw). Until then, we + ensure the text we send in `didOpen` matches byte-for-byte what the + server reads from disk by preloading the file buffer with a + CRLF-preserving read before the LSP notification is sent. + + This is the only place in Serena that overrides `open_file`; the fix is + deliberately scoped to the HLSL language server. It mirrors the base + class logic in `SolidLanguageServer.open_file` verbatim except for the + buffer construction branch, where creation is deferred (`open_in_ls=False`) + so the buffer's contents can be preloaded before `ensure_open_in_ls` runs. + """ + if not self.server_started: + log.error("open_file called before Language Server started") + raise SolidLSPException("Language Server not started") + + absolute_file_path = Path(self.repository_root_path, relative_file_path) + uri = absolute_file_path.as_uri() + + if uri in self.open_file_buffers: + fb = self.open_file_buffers[uri] + assert fb.uri == uri + assert fb.ref_count >= 1 + + fb.ref_count += 1 + if open_in_ls: + fb.ensure_open_in_ls() + yield fb + fb.ref_count -= 1 + else: + version = 0 + language_id = self._get_language_id_for_file(relative_file_path) + # Defer the didOpen so we can preload CRLF-preserved content first. + fb = LSPFileBuffer( + abs_path=absolute_file_path, + uri=uri, + encoding=self._encoding, + version=version, + language_id=language_id, + ref_count=1, + language_server=self, + open_in_ls=False, + ) + self._preload_crlf_content(fb) + self.open_file_buffers[uri] = fb + if open_in_ls: + fb.ensure_open_in_ls() + yield fb + fb.ref_count -= 1 + + if self.open_file_buffers[uri].ref_count == 0: + self.open_file_buffers[uri].close() + del self.open_file_buffers[uri] + + def _preload_crlf_content(self, fb: LSPFileBuffer) -> None: + """Populate an LSPFileBuffer with a CRLF-preserving read of its backing file. + + Python's default text-mode open applies universal-newlines translation + (CRLF -> LF), which would desync the client's `didOpen` text from the + server-side `std::fs::read_to_string` view that parsed the dependency + tree. Passing `newline=""` disables the translation so bytes match. + """ + with open(fb.abs_path, encoding=fb.encoding, newline="") as f: + raw = f.read() + # Set the buffer's cached state directly: the contents, the mtime + # (required by the contents property's staleness check), and clear the + # hash so it's recomputed against the new bytes. + fb._contents = raw + fb._read_file_modified_date = fb.abs_path.stat().st_mtime + fb._content_hash = None + @override def is_ignored_dirname(self, dirname: str) -> bool: """Ignore Unity-specific directories that contain no user-authored shaders.""" From ace531b17aca17e01f5c992f67b0429c8308501e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 9 Apr 2026 21:05:30 +0200 Subject: [PATCH 085/190] Added serena-hooks cli --- pyproject.toml | 1 + src/serena/cli.py | 29 +-- src/serena/hooks.py | 251 ++++++++++++++++++++++++++ src/serena/util/cli_util.py | 24 +++ test/serena/test_hooks.py | 344 ++++++++++++++++++++++++++++++++++++ 5 files changed, 627 insertions(+), 22 deletions(-) create mode 100644 src/serena/hooks.py create mode 100644 test/serena/test_hooks.py diff --git a/pyproject.toml b/pyproject.toml index 0cc71524..a69e2ed6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ explicit = true [project.scripts] serena = "serena.cli:top_level" +serena-hooks = "serena.hooks:hook_commands" [project.license] text = "MIT" diff --git a/src/serena/cli.py b/src/serena/cli.py index 6e297cac..84f2fcc9 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -39,6 +39,7 @@ from serena.constants import ( from serena.mcp import SerenaMCPFactory from serena.project import Project from serena.tools import FindReferencingSymbolsTool, FindSymbolTool, GetSymbolsOverviewTool, SearchForPatternTool, ToolRegistry +from serena.util.cli_util import AutoRegisteringGroup from serena.util.dataclass import get_dataclass_default from serena.util.logging import MemoryLogHandler from solidlsp.ls_config import Language @@ -127,32 +128,16 @@ class ProjectType(click.ParamType): PROJECT_TYPE = ProjectType() -class AutoRegisteringGroup(click.Group): - """ - A click.Group subclass that automatically registers any click.Command - attributes defined on the class into the group. - - After initialization, it inspects its own class for attributes that are - instances of click.Command (typically created via @click.command) and - calls self.add_command(cmd) on each. This lets you define your commands - as static methods on the subclass for IDE-friendly organization without - manual registration. - """ - - def __init__(self, name: str, help: str): - super().__init__(name=name, help=help) - # Scan class attributes for click.Command instances and register them. - for attr in dir(self.__class__): - cmd = getattr(self.__class__, attr) - if isinstance(cmd, click.Command): - self.add_command(cmd) - - class TopLevelCommands(AutoRegisteringGroup): """Root CLI group containing the core Serena commands.""" def __init__(self) -> None: - super().__init__(name="serena", help="Serena CLI commands. You can run ` --help` for more info on each command.") + super().__init__( + name="serena", + help="Main serena CLI commands. " + "Note that you also have access to `serena-hooks` CLI commands which are kept under " + "that separate entrypoint for performance reasons, see `serena-hooks --help`. You can run ` --help` for more info on each command.", + ) @staticmethod @click.command( diff --git a/src/serena/hooks.py b/src/serena/hooks.py new file mode 100644 index 00000000..264e6fe5 --- /dev/null +++ b/src/serena/hooks.py @@ -0,0 +1,251 @@ +import json +import os +import pickle +import shutil +import sys +from abc import ABC, abstractmethod +from dataclasses import dataclass +from datetime import datetime +from enum import Enum +from pathlib import Path +from typing import Literal, Self + +import click + +from serena.util.cli_util import AutoRegisteringGroup + +# copied from serena_config.py, we don't want to import anything here to keep the hook commands fast +serena_home_dir = os.getenv("SERENA_HOME", "").strip() or str(Path.home() / ".serena") + + +class HookClient(Enum): + """The client application that triggered the hook.""" + + CLAUDE_CODE = "claude-code" + VSCODE = "vscode" + CODEX = "codex" + + +class Hook(ABC): + def __init__(self, client: HookClient): + raw = sys.stdin.read() + input_data = json.loads(raw) + self._input_data = input_data + self._client = client + + session_id = input_data.get("session_id") or input_data.get("sessionId") + if not session_id: + raise ValueError("Session ID is required in the hook input data") + self._session_id = str(session_id) + self.session_persistence_dir = os.path.join(serena_home_dir, "hook_data", self._session_id) + # tool input has a timestamp but using now is enough + self.triggered_at_timestamp = datetime.now() + + @abstractmethod + def execute(self) -> None: + pass + + +class PreToolUseRemindAboutSerenaHook(Hook): + """Pre-tool-use hook that nudges the agent toward Serena's symbolic tools. + + Tracks consecutive uses of grep and read-file tools via a persisted + :class:`ToolUseCounter`. When the number of recent calls reaches the + configured threshold, a deny response is emitted with a reminder to + use symbolic alternatives. The counter resets whenever a Serena tool + is invoked or a deny is emitted, so the agent is only reminded after + sustained non-symbolic tool use without intermittent Serena calls. + """ + + @dataclass + class OutputData: + permission_decision: Literal["deny", "allow"] + permission_decision_reason: str + additional_context: str + + def to_json_string(self) -> str: + hook_output = { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": self.permission_decision, + "permissionDecisionReason": self.permission_decision_reason, + "additionalContext": self.additional_context, + } + } + return json.dumps(hook_output) + + @dataclass + class ToolUseCounter: + _FILE_NAME = "tool_use_counter.pkl" + _GREP_USES_THRESHOLD = 3 + _READ_FILE_USES_THRESHOLD = 3 + + _PERIOD_TO_RESET_COUNTERS_SECONDS = 10 + + n_recent_read_file_uses: int = 0 + n_recent_grep_uses: int = 0 + last_grep_use_timestamp: datetime | None = None + last_read_file_use_timestamp: datetime | None = None + + def too_many_recent_reads(self) -> bool: + return self.n_recent_read_file_uses >= self._READ_FILE_USES_THRESHOLD + + def too_many_recent_greps(self) -> bool: + return self.n_recent_grep_uses >= self._GREP_USES_THRESHOLD + + @classmethod + def _get_persistence_path(cls, hook: Hook) -> Path: + return Path(hook.session_persistence_dir) / cls._FILE_NAME + + @classmethod + def load(cls, hook: Hook) -> Self: + path = cls._get_persistence_path(hook) + try: + with open(path, "rb") as f: + return pickle.load(f) + except Exception: + return cls() + + def save(self, hook: Hook) -> None: + path = self._get_persistence_path(hook) + try: + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "wb") as f: + pickle.dump(self, f) + except Exception: + pass + + def update(self, hook: "PreToolUseRemindAboutSerenaHook") -> None: + if hook.is_serena_tool(): + self.reset() + return + + now = hook.triggered_at_timestamp + period = self._PERIOD_TO_RESET_COUNTERS_SECONDS + + # update grep counter + if hook.is_grep_tool(): + if self.last_grep_use_timestamp is not None and (now - self.last_grep_use_timestamp).total_seconds() <= period: + self.n_recent_grep_uses += 1 + else: + self.n_recent_grep_uses = 1 + self.last_grep_use_timestamp = now + + # update read file counter + if hook.is_read_file_tool(): + if self.last_read_file_use_timestamp is not None and (now - self.last_read_file_use_timestamp).total_seconds() <= period: + self.n_recent_read_file_uses += 1 + else: + self.n_recent_read_file_uses = 1 + self.last_read_file_use_timestamp = now + + def reset(self) -> None: + self.n_recent_read_file_uses = 0 + self.n_recent_grep_uses = 0 + self.last_grep_use_timestamp = None + self.last_read_file_use_timestamp = None + + def __init__(self, client: HookClient): + super().__init__(client) + _tool_name = self._input_data.get("tool_name") or self._input_data.get("toolName", "") or "" + _tool_name = str(_tool_name).lower().strip() + if not _tool_name: + raise ValueError("Tool name is required in the hook input data") + self._tool_name = _tool_name + self._tool_input: dict | None = self._input_data.get("tool_input") or self._input_data.get("toolInput") + self._tool_call_counter = self.ToolUseCounter.load(self) + + def is_grep_tool(self) -> bool: + if self._client == HookClient.CLAUDE_CODE: + return self._tool_name == "grep" + return "grep" in self._tool_name + + def is_read_file_tool(self) -> bool: + if self._client == HookClient.CLAUDE_CODE: + return self._tool_name == "read" + return "read" in self._tool_name and "file" in self._tool_name + + def is_serena_tool(self) -> bool: + return "serena" in self._tool_name + + def execute(self) -> None: + self._tool_call_counter.update(self) + output_data = None + if self._tool_call_counter.too_many_recent_greps(): + output_data = self.OutputData( + permission_decision="deny", + permission_decision_reason="Not using symbolic tools. You can continue using grep now if needed, the counter was reset.", + additional_context="You were using many grep calls recently. Consider using Serena's symbolic search tools instead.", + ) + if self._tool_call_counter.too_many_recent_reads(): + output_data = self.OutputData( + permission_decision="deny", + permission_decision_reason="Not using symbolic tools. You can continue using read now if needed, the counter was reset.", + additional_context="You were using many read file calls recently. Consider using Serena's symbolic overview and search instead.", + ) + if output_data is not None: + self._tool_call_counter.reset() + click.echo(output_data.to_json_string()) + self._tool_call_counter.save(self) + + +class SessionStartActivateProjectHook(Hook): + def execute(self) -> None: + message = ( + "**IMPORTANT**: Activate the current working directory as project using Serena's tools. " + "If you haven't read Serena Instructions Manual yet, do so. " + "Follow this instruction before doing anything else." + ) + result = { + "hookSpecificOutput": { + "hookEventName": "SessionStart", + "additionalContext": message, + } + } + click.echo(json.dumps(result)) + + +class SessionEndCleanupHook(Hook): + def execute(self) -> None: + shutil.rmtree(self.session_persistence_dir, ignore_errors=True) + + +_client_option = click.option( + "--client", + type=click.Choice([e.value for e in HookClient], case_sensitive=False), + default=HookClient.CLAUDE_CODE.value, + show_default=True, + help="The client application that triggered the hook.", +) + + +class HookCommands(AutoRegisteringGroup): + def __init__(self) -> None: + super().__init__(name="serena-hook", help="Commands that send reminders to agents when appropriate, to be used in hooks.") + + @staticmethod + @click.command( + "activate", + help="Set this as hook at session startup to prompt the agent to activate the project at the start of the session and read Serena's instructions", + ) + @_client_option + def activate(client: str) -> None: + SessionStartActivateProjectHook(HookClient(client)).execute() + + @staticmethod + @click.command("cleanup", help="Set this as hook at session end all hook data for the current session") + @_client_option + def cleanup(client: str) -> None: + SessionEndCleanupHook(HookClient(client)).execute() + + @staticmethod + @click.command( + "remind", + help="Set this as hook at PreToolUse to remind the agent to use Serena's tools instead of overrelying on read_file and grep", + ) + @_client_option + def remind(client: str) -> None: + PreToolUseRemindAboutSerenaHook(HookClient(client)).execute() + + +hook_commands = HookCommands() diff --git a/src/serena/util/cli_util.py b/src/serena/util/cli_util.py index 66f725bb..807ae7c0 100644 --- a/src/serena/util/cli_util.py +++ b/src/serena/util/cli_util.py @@ -1,3 +1,6 @@ +import click + + def ask_yes_no(question: str, default: bool | None = None) -> bool: default_prompt = "Y/n" if default else "y/N" @@ -10,3 +13,24 @@ def ask_yes_no(question: str, default: bool | None = None) -> bool: if answer in ("n", "no"): return False print("Please answer yes/y or no/n.") + + +class AutoRegisteringGroup(click.Group): + """ + A click.Group subclass that automatically registers any click.Command + attributes defined on the class into the group. + + After initialization, it inspects its own class for attributes that are + instances of click.Command (typically created via @click.command) and + calls self.add_command(cmd) on each. This lets you define your commands + as static methods on the subclass for IDE-friendly organization without + manual registration. + """ + + def __init__(self, name: str, help: str): + super().__init__(name=name, help=help) + # Scan class attributes for click.Command instances and register them. + for attr in dir(self.__class__): + cmd = getattr(self.__class__, attr) + if isinstance(cmd, click.Command): + self.add_command(cmd) diff --git a/test/serena/test_hooks.py b/test/serena/test_hooks.py new file mode 100644 index 00000000..e8291bc7 --- /dev/null +++ b/test/serena/test_hooks.py @@ -0,0 +1,344 @@ +import json +import pickle +from datetime import datetime, timedelta +from io import StringIO +from pathlib import Path +from unittest.mock import patch + +import pytest +from click.testing import CliRunner + +from serena.hooks import ( + HookClient, + PreToolUseRemindAboutSerenaHook, + SessionEndCleanupHook, + SessionStartActivateProjectHook, + hook_commands, +) + +ToolUseCounter = PreToolUseRemindAboutSerenaHook.ToolUseCounter + + +def _make_stdin(data: dict) -> StringIO: + return StringIO(json.dumps(data)) + + +def _base_input(tool_name: str = "grep_search", session_id: str = "test-session-123") -> dict: + return { + "session_id": session_id, + "tool_name": tool_name, + "tool_input": {"query": "foo"}, + } + + +class TestHookClientDetection: + """Tests for the --client option propagation.""" + + def test_claude_code_client(self, tmp_path: Path): + stdin_data = _base_input() + with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)): + hook = PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE) + assert hook._client == HookClient.CLAUDE_CODE + + def test_vscode_client(self, tmp_path: Path): + stdin_data = _base_input() + with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)): + hook = PreToolUseRemindAboutSerenaHook(HookClient.VSCODE) + assert hook._client == HookClient.VSCODE + + +class TestPreToolUseRemindAboutSerenaHook: + """Tests for the PreToolUse hook that nudges the agent toward symbolic tools.""" + + def test_missing_tool_name_raises(self, tmp_path: Path): + stdin_data = {"session_id": "s1"} + with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)): + with pytest.raises(ValueError, match="Tool name is required"): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE) + + def test_missing_session_id_raises(self, tmp_path: Path): + stdin_data = {"tool_name": "grep"} + with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)): + with pytest.raises(ValueError, match="Session ID is required"): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE) + + def test_grep_tool_detection(self, tmp_path: Path): + for name, expected in [("grep_search", True), ("mcp_grep", True), ("read_file", False), ("serena_find", False)]: + with patch("sys.stdin", _make_stdin(_base_input(tool_name=name))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + hook = PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE) + assert hook.is_grep_tool() == expected, f"is_grep_tool() wrong for {name}" + + def test_read_file_tool_detection(self, tmp_path: Path): + for name, expected in [("read_file", True), ("readFile", True), ("grep_search", False), ("file_writer", False)]: + with patch("sys.stdin", _make_stdin(_base_input(tool_name=name))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + hook = PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE) + assert hook.is_read_file_tool() == expected, f"is_read_file_tool() wrong for {name}" + + def test_serena_tool_detection(self, tmp_path: Path): + for name, expected in [("mcp_serena_find_symbol", True), ("serena_overview", True), ("grep_search", False)]: + with patch("sys.stdin", _make_stdin(_base_input(tool_name=name))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + hook = PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE) + assert hook.is_serena_tool() == expected, f"is_serena_tool() wrong for {name}" + + def test_no_output_below_threshold(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """Below the threshold, the hook should produce no output (tool is allowed).""" + for _ in range(ToolUseCounter._GREP_USES_THRESHOLD - 1): + with patch("sys.stdin", _make_stdin(_base_input("grep_search"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() + assert capsys.readouterr().out == "" + + def test_deny_output_after_threshold_greps_claude_code(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """After reaching the grep threshold, the hook should output a deny.""" + for _ in range(ToolUseCounter._GREP_USES_THRESHOLD): + with patch("sys.stdin", _make_stdin(_base_input("grep_search"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() + + output = capsys.readouterr().out.strip() + result = json.loads(output) + hook_output = result["hookSpecificOutput"] + assert hook_output["permissionDecision"] == "deny" + assert "grep" in hook_output["additionalContext"].lower() + + def test_deny_output_after_threshold_greps_vscode(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """After reaching the grep threshold, the hook should output a deny for VS Code.""" + for _ in range(ToolUseCounter._GREP_USES_THRESHOLD): + with patch("sys.stdin", _make_stdin(_base_input("grep_search"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseRemindAboutSerenaHook(HookClient.VSCODE).execute() + + output = capsys.readouterr().out.strip() + result = json.loads(output) + hook_output = result["hookSpecificOutput"] + assert hook_output["permissionDecision"] == "deny" + assert "grep" in hook_output["additionalContext"].lower() + + def test_deny_output_after_threshold_reads(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """After reaching the read file threshold, the hook should output a deny.""" + for _ in range(ToolUseCounter._READ_FILE_USES_THRESHOLD): + with patch("sys.stdin", _make_stdin(_base_input("read_file"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() + + output = capsys.readouterr().out.strip() + result = json.loads(output) + hook_output = result["hookSpecificOutput"] + assert hook_output["permissionDecision"] == "deny" + assert "read file" in hook_output["additionalContext"].lower() + + def test_serena_tool_resets_counters(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """Using a Serena tool should reset counters, so the threshold is not reached.""" + for _ in range(ToolUseCounter._GREP_USES_THRESHOLD - 1): + with patch("sys.stdin", _make_stdin(_base_input("grep_search"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() + + with patch("sys.stdin", _make_stdin(_base_input("mcp_serena_find_symbol"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() + + with patch("sys.stdin", _make_stdin(_base_input("grep_search"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() + + assert capsys.readouterr().out == "" + + def test_counter_resets_after_deny(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """After a deny is emitted, the counter is reset so the next burst starts fresh.""" + for _ in range(ToolUseCounter._GREP_USES_THRESHOLD): + with patch("sys.stdin", _make_stdin(_base_input("grep_search"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() + capsys.readouterr() + + with patch("sys.stdin", _make_stdin(_base_input("grep_search"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() + + assert capsys.readouterr().out == "" + + +class TestToolUseCounter: + """Tests for the time-windowed tool-use counter logic.""" + + def test_update_increments_grep_within_period(self): + counter = ToolUseCounter() + now = datetime.now() + counter.last_grep_use_timestamp = now - timedelta(seconds=1) + counter.n_recent_grep_uses = 1 + + hook = self._make_hook_stub("grep_search", now) + counter.update(hook) + + assert counter.n_recent_grep_uses == 2 + assert counter.last_grep_use_timestamp == now + + def test_update_resets_grep_outside_period(self): + counter = ToolUseCounter() + now = datetime.now() + counter.last_grep_use_timestamp = now - timedelta(seconds=ToolUseCounter._PERIOD_TO_RESET_COUNTERS_SECONDS + 1) + counter.n_recent_grep_uses = 2 + + hook = self._make_hook_stub("grep_search", now) + counter.update(hook) + + assert counter.n_recent_grep_uses == 1 + assert counter.last_grep_use_timestamp == now + + def test_update_increments_read_file_within_period(self): + counter = ToolUseCounter() + now = datetime.now() + counter.last_read_file_use_timestamp = now - timedelta(seconds=1) + counter.n_recent_read_file_uses = 1 + + hook = self._make_hook_stub("read_file", now) + counter.update(hook) + + assert counter.n_recent_read_file_uses == 2 + + def test_update_resets_read_file_outside_period(self): + counter = ToolUseCounter() + now = datetime.now() + counter.last_read_file_use_timestamp = now - timedelta(seconds=ToolUseCounter._PERIOD_TO_RESET_COUNTERS_SECONDS + 1) + counter.n_recent_read_file_uses = 2 + + hook = self._make_hook_stub("read_file", now) + counter.update(hook) + + assert counter.n_recent_read_file_uses == 1 + + def test_serena_tool_resets_all_counters(self): + counter = ToolUseCounter( + n_recent_grep_uses=2, + n_recent_read_file_uses=2, + last_grep_use_timestamp=datetime.now(), + last_read_file_use_timestamp=datetime.now(), + ) + hook = self._make_hook_stub("mcp_serena_overview", datetime.now()) + counter.update(hook) + + assert counter.n_recent_grep_uses == 0 + assert counter.n_recent_read_file_uses == 0 + assert counter.last_grep_use_timestamp is None + assert counter.last_read_file_use_timestamp is None + + def test_non_matching_tool_leaves_counters_unchanged(self): + counter = ToolUseCounter(n_recent_grep_uses=1, n_recent_read_file_uses=1) + hook = self._make_hook_stub("write_file", datetime.now()) + counter.update(hook) + + assert counter.n_recent_grep_uses == 1 + assert counter.n_recent_read_file_uses == 1 + + def test_persistence_round_trip(self, tmp_path: Path): + counter = ToolUseCounter(n_recent_grep_uses=2, n_recent_read_file_uses=1) + + hook_stub = type("HookStub", (), {"session_persistence_dir": str(tmp_path)})() + counter.save(hook_stub) # type: ignore[arg-type] + loaded = ToolUseCounter.load(hook_stub) # type: ignore[arg-type] + + assert loaded.n_recent_grep_uses == 2 + assert loaded.n_recent_read_file_uses == 1 + + def test_load_returns_fresh_counter_on_missing_file(self, tmp_path: Path): + hook_stub = type("HookStub", (), {"session_persistence_dir": str(tmp_path / "nonexistent")})() + loaded = ToolUseCounter.load(hook_stub) # type: ignore[arg-type] + assert loaded == ToolUseCounter() + + def test_load_returns_fresh_counter_on_corrupt_file(self, tmp_path: Path): + hook_stub = type("HookStub", (), {"session_persistence_dir": str(tmp_path)})() + path = tmp_path / ToolUseCounter._FILE_NAME + path.write_bytes(b"not a pickle") + loaded = ToolUseCounter.load(hook_stub) # type: ignore[arg-type] + assert loaded == ToolUseCounter() + + @staticmethod + def _make_hook_stub(tool_name: str, timestamp: datetime) -> PreToolUseRemindAboutSerenaHook: + """Create a minimal stub that satisfies ToolUseCounter.update without reading stdin.""" + stub = object.__new__(PreToolUseRemindAboutSerenaHook) + stub._tool_name = tool_name.lower() + stub.triggered_at_timestamp = timestamp + return stub + + +class TestSessionStartActivateProjectHook: + def test_outputs_activation_message(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + stdin_data = {"session_id": "s1"} + with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)): + SessionStartActivateProjectHook(HookClient.VSCODE).execute() + + output = capsys.readouterr().out.strip() + result = json.loads(output) + context = result["hookSpecificOutput"]["additionalContext"] + assert "Activate" in context + assert "Serena Instructions Manual" in context + + +class TestSessionEndCleanupHook: + def test_removes_session_dir(self, tmp_path: Path): + session_dir = tmp_path / "hook_data" / "cleanup-session" + session_dir.mkdir(parents=True) + # place a file inside to verify recursive removal + (session_dir / "tool_use_counter.pkl").write_bytes(pickle.dumps(ToolUseCounter())) + + stdin_data = {"session_id": "cleanup-session"} + with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)): + SessionEndCleanupHook(HookClient.CLAUDE_CODE).execute() + + assert not session_dir.exists() + + def test_cleanup_is_idempotent(self, tmp_path: Path): + """Cleaning up a non-existent session directory should not raise.""" + stdin_data = {"session_id": "nonexistent-session"} + with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)): + SessionEndCleanupHook(HookClient.CLAUDE_CODE).execute() + + +class TestHookCli: + """Tests for the Click CLI entry point (serena-hooks).""" + + def test_activate_command(self, tmp_path: Path): + stdin_json = json.dumps({"session_id": "cli-test"}) + runner = CliRunner() + with patch("serena.hooks.serena_home_dir", str(tmp_path)): + result = runner.invoke(hook_commands, ["activate", "--client", "vscode"], input=stdin_json) + assert result.exit_code == 0 + output = json.loads(result.output) + assert "Activate" in output["hookSpecificOutput"]["additionalContext"] + + def test_cleanup_command(self, tmp_path: Path): + session_dir = tmp_path / "hook_data" / "cli-cleanup" + session_dir.mkdir(parents=True) + (session_dir / "somefile").write_text("data") + + stdin_json = json.dumps({"session_id": "cli-cleanup"}) + runner = CliRunner() + with patch("serena.hooks.serena_home_dir", str(tmp_path)): + result = runner.invoke(hook_commands, ["cleanup", "--client", "claude-code"], input=stdin_json) + assert result.exit_code == 0 + assert not session_dir.exists() + + def test_remind_command(self, tmp_path: Path): + """Invoke the remind command enough times to trigger a deny.""" + runner = CliRunner() + for _ in range(ToolUseCounter._GREP_USES_THRESHOLD): + stdin_json = json.dumps({"session_id": "cli-remind", "tool_name": "grep_search", "tool_input": {}}) + with patch("serena.hooks.serena_home_dir", str(tmp_path)): + result = runner.invoke(hook_commands, ["remind", "--client", "claude-code"], input=stdin_json) + assert result.exit_code == 0 + + output = json.loads(result.output) + assert output["hookSpecificOutput"]["permissionDecision"] == "deny" + + def test_client_default_is_claude_code(self, tmp_path: Path): + """When --client is omitted, it defaults to claude-code.""" + stdin_json = json.dumps({"session_id": "cli-default"}) + runner = CliRunner() + with patch("serena.hooks.serena_home_dir", str(tmp_path)): + result = runner.invoke(hook_commands, ["activate"], input=stdin_json) + assert result.exit_code == 0 + + def test_invalid_client_rejected(self, tmp_path: Path): + stdin_json = json.dumps({"session_id": "s1"}) + runner = CliRunner() + with patch("serena.hooks.serena_home_dir", str(tmp_path)): + result = runner.invoke(hook_commands, ["activate", "--client", "invalid"], input=stdin_json) + assert result.exit_code != 0 + + def test_invalid_stdin_exits_nonzero(self, tmp_path: Path): + runner = CliRunner() + with patch("serena.hooks.serena_home_dir", str(tmp_path)): + result = runner.invoke(hook_commands, ["activate", "--client", "claude-code"], input="not json") + assert result.exit_code != 0 From 71cb3db8ddb0ea9bd873b3594607ed550b7e7044 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 11:03:08 +0200 Subject: [PATCH 086/190] Extensions in serena-hooks cli --- src/serena/hooks.py | 107 ++++++++++++++++++------- test/serena/test_hooks.py | 161 ++++++++++++++++++++++++++++++++++---- 2 files changed, 223 insertions(+), 45 deletions(-) diff --git a/src/serena/hooks.py b/src/serena/hooks.py index 264e6fe5..8988d8a8 100644 --- a/src/serena/hooks.py +++ b/src/serena/hooks.py @@ -46,22 +46,25 @@ class Hook(ABC): pass -class PreToolUseRemindAboutSerenaHook(Hook): - """Pre-tool-use hook that nudges the agent toward Serena's symbolic tools. +class PreToolUseHook(Hook, ABC): + def __init__(self, client: HookClient): + super().__init__(client) + _tool_name = self._input_data.get("tool_name") or self._input_data.get("toolName", "") or "" + _tool_name = str(_tool_name).lower().strip() + if not _tool_name: + raise ValueError("Tool name is required in the hook input data") + self._tool_name = _tool_name + self._tool_input: dict | None = self._input_data.get("tool_input") or self._input_data.get("toolInput") - Tracks consecutive uses of grep and read-file tools via a persisted - :class:`ToolUseCounter`. When the number of recent calls reaches the - configured threshold, a deny response is emitted with a reminder to - use symbolic alternatives. The counter resets whenever a Serena tool - is invoked or a deny is emitted, so the agent is only reminded after - sustained non-symbolic tool use without intermittent Serena calls. - """ + # only relevant in claude code at the moment, (not all events include this field; default to empty string) + raw_permission_mode = self._input_data.get("permission_mode") or self._input_data.get("permissionMode") or "" + self._permission_mode = str(raw_permission_mode).strip() @dataclass class OutputData: permission_decision: Literal["deny", "allow"] permission_decision_reason: str - additional_context: str + additional_context: str = "" def to_json_string(self) -> str: hook_output = { @@ -74,13 +77,29 @@ class PreToolUseRemindAboutSerenaHook(Hook): } return json.dumps(hook_output) + def is_serena_tool(self) -> bool: + return "serena" in self._tool_name + + +class PreToolUseRemindAboutSerenaHook(PreToolUseHook): + """Pre-tool-use hook that nudges the agent toward Serena's symbolic tools. + + Tracks consecutive uses of grep and read-file tools via a persisted + :class:`ToolUseCounter`. When the number of recent calls reaches the + configured threshold, a deny response is emitted with a reminder to + use symbolic alternatives. The counter resets whenever a Serena tool + is invoked or a deny is emitted, so the agent is only reminded after + sustained non-symbolic tool use without intermittent Serena calls. + """ + @dataclass class ToolUseCounter: _FILE_NAME = "tool_use_counter.pkl" _GREP_USES_THRESHOLD = 3 _READ_FILE_USES_THRESHOLD = 3 - _PERIOD_TO_RESET_COUNTERS_SECONDS = 10 + _GREP_RESET_PERIOD_SECONDS = 10 + _READ_FILE_RESET_PERIOD_SECONDS = 20 n_recent_read_file_uses: int = 0 n_recent_grep_uses: int = 0 @@ -121,11 +140,11 @@ class PreToolUseRemindAboutSerenaHook(Hook): return now = hook.triggered_at_timestamp - period = self._PERIOD_TO_RESET_COUNTERS_SECONDS # update grep counter if hook.is_grep_tool(): - if self.last_grep_use_timestamp is not None and (now - self.last_grep_use_timestamp).total_seconds() <= period: + grep_period = self._GREP_RESET_PERIOD_SECONDS + if self.last_grep_use_timestamp is not None and (now - self.last_grep_use_timestamp).total_seconds() <= grep_period: self.n_recent_grep_uses += 1 else: self.n_recent_grep_uses = 1 @@ -133,7 +152,11 @@ class PreToolUseRemindAboutSerenaHook(Hook): # update read file counter if hook.is_read_file_tool(): - if self.last_read_file_use_timestamp is not None and (now - self.last_read_file_use_timestamp).total_seconds() <= period: + read_period = self._READ_FILE_RESET_PERIOD_SECONDS + if ( + self.last_read_file_use_timestamp is not None + and (now - self.last_read_file_use_timestamp).total_seconds() <= read_period + ): self.n_recent_read_file_uses += 1 else: self.n_recent_read_file_uses = 1 @@ -147,12 +170,6 @@ class PreToolUseRemindAboutSerenaHook(Hook): def __init__(self, client: HookClient): super().__init__(client) - _tool_name = self._input_data.get("tool_name") or self._input_data.get("toolName", "") or "" - _tool_name = str(_tool_name).lower().strip() - if not _tool_name: - raise ValueError("Tool name is required in the hook input data") - self._tool_name = _tool_name - self._tool_input: dict | None = self._input_data.get("tool_input") or self._input_data.get("toolInput") self._tool_call_counter = self.ToolUseCounter.load(self) def is_grep_tool(self) -> bool: @@ -165,23 +182,20 @@ class PreToolUseRemindAboutSerenaHook(Hook): return self._tool_name == "read" return "read" in self._tool_name and "file" in self._tool_name - def is_serena_tool(self) -> bool: - return "serena" in self._tool_name - def execute(self) -> None: self._tool_call_counter.update(self) output_data = None if self._tool_call_counter.too_many_recent_greps(): output_data = self.OutputData( permission_decision="deny", - permission_decision_reason="Not using symbolic tools. You can continue using grep now if needed, the counter was reset.", - additional_context="You were using many grep calls recently. Consider using Serena's symbolic search tools instead.", + permission_decision_reason="Too many consecutive grep calls without using symbolic tools.", + additional_context="You were using many grep calls recently. Consider using Serena's symbolic search tools instead. You can continue using grep now if needed, the counter was reset.", ) if self._tool_call_counter.too_many_recent_reads(): output_data = self.OutputData( permission_decision="deny", - permission_decision_reason="Not using symbolic tools. You can continue using read now if needed, the counter was reset.", - additional_context="You were using many read file calls recently. Consider using Serena's symbolic overview and search instead.", + permission_decision_reason="Too many consecutive read calls without using symbolic tools.", + additional_context="You were using many read file calls recently. Consider using Serena's symbolic overview and search instead. You can continue using read now if needed, the counter was reset", ) if output_data is not None: self._tool_call_counter.reset() @@ -192,7 +206,7 @@ class PreToolUseRemindAboutSerenaHook(Hook): class SessionStartActivateProjectHook(Hook): def execute(self) -> None: message = ( - "**IMPORTANT**: Activate the current working directory as project using Serena's tools. " + "**IMPORTANT**: Activate the current working directory as project using Serena's tools if not already done. " "If you haven't read Serena Instructions Manual yet, do so. " "Follow this instruction before doing anything else." ) @@ -210,6 +224,34 @@ class SessionEndCleanupHook(Hook): shutil.rmtree(self.session_persistence_dir, ignore_errors=True) +class PreToolUseAutoApproveSerenaHook(PreToolUseHook): + """Pre-tool-use hook that auto-approves Serena tool calls while the client is in accept-edits mode. + + Claude Code's ``acceptEdits`` permission mode only applies to its built-in editing tools; + Serena's destructive tools (e.g. ``replace_symbol_body`` or ``rename_symbol``) would still + prompt the user on every call. This hook emits an ``allow`` decision for any Serena MCP + tool call whenever the client reports ``acceptEdits`` as the active permission mode, so + blanket edit approvals also cover Serena's tools. In all other situations it stays silent, + preserving the default approval flow. + """ + + _ACCEPT_EDITS_MODE = "acceptEdits" + + def is_accept_edits_mode(self) -> bool: + return self._permission_mode == self._ACCEPT_EDITS_MODE + + def execute(self) -> None: + # only emit a decision when both the tool and the mode match; stay silent otherwise + if not self.is_serena_tool() or not self.is_accept_edits_mode(): + return + + output_data = self.OutputData( + permission_decision="allow", + permission_decision_reason="Auto-approved: Serena tool call while client is in acceptEdits mode.", + ) + click.echo(output_data.to_json_string()) + + _client_option = click.option( "--client", type=click.Choice([e.value for e in HookClient], case_sensitive=False), @@ -247,5 +289,14 @@ class HookCommands(AutoRegisteringGroup): def remind(client: str) -> None: PreToolUseRemindAboutSerenaHook(HookClient(client)).execute() + @staticmethod + @click.command( + "auto-approve", + help="Set this as hook at PreToolUse to auto-approve Serena tool calls while the client is in acceptEdits mode (Claude Code).", + ) + @_client_option + def auto_approve(client: str) -> None: + PreToolUseAutoApproveSerenaHook(HookClient(client)).execute() + hook_commands = HookCommands() diff --git a/test/serena/test_hooks.py b/test/serena/test_hooks.py index e8291bc7..3023261f 100644 --- a/test/serena/test_hooks.py +++ b/test/serena/test_hooks.py @@ -10,6 +10,7 @@ from click.testing import CliRunner from serena.hooks import ( HookClient, + PreToolUseAutoApproveSerenaHook, PreToolUseRemindAboutSerenaHook, SessionEndCleanupHook, SessionStartActivateProjectHook, @@ -62,17 +63,33 @@ class TestPreToolUseRemindAboutSerenaHook: with pytest.raises(ValueError, match="Session ID is required"): PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE) - def test_grep_tool_detection(self, tmp_path: Path): + def test_grep_tool_detection_claude_code(self, tmp_path: Path): + """Claude Code uses the exact tool name ``Grep`` (lowercased to ``grep``).""" + for name, expected in [("grep", True), ("grep_search", False), ("mcp_grep", False), ("read", False)]: + with patch("sys.stdin", _make_stdin(_base_input(tool_name=name))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + hook = PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE) + assert hook.is_grep_tool() == expected, f"is_grep_tool() wrong for {name} (claude-code)" + + def test_grep_tool_detection_non_claude_code(self, tmp_path: Path): + """Non-Claude-Code clients fall back to substring matching to cover verbose tool names.""" for name, expected in [("grep_search", True), ("mcp_grep", True), ("read_file", False), ("serena_find", False)]: with patch("sys.stdin", _make_stdin(_base_input(tool_name=name))), patch("serena.hooks.serena_home_dir", str(tmp_path)): - hook = PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE) - assert hook.is_grep_tool() == expected, f"is_grep_tool() wrong for {name}" + hook = PreToolUseRemindAboutSerenaHook(HookClient.VSCODE) + assert hook.is_grep_tool() == expected, f"is_grep_tool() wrong for {name} (vscode)" - def test_read_file_tool_detection(self, tmp_path: Path): - for name, expected in [("read_file", True), ("readFile", True), ("grep_search", False), ("file_writer", False)]: + def test_read_file_tool_detection_claude_code(self, tmp_path: Path): + """Claude Code uses the exact tool name ``Read`` (lowercased to ``read``).""" + for name, expected in [("read", True), ("read_file", False), ("readFile", False), ("grep", False)]: with patch("sys.stdin", _make_stdin(_base_input(tool_name=name))), patch("serena.hooks.serena_home_dir", str(tmp_path)): hook = PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE) - assert hook.is_read_file_tool() == expected, f"is_read_file_tool() wrong for {name}" + assert hook.is_read_file_tool() == expected, f"is_read_file_tool() wrong for {name} (claude-code)" + + def test_read_file_tool_detection_non_claude_code(self, tmp_path: Path): + """Non-Claude-Code clients fall back to substring matching requiring both ``read`` and ``file``.""" + for name, expected in [("read_file", True), ("readFile", True), ("grep_search", False), ("file_writer", False)]: + with patch("sys.stdin", _make_stdin(_base_input(tool_name=name))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + hook = PreToolUseRemindAboutSerenaHook(HookClient.VSCODE) + assert hook.is_read_file_tool() == expected, f"is_read_file_tool() wrong for {name} (vscode)" def test_serena_tool_detection(self, tmp_path: Path): for name, expected in [("mcp_serena_find_symbol", True), ("serena_overview", True), ("grep_search", False)]: @@ -83,14 +100,14 @@ class TestPreToolUseRemindAboutSerenaHook: def test_no_output_below_threshold(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): """Below the threshold, the hook should produce no output (tool is allowed).""" for _ in range(ToolUseCounter._GREP_USES_THRESHOLD - 1): - with patch("sys.stdin", _make_stdin(_base_input("grep_search"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() assert capsys.readouterr().out == "" def test_deny_output_after_threshold_greps_claude_code(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): """After reaching the grep threshold, the hook should output a deny.""" for _ in range(ToolUseCounter._GREP_USES_THRESHOLD): - with patch("sys.stdin", _make_stdin(_base_input("grep_search"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() output = capsys.readouterr().out.strip() @@ -114,7 +131,7 @@ class TestPreToolUseRemindAboutSerenaHook: def test_deny_output_after_threshold_reads(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): """After reaching the read file threshold, the hook should output a deny.""" for _ in range(ToolUseCounter._READ_FILE_USES_THRESHOLD): - with patch("sys.stdin", _make_stdin(_base_input("read_file"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + with patch("sys.stdin", _make_stdin(_base_input("read"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() output = capsys.readouterr().out.strip() @@ -126,13 +143,13 @@ class TestPreToolUseRemindAboutSerenaHook: def test_serena_tool_resets_counters(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): """Using a Serena tool should reset counters, so the threshold is not reached.""" for _ in range(ToolUseCounter._GREP_USES_THRESHOLD - 1): - with patch("sys.stdin", _make_stdin(_base_input("grep_search"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() with patch("sys.stdin", _make_stdin(_base_input("mcp_serena_find_symbol"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() - with patch("sys.stdin", _make_stdin(_base_input("grep_search"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() assert capsys.readouterr().out == "" @@ -140,11 +157,11 @@ class TestPreToolUseRemindAboutSerenaHook: def test_counter_resets_after_deny(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): """After a deny is emitted, the counter is reset so the next burst starts fresh.""" for _ in range(ToolUseCounter._GREP_USES_THRESHOLD): - with patch("sys.stdin", _make_stdin(_base_input("grep_search"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() capsys.readouterr() - with patch("sys.stdin", _make_stdin(_base_input("grep_search"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() assert capsys.readouterr().out == "" @@ -168,7 +185,7 @@ class TestToolUseCounter: def test_update_resets_grep_outside_period(self): counter = ToolUseCounter() now = datetime.now() - counter.last_grep_use_timestamp = now - timedelta(seconds=ToolUseCounter._PERIOD_TO_RESET_COUNTERS_SECONDS + 1) + counter.last_grep_use_timestamp = now - timedelta(seconds=ToolUseCounter._GREP_RESET_PERIOD_SECONDS + 1) counter.n_recent_grep_uses = 2 hook = self._make_hook_stub("grep_search", now) @@ -191,7 +208,7 @@ class TestToolUseCounter: def test_update_resets_read_file_outside_period(self): counter = ToolUseCounter() now = datetime.now() - counter.last_read_file_use_timestamp = now - timedelta(seconds=ToolUseCounter._PERIOD_TO_RESET_COUNTERS_SECONDS + 1) + counter.last_read_file_use_timestamp = now - timedelta(seconds=ToolUseCounter._READ_FILE_RESET_PERIOD_SECONDS + 1) counter.n_recent_read_file_uses = 2 hook = self._make_hook_stub("read_file", now) @@ -246,13 +263,90 @@ class TestToolUseCounter: @staticmethod def _make_hook_stub(tool_name: str, timestamp: datetime) -> PreToolUseRemindAboutSerenaHook: - """Create a minimal stub that satisfies ToolUseCounter.update without reading stdin.""" + """Create a minimal stub that satisfies ToolUseCounter.update without reading stdin. + + Uses ``HookClient.VSCODE`` so that ``is_grep_tool`` / ``is_read_file_tool`` apply the + substring-matching branch — the counter tests below feed verbose tool names like + ``grep_search`` / ``read_file`` which are only recognized under the non-Claude-Code + branch (Claude Code uses exact names ``grep`` / ``read``). + """ stub = object.__new__(PreToolUseRemindAboutSerenaHook) stub._tool_name = tool_name.lower() + stub._client = HookClient.VSCODE stub.triggered_at_timestamp = timestamp return stub +class TestPreToolUseAutoApproveSerenaHook: + """Tests for the auto-approve hook that allows Serena tools while the client is in ``acceptEdits`` mode.""" + + @staticmethod + def _approve_input( + tool_name: str = "mcp__serena__find_symbol", + permission_mode: str | None = "acceptEdits", + session_id: str = "auto-approve-session", + permission_mode_key: str = "permission_mode", + ) -> dict: + data: dict = { + "session_id": session_id, + "tool_name": tool_name, + "tool_input": {}, + } + if permission_mode is not None: + data[permission_mode_key] = permission_mode + return data + + def test_approves_serena_tool_in_accept_edits_mode(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """When the tool is a Serena tool and the mode is ``acceptEdits``, an allow decision is emitted.""" + stdin_data = self._approve_input() + with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseAutoApproveSerenaHook(HookClient.CLAUDE_CODE).execute() + + output = capsys.readouterr().out.strip() + result = json.loads(output) + hook_output = result["hookSpecificOutput"] + assert hook_output["hookEventName"] == "PreToolUse" + assert hook_output["permissionDecision"] == "allow" + assert "acceptedits" in hook_output["permissionDecisionReason"].lower() + + def test_accepts_camel_case_permission_mode(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """The hook also reads the ``permissionMode`` (camelCase) variant of the field.""" + stdin_data = self._approve_input(permission_mode_key="permissionMode") + with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseAutoApproveSerenaHook(HookClient.CLAUDE_CODE).execute() + + output = capsys.readouterr().out.strip() + assert json.loads(output)["hookSpecificOutput"]["permissionDecision"] == "allow" + + def test_stays_silent_for_non_serena_tool(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """Non-Serena tools get no decision even in ``acceptEdits`` mode (the hook stays silent).""" + stdin_data = self._approve_input(tool_name="Grep") + with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseAutoApproveSerenaHook(HookClient.CLAUDE_CODE).execute() + assert capsys.readouterr().out == "" + + def test_stays_silent_in_default_mode(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """Serena tools in ``default`` mode get no decision (the hook stays silent).""" + stdin_data = self._approve_input(permission_mode="default") + with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseAutoApproveSerenaHook(HookClient.CLAUDE_CODE).execute() + assert capsys.readouterr().out == "" + + def test_stays_silent_in_plan_mode(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """Other permission modes (e.g. ``plan``) must not trigger an auto-approve.""" + stdin_data = self._approve_input(permission_mode="plan") + with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseAutoApproveSerenaHook(HookClient.CLAUDE_CODE).execute() + assert capsys.readouterr().out == "" + + def test_stays_silent_when_permission_mode_missing(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """If ``permission_mode`` is missing from the input, the hook stays silent rather than erroring.""" + stdin_data = self._approve_input(permission_mode=None) + with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseAutoApproveSerenaHook(HookClient.CLAUDE_CODE).execute() + assert capsys.readouterr().out == "" + + class TestSessionStartActivateProjectHook: def test_outputs_activation_message(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): stdin_data = {"session_id": "s1"} @@ -314,7 +408,7 @@ class TestHookCli: """Invoke the remind command enough times to trigger a deny.""" runner = CliRunner() for _ in range(ToolUseCounter._GREP_USES_THRESHOLD): - stdin_json = json.dumps({"session_id": "cli-remind", "tool_name": "grep_search", "tool_input": {}}) + stdin_json = json.dumps({"session_id": "cli-remind", "tool_name": "grep", "tool_input": {}}) with patch("serena.hooks.serena_home_dir", str(tmp_path)): result = runner.invoke(hook_commands, ["remind", "--client", "claude-code"], input=stdin_json) assert result.exit_code == 0 @@ -322,6 +416,39 @@ class TestHookCli: output = json.loads(result.output) assert output["hookSpecificOutput"]["permissionDecision"] == "deny" + def test_auto_approve_command(self, tmp_path: Path): + """The ``auto-approve`` CLI command emits an allow for a Serena tool in acceptEdits mode.""" + stdin_json = json.dumps( + { + "session_id": "cli-auto-approve", + "tool_name": "mcp__serena__find_symbol", + "tool_input": {}, + "permission_mode": "acceptEdits", + } + ) + runner = CliRunner() + with patch("serena.hooks.serena_home_dir", str(tmp_path)): + result = runner.invoke(hook_commands, ["auto-approve", "--client", "claude-code"], input=stdin_json) + assert result.exit_code == 0 + output = json.loads(result.output) + assert output["hookSpecificOutput"]["permissionDecision"] == "allow" + + def test_auto_approve_command_stays_silent_in_default_mode(self, tmp_path: Path): + """The ``auto-approve`` CLI command emits nothing when the mode is not ``acceptEdits``.""" + stdin_json = json.dumps( + { + "session_id": "cli-auto-approve-default", + "tool_name": "mcp__serena__find_symbol", + "tool_input": {}, + "permission_mode": "default", + } + ) + runner = CliRunner() + with patch("serena.hooks.serena_home_dir", str(tmp_path)): + result = runner.invoke(hook_commands, ["auto-approve", "--client", "claude-code"], input=stdin_json) + assert result.exit_code == 0 + assert result.output == "" + def test_client_default_is_claude_code(self, tmp_path: Path): """When --client is omitted, it defaults to claude-code.""" stdin_json = json.dumps({"session_id": "cli-default"}) From 1a4b1f61b2c7a9d286f7a12c8fdd32370fb28f54 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 11:03:45 +0200 Subject: [PATCH 087/190] Mentioned local installation in CONTRIBUTING.md --- CONTRIBUTING.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a52b439c..77aa7c0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,14 @@ You can install a virtual environment with the required as follows * On Windows outside of Git Bash: `.venv\Scripts\activate.bat` (in cmd/ps) or `source .venv/Scripts/activate` (in git-bash) 3. Install the required packages with all extras: `uv sync --extra dev` +## Local Installation as Tool + +To install Serena as a local tool, run + +```shell +uv tool install --reinstall -p 3.13 . +``` + ## Poe Tasks We use poe to execute development tasks: From 0c945e08a475c320578b420991260321d8fc992d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 11:04:51 +0200 Subject: [PATCH 088/190] Major enhancement in client connection documentation Added new contexts and extended some older ones to exclude more basic tool --- docs/02-usage/030_clients.md | 375 ++++++++++++------ .../resources/config/contexts/antigravity.yml | 26 ++ .../resources/config/contexts/claude-code.yml | 2 + .../resources/config/contexts/codex.yml | 2 + .../resources/config/contexts/copilot-cli.yml | 25 ++ src/serena/resources/config/contexts/ide.yml | 2 + .../config/contexts/jb-ai-assistant.yml | 26 ++ .../resources/config/contexts/junie.yml | 24 ++ .../resources/config/contexts/vscode.yml | 29 ++ 9 files changed, 394 insertions(+), 117 deletions(-) create mode 100644 src/serena/resources/config/contexts/antigravity.yml create mode 100644 src/serena/resources/config/contexts/copilot-cli.yml create mode 100644 src/serena/resources/config/contexts/jb-ai-assistant.yml create mode 100644 src/serena/resources/config/contexts/junie.yml create mode 100644 src/serena/resources/config/contexts/vscode.yml diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index 05610861..be2a8021 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -3,13 +3,6 @@ In the following, we provide general instructions on how to connect Serena to your MCP-enabled client, as well as specific instructions for popular clients. -:::{note} -The configurations we provide for particular clients below will run the latest version of Serena -using the `stdio` protocol with `uvx`. -Adapt the commands to your preferred way of [running Serena](020_running), adding any additional -command-line arguments as needed. -::: - (clients-general-instructions)= ## General Instructions @@ -47,14 +40,9 @@ A key mechanism for this is to use the appropriate [context](#contexts) when sta (clients-common-pitfalls)= ### Common Pitfalls -**Escaping Paths Correctly**. -Note that if your client configuration uses JSON, special characters (like backslashes) need to be escaped properly. -In particular, if you are specifying paths containing backslashes on Windows -(note that you can also just use forward slashes), be sure to escape them correctly (`\\`). - -**Discoverability of `uvx`**. -Your client may not find the `uvx` command, even if it is on your system PATH. -In this case, a workaround is to provide the full path to the `uvx` executable. +**Discoverability of the `serena`**. +Your client may not find the `serena` CLI command, even if it is on your system PATH. +In this case, a workaround is to provide the full path to the `serena` executable. **Environment Variables**. Some language servers may require additional environment variables to be set (e.g. F# on macOS with Homebrew), @@ -71,42 +59,63 @@ object, e.g. } ``` +## Copilot in JetBrains + +Open the settings of your JetBrains IDE and go to Tools / GitHub Copilot / Model Context Protocol (MCP). Then click +on the Configure button. This will open your global `mcp.json` file, where you can add the following entry for Serena: + +```json +{ + "servers": { + "serena": { + "type": "stdio", + "command": "serena", + "args": [ + "start-mcp-server", + "--context=jb-copilot-plugin" + ] + } + } +} +``` + +**Verification.** +Open Copilot, switch to Agent mode, and click on the configure tools button. You should see Serena's tools in the list and be able to start +the Serena server there (you do not generally have to start Serena in the future, Copilot will start the server by itself). If the server is shown as running, Copilot is successfully connected to Serena. Most models will understand how to use Serena's tools out of the box, but for some models you may have to prompt "Activate the current project with Serena and read initial instructions" in the beginning of the chat. + +**Recommended Configuration**. +The `jb-copilot-plugin` context (see above) comes with our recommended subset of Serena's tools for Copilot in JetBrains IDEs. We also +recommend *disabling* the following built-in tools for optimal performance: +replace_string_in_file, apply_patch, list_dir, file_search, grep_search. Note that running subagents may not use MCP servers, consider deactivating the run_subagent tool as well. + +Serena offers better alternatives to these basic tools. If you do prefer to use the built-in tools instead, +you should disable corresponding Serena tools instead to prevent context bloat. + +We also recommend marking Serena's tools as approved so you don't have to manually approve them in agent sessions. +You can do this in Tools / GitHub Copilot / Chat, where at the bottom you can click on the Configure button for MCP tool auto-approval. + ## Claude Code Serena is a great way to make Claude Code both cheaper and more powerful! - -:::{note} -Serena might take some time to start up, especially on the first run. -To make sure that enough time is available for the server to start, -set `MCP_TIMEOUT` to a sufficiently high value (e.g. by adding `export MCP_TIMEOUT=60000` to your shell profile) -before starting Claude Code. - -Confirm that Claude Code is connected to Serena by running the `/mcp` command and by reconnecting, if necessary. -::: +To add Serena to Claude Code, you can simply run `serena setup claude-code`. Alternatively, follow the instructions below. **Global Configuration**. To add the Serena MCP server for all your projects, use the user-level configuration of claude code and the `--project-from-cwd` flag: ```shell -claude mcp add --scope user serena -- uvx --python 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --context=claude-code --project-from-cwd +claude mcp add --scope user serena -- serena start-mcp-server --context claude-code --project-from-cwd ``` **Per-Project Configuration.** Alternatively, to add Serena only for the current project in the current directory, use the command: ```shell -claude mcp add serena -- uvx --python 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --context claude-code --project "$(pwd)" +claude mcp add serena -- serena start-mcp-server --context claude-code --project "$(pwd)" ``` -Note: - * We use the `claude-code` context to disable unnecessary tools (avoiding duplication - with Claude Code's built-in capabilities). - * We specify the current directory as the project directory with `--project "$(pwd)"`, such - that Serena is configured to work on the current project from the get-go, following - Claude Code's mode of operation. - -Whenever you start Claude Code, Serena will search up from the current directory for `.serena/project.yml` or `.git` markers, -activating the containing directory as the project (if any). -This mechanism makes it suitable for a single global MCP configuration. +**Verification.** +Confirm that Claude Code is connected to Serena by running the `/mcp` command and by reconnecting, if necessary. +If Serena fails to start fast enough, you should set `MCP_TIMEOUT` to a sufficiently high value +(e.g. by adding `export MCP_TIMEOUT=60000` to your shell profile) **Maximum Token Efficiency.** To maximize token efficiency, you may want to use Claude Code's *on-demand tool loading* feature, which is supported since at least v2.0.74 of Claude Code. @@ -124,64 +133,173 @@ set ENABLE_TOOL_SEARCH=true && claude ``` in Windows CMD to launch Claude Code. -## VSCode +**Hooks.** +Claude Code's agent can sometimes drift away from using MCP tools during long sessions, falling back +to built-in tools or forgetting to load Serena's instructions. Serena provides hook commands that +automatically remind the agent to use Serena's tools when appropriate. -While serena can be directly installed from the GitHub MCP server registry, we recommend to set it up manually -(at least for now, until the configuration there has been improved). Just paste the following into -`/.vscode/mcp.json`, or edit the entry after using the option `install into workspace`: +:::{note} +Hooks are an **alpha feature** that we provide to overcome context drift in several agent harnesses +(where agents, especially those powered by Anthropic models, may forget to load and use MCP tools or +fail to read MCP instructions). +::: + +To set up hooks, add the following to your Claude Code settings file +(`.claude/settings.json` in your project directory, or `~/.claude/settings.json` globally): + +All hooks below are opt-in — include only the ones you want. Add the following to your +Claude Code settings file (`.claude/settings.json` in your project directory, or +`~/.claude/settings.json` globally): ```json { - "servers": { - "oraios/serena": { - "type": "stdio", - "command": "uvx", - "args": [ - "-p", - "3.13", - "--from", - "git+https://github.com/oraios/serena", - "serena", - "start-mcp-server", - "--context", - "ide", - "--project", - "${workspaceFolder}" - ] + "hooks": { + "PreToolUse": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "serena-hooks remind --client=claude-code" + } + ] + }, + { + "matcher": "mcp__serena__*", + "hooks": [ + { + "type": "command", + "command": "serena-hooks auto-approve --client=claude-code" + } + ] + } + ], + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "serena-hooks activate --client=claude-code" + } + ] + } + ], + "Stop": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "serena-hooks cleanup --client=claude-code" + } + ] + } + ] } - }, - "inputs": [] } ``` -## Codex +The hooks will: -Serena works with OpenAI's Codex CLI and app out of the box, but you have to use the `codex` context for it to work properly. (The technical reason is that Codex doesn't fully support the MCP specifications, so some massaging of tools is required.). +- **`remind`**: Nudge the agent to use Serena's symbolic tools when it makes too many consecutive + `grep` or `read_file` calls without using any Serena tools in between. +- **`activate`**: Prompt the agent to activate the project and read Serena's instructions at session start. +- **`cleanup`**: Clean up hook session data when the session ends. +- **`auto-approve`**: Auto-approve Serena tool calls whenever Claude Code is in `acceptEdits` mode, + so blanket edit approvals cover Serena's destructive tools (e.g. `replace_symbol_body`, + `rename_symbol`) instead of prompting on every call. -Add a [run command](020_running) to `~/.codex/config.toml` to configure Serena for all Codex sessions; -create the file if it does not exist. -For example, when using `uvx`, add the following section: +For more details on Claude Code's hook system, see the +[Claude Code hooks documentation](https://code.claude.com/docs/en/hooks). + +## VSCode + +You can add Serena to VSCode by running the MCP: Add Server command. +In that dialogue, select the Command (stdio) option. You can decide between installing it globally +or in the workspace (only for the currently open project), and the command you should enter depends on that choice. +(You will be asked to choose after entering the mcp run command.) + +**Global.** (Recommended) +Enter `serena start-mcp-server --context=vscode`. Unfortunately, due to a [bug in VSCode](https://github.com/microsoft/vscode/issues/245905), +in this setting Serena won't be able to activate the project automatically. You will have to remember to prompt +"Activate the current dir as project using serena" at the start of each session. + +**Workspace.** +Enter `serena start-mcp-server --context=vscode --project ${workspaceFolder}`. This will allow Serena to automatically activate the project, +with the downside that you will have to add Serena to each project you want to use it in. + +In both cases, proceed to enter Serena as the name, then select either global or workspace. + +**Verification.** +You should be able to see Serena in the tools overview in the AI Chat window. + +**Hooks.** +VSCode's Copilot agent can sometimes over-rely on basic tools like `read_file` and `grep_search` instead of +using Serena's more efficient symbolic tools. You can use Serena's hook commands to automatically correct +for this — the hooks will simply remind the agent to use Serena's tools when appropriate. + +:::{note} +Hooks are an **alpha feature** that we provide to overcome context drift in several agent harnesses +(where agents, especially those powered by Anthropic models, may forget to load and use MCP tools or +fail to read MCP instructions). +::: + +The hooks will: + +- **`remind`**: Nudge the agent to use Serena's symbolic tools when it makes too many consecutive + `grep` or `read_file` calls without using any Serena tools in between. +- **`activate`**: Prompt the agent to activate the project and read Serena's instructions at session start. +- **`cleanup`**: Clean up hook session data when the session ends. + +To set this up, create the file `~/.copilot/hooks/serena-hooks.json` with the following content: + +```json +{ + "hooks": { + "PreToolUse": [ + { + "type": "command", + "command": "serena-hooks remind --client=vscode" + } + ], + "SessionStart": [ + { + "type": "command", + "command": "serena-hooks activate --client=vscode" + } + ], + "Stop": [ + { + "type": "command", + "command": "serena-hooks cleanup --client=vscode" + } + ] + } +} +``` + +The `SessionStart` hook also addresses the global configuration limitation mentioned above — it will +automatically prompt the agent to activate the project directory, so you no longer need to do this manually. + +## Codex (CLI and App) + +You can simply run `serena setup codex`. + +Alternatively, you can manually add the following to `~/.codex/config.toml` (create the file if it does not exist): ```toml [mcp_servers.serena] -startup_timeout_sec = 25 -command = "uvx" -args = ["-p", "3.13", "--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server", "--project-from-cwd", "--context", "codex"] +startup_timeout_sec = 15 +command = "serena" +args = ["start-mcp-server", "--project-from-cwd", "--context=codex"] ``` -The larger startup timeout is to permit uvx to download the necessary dependencies. Once downloaded, the startup time is much faster. - -:::{note} -The codex app does not start a session in the project's directory, so when using the app, you will have to -ask Codex to "Activate the current dir as project using serena" at the start of each session. -This is not necessary when using the codex CLI. -::: - -:::{attention} -Codex currently ignores the [instructions](https://blog.modelcontextprotocol.io/posts/2025-11-03-using-server-instructions/) -property of MCP servers, so it is recommended to prompt the agent to -"read initial instructions for Serena" when starting your session, e.g. by mentioning this in your `AGENT.md`. -::: +**Verification.** +Run the `/mcp` command and verify that Serena is connected. +The Codex app does not start a session in the project's directory, so when using the app, we recommend +asking Codex to "Activate the current dir as project using serena" at the start of each session (though Codex might +do this automatically). ## Claude Desktop @@ -197,14 +315,10 @@ Add the `serena` MCP server configuration { "mcpServers": { "serena": { - "command": "uvx", + "command": "serena", "args": [ - "-p", - "3.13", - "--from", - "git+https://github.com/oraios/serena", - "serena", - "start-mcp-server" + "start-mcp-server", + "--context=claude-desktop" ] } } @@ -214,6 +328,7 @@ Add the `serena` MCP server configuration If your language server requires specific environment variables to be set (e.g. F# on macOS with Homebrew), you can add them via an `env` key (see [above](#clients-common-pitfalls)). +**Verification.** Once you have created the new MCP server entry, save the config and then restart Claude Desktop. :::{attention} @@ -223,28 +338,49 @@ minimize it. After restarting, you should see Serena's tools in your chat interface (notice the small hammer icon). -For more information on MCP servers with Claude Desktop, -see [the official quick start guide](https://modelcontextprotocol.io/quickstart/user). +## Copilot CLI -## JetBrains Junie +In the interactive mode, you can call `/mcp add` from within the copilot CLI. There, use serena as name, +STDIO as the server type, and `serena start-mcp-server --context=copilot-cli --project-from-cwd` as command. -Open Junie, go to the three dots in the top right corner, then Settings / MCP Settings and add Serena to Junie's global -MCP server configuration: +Alternatively, add the following to `~/.copilot/mcp-config.json` (create the file if it does not exist): ```json { "mcpServers": { "serena": { - "command": "uvx", + "type": "stdio", + "command": "serena", + "tools": [ + "*" + ], "args": [ - "-p", - "3.13", - "--from", - "git+https://github.com/oraios/serena", - "serena", "start-mcp-server", - "--context", - "ide" + "--context=copilot-cli", + "--project-from-cwd" + ] + } + } +} +``` + +**Verification.** +Copilot should now show that Serena is running, though you may have to restart it. + + +## JetBrains Junie + +For the Junie plugin in JetBrains IDEs, go to the three dots in the top right corner, +then Settings / MCP Settings and add Serena to Junie's global MCP server configuration: + +```json +{ + "mcpServers": { + "serena": { + "command": "serena", + "args": [ + "start-mcp-server", + "--context=junie" ] } } @@ -254,6 +390,23 @@ MCP server configuration: You will have to prompt Junie to "Activate the current project using serena's activation tool" at the start of each session. +For Junie CLI, where the project can be activated automatically on startup, edit `~/.junie/mcp/mcp.json` and add + +```json +{ + "mcpServers": { + "serena": { + "command": "serena", + "args": [ + "start-mcp-server", + "--context=junie", + "--project-from-cwd" + ] + } + } +} +``` + ## JetBrains AI Assistant Here you can set up the more convenient per-project MCP server configuration, as the AI assistant supports specifying @@ -265,16 +418,10 @@ Go to Settings / Tools / AI Assistant / MCP and add a new **local** configuratio { "mcpServers": { "serena": { - "command": "uvx", + "command": "serena", "args": [ - "-p", - "3.13", - "--from", - "git+https://github.com/oraios/serena", - "serena", "start-mcp-server", - "--context", - "ide", + "--context=jb-ai-assistant", "--project", "$(pwd)" ] @@ -293,16 +440,10 @@ Add this configuration: { "mcpServers": { "serena": { - "command": "uvx", + "command": "serena", "args": [ - "-p", - "3.13", - "--from", - "git+https://github.com/oraios/serena", - "serena", "start-mcp-server", - "--context", - "ide" + "--context=antigravity" ] } } @@ -340,7 +481,7 @@ in order to reduce tool duplication. Most of the popular existing coding assistants (e.g. IDE extensions) and AI-enabled IDEs themselves support connections to MCP Servers. Serena generally boosts performance by providing efficient tools for symbolic operations. -We generally recommend to use the `ide` context for these integrations by adding the arguments `--context ide` +We generally recommend using the `ide` context for these integrations by adding the arguments `--context ide` in order to reduce tool duplication. ### Local GUIs and Agent Frameworks @@ -354,5 +495,5 @@ Some of the leading open source GUI applications are * [OpenWebUI](https://docs.openwebui.com/openapi-servers/mcp) and * [Agno](https://docs.agno.com/introduction/playground). -These applications allow to combine Serena with almost any LLM (including locally running ones) +These applications allow combining Serena with almost any LLM (including locally running ones) and offer various other integrations. diff --git a/src/serena/resources/config/contexts/antigravity.yml b/src/serena/resources/config/contexts/antigravity.yml new file mode 100644 index 00000000..33411ac2 --- /dev/null +++ b/src/serena/resources/config/contexts/antigravity.yml @@ -0,0 +1,26 @@ +description: Generic IDE coding agent context (basic file operations and shell operations assumed to be covered; single project mode) +prompt: | + You are running in an IDE assistant context where file operations, basic (line-based) edits and reads, + and shell commands are handled by your own, internal tools. + + If Serena's tools can be used to achieve your task, you should prioritize them. + In particular, it is important that you avoid reading entire source code files unless it is strictly necessary! + Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools. + For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool. + +excluded_tools: + - create_text_file + - read_file + - execute_shell_command + - find_file + - list_dir + +tool_description_overrides: {} + +# whether to assume that Serena shall only work on a single project in this context (provided that a project is given +# when Serena is started). +# If set to true and a project is provided at startup, the set of tools is limited to those required by the project's +# concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal. +# Tools explicitly disabled by the project will not be available at all. +# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed. +single_project: true diff --git a/src/serena/resources/config/contexts/claude-code.yml b/src/serena/resources/config/contexts/claude-code.yml index c7da899e..2692d3de 100644 --- a/src/serena/resources/config/contexts/claude-code.yml +++ b/src/serena/resources/config/contexts/claude-code.yml @@ -13,6 +13,8 @@ excluded_tools: - read_file - execute_shell_command - replace_content + - find_file + - list_dir tool_description_overrides: {} diff --git a/src/serena/resources/config/contexts/codex.yml b/src/serena/resources/config/contexts/codex.yml index ccad93b5..2dbd870a 100644 --- a/src/serena/resources/config/contexts/codex.yml +++ b/src/serena/resources/config/contexts/codex.yml @@ -14,6 +14,8 @@ excluded_tools: - read_file - execute_shell_command - replace_content + - find_file + - list_dir tool_description_overrides: {} diff --git a/src/serena/resources/config/contexts/copilot-cli.yml b/src/serena/resources/config/contexts/copilot-cli.yml new file mode 100644 index 00000000..c7da899e --- /dev/null +++ b/src/serena/resources/config/contexts/copilot-cli.yml @@ -0,0 +1,25 @@ +description: Claude Code (CLI agent where file operations, basic edits, etc. are already covered; single project mode) +prompt: | + You are running in a CLI coding agent context where file operations, basic (line-based) edits and reads + as well as shell commands are handled by your own, internal tools. + + If Serena's tools can be used to achieve your task, you should prioritize them. + In particular, it is important that you avoid reading entire source code files unless it is strictly necessary! + Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools. + For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool. + +excluded_tools: + - create_text_file + - read_file + - execute_shell_command + - replace_content + +tool_description_overrides: {} + +# whether to assume that Serena shall only work on a single project in this context (provided that a project is given +# when Serena is started). +# If set to true and a project is provided at startup, the set of tools is limited to those required by the project's +# concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal. +# Tools explicitly disabled by the project will not be available at all. +# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed. +single_project: true diff --git a/src/serena/resources/config/contexts/ide.yml b/src/serena/resources/config/contexts/ide.yml index e8806f4e..33411ac2 100644 --- a/src/serena/resources/config/contexts/ide.yml +++ b/src/serena/resources/config/contexts/ide.yml @@ -12,6 +12,8 @@ excluded_tools: - create_text_file - read_file - execute_shell_command + - find_file + - list_dir tool_description_overrides: {} diff --git a/src/serena/resources/config/contexts/jb-ai-assistant.yml b/src/serena/resources/config/contexts/jb-ai-assistant.yml new file mode 100644 index 00000000..33411ac2 --- /dev/null +++ b/src/serena/resources/config/contexts/jb-ai-assistant.yml @@ -0,0 +1,26 @@ +description: Generic IDE coding agent context (basic file operations and shell operations assumed to be covered; single project mode) +prompt: | + You are running in an IDE assistant context where file operations, basic (line-based) edits and reads, + and shell commands are handled by your own, internal tools. + + If Serena's tools can be used to achieve your task, you should prioritize them. + In particular, it is important that you avoid reading entire source code files unless it is strictly necessary! + Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools. + For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool. + +excluded_tools: + - create_text_file + - read_file + - execute_shell_command + - find_file + - list_dir + +tool_description_overrides: {} + +# whether to assume that Serena shall only work on a single project in this context (provided that a project is given +# when Serena is started). +# If set to true and a project is provided at startup, the set of tools is limited to those required by the project's +# concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal. +# Tools explicitly disabled by the project will not be available at all. +# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed. +single_project: true diff --git a/src/serena/resources/config/contexts/junie.yml b/src/serena/resources/config/contexts/junie.yml new file mode 100644 index 00000000..e8806f4e --- /dev/null +++ b/src/serena/resources/config/contexts/junie.yml @@ -0,0 +1,24 @@ +description: Generic IDE coding agent context (basic file operations and shell operations assumed to be covered; single project mode) +prompt: | + You are running in an IDE assistant context where file operations, basic (line-based) edits and reads, + and shell commands are handled by your own, internal tools. + + If Serena's tools can be used to achieve your task, you should prioritize them. + In particular, it is important that you avoid reading entire source code files unless it is strictly necessary! + Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools. + For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool. + +excluded_tools: + - create_text_file + - read_file + - execute_shell_command + +tool_description_overrides: {} + +# whether to assume that Serena shall only work on a single project in this context (provided that a project is given +# when Serena is started). +# If set to true and a project is provided at startup, the set of tools is limited to those required by the project's +# concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal. +# Tools explicitly disabled by the project will not be available at all. +# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed. +single_project: true diff --git a/src/serena/resources/config/contexts/vscode.yml b/src/serena/resources/config/contexts/vscode.yml new file mode 100644 index 00000000..fd74fd4d --- /dev/null +++ b/src/serena/resources/config/contexts/vscode.yml @@ -0,0 +1,29 @@ +description: Generic IDE coding agent context (basic file operations and shell operations assumed to be covered; single project mode) +prompt: | + In addition to Serena's tools, you have access to internal tools, including for, basic (line-based) edits and reads, + and shell commands. + + If Serena's tools can be used to achieve your task, you should prioritize them. + In particular, it is important that you avoid reading entire source code files unless it is strictly necessary! + Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools. + For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool. + + Make sure to first activate the working directory as using Serena's `activate_project` tool and to read initial + instructions (if not done so already) with the `initial_instructions` tool. + +excluded_tools: + - create_text_file + - read_file + - execute_shell_command + - find_file + - list_dir + +tool_description_overrides: {} + +# whether to assume that Serena shall only work on a single project in this context (provided that a project is given +# when Serena is started). +# If set to true and a project is provided at startup, the set of tools is limited to those required by the project's +# concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal. +# Tools explicitly disabled by the project will not be available at all. +# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed. +single_project: true From bbe499354015c257a9991006107a7cef41284272 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 11:04:51 +0200 Subject: [PATCH 089/190] Major enhancement in client connection documentation Added new contexts and extended some older ones to exclude more basic tool --- docs/02-usage/030_clients.md | 375 ++++++++++++------ .../resources/config/contexts/antigravity.yml | 26 ++ .../resources/config/contexts/claude-code.yml | 11 +- .../resources/config/contexts/codex.yml | 2 + .../resources/config/contexts/copilot-cli.yml | 25 ++ src/serena/resources/config/contexts/ide.yml | 2 + .../config/contexts/jb-ai-assistant.yml | 26 ++ .../resources/config/contexts/junie.yml | 24 ++ .../resources/config/contexts/vscode.yml | 29 ++ 9 files changed, 400 insertions(+), 120 deletions(-) create mode 100644 src/serena/resources/config/contexts/antigravity.yml create mode 100644 src/serena/resources/config/contexts/copilot-cli.yml create mode 100644 src/serena/resources/config/contexts/jb-ai-assistant.yml create mode 100644 src/serena/resources/config/contexts/junie.yml create mode 100644 src/serena/resources/config/contexts/vscode.yml diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index 05610861..be2a8021 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -3,13 +3,6 @@ In the following, we provide general instructions on how to connect Serena to your MCP-enabled client, as well as specific instructions for popular clients. -:::{note} -The configurations we provide for particular clients below will run the latest version of Serena -using the `stdio` protocol with `uvx`. -Adapt the commands to your preferred way of [running Serena](020_running), adding any additional -command-line arguments as needed. -::: - (clients-general-instructions)= ## General Instructions @@ -47,14 +40,9 @@ A key mechanism for this is to use the appropriate [context](#contexts) when sta (clients-common-pitfalls)= ### Common Pitfalls -**Escaping Paths Correctly**. -Note that if your client configuration uses JSON, special characters (like backslashes) need to be escaped properly. -In particular, if you are specifying paths containing backslashes on Windows -(note that you can also just use forward slashes), be sure to escape them correctly (`\\`). - -**Discoverability of `uvx`**. -Your client may not find the `uvx` command, even if it is on your system PATH. -In this case, a workaround is to provide the full path to the `uvx` executable. +**Discoverability of the `serena`**. +Your client may not find the `serena` CLI command, even if it is on your system PATH. +In this case, a workaround is to provide the full path to the `serena` executable. **Environment Variables**. Some language servers may require additional environment variables to be set (e.g. F# on macOS with Homebrew), @@ -71,42 +59,63 @@ object, e.g. } ``` +## Copilot in JetBrains + +Open the settings of your JetBrains IDE and go to Tools / GitHub Copilot / Model Context Protocol (MCP). Then click +on the Configure button. This will open your global `mcp.json` file, where you can add the following entry for Serena: + +```json +{ + "servers": { + "serena": { + "type": "stdio", + "command": "serena", + "args": [ + "start-mcp-server", + "--context=jb-copilot-plugin" + ] + } + } +} +``` + +**Verification.** +Open Copilot, switch to Agent mode, and click on the configure tools button. You should see Serena's tools in the list and be able to start +the Serena server there (you do not generally have to start Serena in the future, Copilot will start the server by itself). If the server is shown as running, Copilot is successfully connected to Serena. Most models will understand how to use Serena's tools out of the box, but for some models you may have to prompt "Activate the current project with Serena and read initial instructions" in the beginning of the chat. + +**Recommended Configuration**. +The `jb-copilot-plugin` context (see above) comes with our recommended subset of Serena's tools for Copilot in JetBrains IDEs. We also +recommend *disabling* the following built-in tools for optimal performance: +replace_string_in_file, apply_patch, list_dir, file_search, grep_search. Note that running subagents may not use MCP servers, consider deactivating the run_subagent tool as well. + +Serena offers better alternatives to these basic tools. If you do prefer to use the built-in tools instead, +you should disable corresponding Serena tools instead to prevent context bloat. + +We also recommend marking Serena's tools as approved so you don't have to manually approve them in agent sessions. +You can do this in Tools / GitHub Copilot / Chat, where at the bottom you can click on the Configure button for MCP tool auto-approval. + ## Claude Code Serena is a great way to make Claude Code both cheaper and more powerful! - -:::{note} -Serena might take some time to start up, especially on the first run. -To make sure that enough time is available for the server to start, -set `MCP_TIMEOUT` to a sufficiently high value (e.g. by adding `export MCP_TIMEOUT=60000` to your shell profile) -before starting Claude Code. - -Confirm that Claude Code is connected to Serena by running the `/mcp` command and by reconnecting, if necessary. -::: +To add Serena to Claude Code, you can simply run `serena setup claude-code`. Alternatively, follow the instructions below. **Global Configuration**. To add the Serena MCP server for all your projects, use the user-level configuration of claude code and the `--project-from-cwd` flag: ```shell -claude mcp add --scope user serena -- uvx --python 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --context=claude-code --project-from-cwd +claude mcp add --scope user serena -- serena start-mcp-server --context claude-code --project-from-cwd ``` **Per-Project Configuration.** Alternatively, to add Serena only for the current project in the current directory, use the command: ```shell -claude mcp add serena -- uvx --python 3.13 --from git+https://github.com/oraios/serena serena start-mcp-server --context claude-code --project "$(pwd)" +claude mcp add serena -- serena start-mcp-server --context claude-code --project "$(pwd)" ``` -Note: - * We use the `claude-code` context to disable unnecessary tools (avoiding duplication - with Claude Code's built-in capabilities). - * We specify the current directory as the project directory with `--project "$(pwd)"`, such - that Serena is configured to work on the current project from the get-go, following - Claude Code's mode of operation. - -Whenever you start Claude Code, Serena will search up from the current directory for `.serena/project.yml` or `.git` markers, -activating the containing directory as the project (if any). -This mechanism makes it suitable for a single global MCP configuration. +**Verification.** +Confirm that Claude Code is connected to Serena by running the `/mcp` command and by reconnecting, if necessary. +If Serena fails to start fast enough, you should set `MCP_TIMEOUT` to a sufficiently high value +(e.g. by adding `export MCP_TIMEOUT=60000` to your shell profile) **Maximum Token Efficiency.** To maximize token efficiency, you may want to use Claude Code's *on-demand tool loading* feature, which is supported since at least v2.0.74 of Claude Code. @@ -124,64 +133,173 @@ set ENABLE_TOOL_SEARCH=true && claude ``` in Windows CMD to launch Claude Code. -## VSCode +**Hooks.** +Claude Code's agent can sometimes drift away from using MCP tools during long sessions, falling back +to built-in tools or forgetting to load Serena's instructions. Serena provides hook commands that +automatically remind the agent to use Serena's tools when appropriate. -While serena can be directly installed from the GitHub MCP server registry, we recommend to set it up manually -(at least for now, until the configuration there has been improved). Just paste the following into -`/.vscode/mcp.json`, or edit the entry after using the option `install into workspace`: +:::{note} +Hooks are an **alpha feature** that we provide to overcome context drift in several agent harnesses +(where agents, especially those powered by Anthropic models, may forget to load and use MCP tools or +fail to read MCP instructions). +::: + +To set up hooks, add the following to your Claude Code settings file +(`.claude/settings.json` in your project directory, or `~/.claude/settings.json` globally): + +All hooks below are opt-in — include only the ones you want. Add the following to your +Claude Code settings file (`.claude/settings.json` in your project directory, or +`~/.claude/settings.json` globally): ```json { - "servers": { - "oraios/serena": { - "type": "stdio", - "command": "uvx", - "args": [ - "-p", - "3.13", - "--from", - "git+https://github.com/oraios/serena", - "serena", - "start-mcp-server", - "--context", - "ide", - "--project", - "${workspaceFolder}" - ] + "hooks": { + "PreToolUse": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "serena-hooks remind --client=claude-code" + } + ] + }, + { + "matcher": "mcp__serena__*", + "hooks": [ + { + "type": "command", + "command": "serena-hooks auto-approve --client=claude-code" + } + ] + } + ], + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "serena-hooks activate --client=claude-code" + } + ] + } + ], + "Stop": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "serena-hooks cleanup --client=claude-code" + } + ] + } + ] } - }, - "inputs": [] } ``` -## Codex +The hooks will: -Serena works with OpenAI's Codex CLI and app out of the box, but you have to use the `codex` context for it to work properly. (The technical reason is that Codex doesn't fully support the MCP specifications, so some massaging of tools is required.). +- **`remind`**: Nudge the agent to use Serena's symbolic tools when it makes too many consecutive + `grep` or `read_file` calls without using any Serena tools in between. +- **`activate`**: Prompt the agent to activate the project and read Serena's instructions at session start. +- **`cleanup`**: Clean up hook session data when the session ends. +- **`auto-approve`**: Auto-approve Serena tool calls whenever Claude Code is in `acceptEdits` mode, + so blanket edit approvals cover Serena's destructive tools (e.g. `replace_symbol_body`, + `rename_symbol`) instead of prompting on every call. -Add a [run command](020_running) to `~/.codex/config.toml` to configure Serena for all Codex sessions; -create the file if it does not exist. -For example, when using `uvx`, add the following section: +For more details on Claude Code's hook system, see the +[Claude Code hooks documentation](https://code.claude.com/docs/en/hooks). + +## VSCode + +You can add Serena to VSCode by running the MCP: Add Server command. +In that dialogue, select the Command (stdio) option. You can decide between installing it globally +or in the workspace (only for the currently open project), and the command you should enter depends on that choice. +(You will be asked to choose after entering the mcp run command.) + +**Global.** (Recommended) +Enter `serena start-mcp-server --context=vscode`. Unfortunately, due to a [bug in VSCode](https://github.com/microsoft/vscode/issues/245905), +in this setting Serena won't be able to activate the project automatically. You will have to remember to prompt +"Activate the current dir as project using serena" at the start of each session. + +**Workspace.** +Enter `serena start-mcp-server --context=vscode --project ${workspaceFolder}`. This will allow Serena to automatically activate the project, +with the downside that you will have to add Serena to each project you want to use it in. + +In both cases, proceed to enter Serena as the name, then select either global or workspace. + +**Verification.** +You should be able to see Serena in the tools overview in the AI Chat window. + +**Hooks.** +VSCode's Copilot agent can sometimes over-rely on basic tools like `read_file` and `grep_search` instead of +using Serena's more efficient symbolic tools. You can use Serena's hook commands to automatically correct +for this — the hooks will simply remind the agent to use Serena's tools when appropriate. + +:::{note} +Hooks are an **alpha feature** that we provide to overcome context drift in several agent harnesses +(where agents, especially those powered by Anthropic models, may forget to load and use MCP tools or +fail to read MCP instructions). +::: + +The hooks will: + +- **`remind`**: Nudge the agent to use Serena's symbolic tools when it makes too many consecutive + `grep` or `read_file` calls without using any Serena tools in between. +- **`activate`**: Prompt the agent to activate the project and read Serena's instructions at session start. +- **`cleanup`**: Clean up hook session data when the session ends. + +To set this up, create the file `~/.copilot/hooks/serena-hooks.json` with the following content: + +```json +{ + "hooks": { + "PreToolUse": [ + { + "type": "command", + "command": "serena-hooks remind --client=vscode" + } + ], + "SessionStart": [ + { + "type": "command", + "command": "serena-hooks activate --client=vscode" + } + ], + "Stop": [ + { + "type": "command", + "command": "serena-hooks cleanup --client=vscode" + } + ] + } +} +``` + +The `SessionStart` hook also addresses the global configuration limitation mentioned above — it will +automatically prompt the agent to activate the project directory, so you no longer need to do this manually. + +## Codex (CLI and App) + +You can simply run `serena setup codex`. + +Alternatively, you can manually add the following to `~/.codex/config.toml` (create the file if it does not exist): ```toml [mcp_servers.serena] -startup_timeout_sec = 25 -command = "uvx" -args = ["-p", "3.13", "--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server", "--project-from-cwd", "--context", "codex"] +startup_timeout_sec = 15 +command = "serena" +args = ["start-mcp-server", "--project-from-cwd", "--context=codex"] ``` -The larger startup timeout is to permit uvx to download the necessary dependencies. Once downloaded, the startup time is much faster. - -:::{note} -The codex app does not start a session in the project's directory, so when using the app, you will have to -ask Codex to "Activate the current dir as project using serena" at the start of each session. -This is not necessary when using the codex CLI. -::: - -:::{attention} -Codex currently ignores the [instructions](https://blog.modelcontextprotocol.io/posts/2025-11-03-using-server-instructions/) -property of MCP servers, so it is recommended to prompt the agent to -"read initial instructions for Serena" when starting your session, e.g. by mentioning this in your `AGENT.md`. -::: +**Verification.** +Run the `/mcp` command and verify that Serena is connected. +The Codex app does not start a session in the project's directory, so when using the app, we recommend +asking Codex to "Activate the current dir as project using serena" at the start of each session (though Codex might +do this automatically). ## Claude Desktop @@ -197,14 +315,10 @@ Add the `serena` MCP server configuration { "mcpServers": { "serena": { - "command": "uvx", + "command": "serena", "args": [ - "-p", - "3.13", - "--from", - "git+https://github.com/oraios/serena", - "serena", - "start-mcp-server" + "start-mcp-server", + "--context=claude-desktop" ] } } @@ -214,6 +328,7 @@ Add the `serena` MCP server configuration If your language server requires specific environment variables to be set (e.g. F# on macOS with Homebrew), you can add them via an `env` key (see [above](#clients-common-pitfalls)). +**Verification.** Once you have created the new MCP server entry, save the config and then restart Claude Desktop. :::{attention} @@ -223,28 +338,49 @@ minimize it. After restarting, you should see Serena's tools in your chat interface (notice the small hammer icon). -For more information on MCP servers with Claude Desktop, -see [the official quick start guide](https://modelcontextprotocol.io/quickstart/user). +## Copilot CLI -## JetBrains Junie +In the interactive mode, you can call `/mcp add` from within the copilot CLI. There, use serena as name, +STDIO as the server type, and `serena start-mcp-server --context=copilot-cli --project-from-cwd` as command. -Open Junie, go to the three dots in the top right corner, then Settings / MCP Settings and add Serena to Junie's global -MCP server configuration: +Alternatively, add the following to `~/.copilot/mcp-config.json` (create the file if it does not exist): ```json { "mcpServers": { "serena": { - "command": "uvx", + "type": "stdio", + "command": "serena", + "tools": [ + "*" + ], "args": [ - "-p", - "3.13", - "--from", - "git+https://github.com/oraios/serena", - "serena", "start-mcp-server", - "--context", - "ide" + "--context=copilot-cli", + "--project-from-cwd" + ] + } + } +} +``` + +**Verification.** +Copilot should now show that Serena is running, though you may have to restart it. + + +## JetBrains Junie + +For the Junie plugin in JetBrains IDEs, go to the three dots in the top right corner, +then Settings / MCP Settings and add Serena to Junie's global MCP server configuration: + +```json +{ + "mcpServers": { + "serena": { + "command": "serena", + "args": [ + "start-mcp-server", + "--context=junie" ] } } @@ -254,6 +390,23 @@ MCP server configuration: You will have to prompt Junie to "Activate the current project using serena's activation tool" at the start of each session. +For Junie CLI, where the project can be activated automatically on startup, edit `~/.junie/mcp/mcp.json` and add + +```json +{ + "mcpServers": { + "serena": { + "command": "serena", + "args": [ + "start-mcp-server", + "--context=junie", + "--project-from-cwd" + ] + } + } +} +``` + ## JetBrains AI Assistant Here you can set up the more convenient per-project MCP server configuration, as the AI assistant supports specifying @@ -265,16 +418,10 @@ Go to Settings / Tools / AI Assistant / MCP and add a new **local** configuratio { "mcpServers": { "serena": { - "command": "uvx", + "command": "serena", "args": [ - "-p", - "3.13", - "--from", - "git+https://github.com/oraios/serena", - "serena", "start-mcp-server", - "--context", - "ide", + "--context=jb-ai-assistant", "--project", "$(pwd)" ] @@ -293,16 +440,10 @@ Add this configuration: { "mcpServers": { "serena": { - "command": "uvx", + "command": "serena", "args": [ - "-p", - "3.13", - "--from", - "git+https://github.com/oraios/serena", - "serena", "start-mcp-server", - "--context", - "ide" + "--context=antigravity" ] } } @@ -340,7 +481,7 @@ in order to reduce tool duplication. Most of the popular existing coding assistants (e.g. IDE extensions) and AI-enabled IDEs themselves support connections to MCP Servers. Serena generally boosts performance by providing efficient tools for symbolic operations. -We generally recommend to use the `ide` context for these integrations by adding the arguments `--context ide` +We generally recommend using the `ide` context for these integrations by adding the arguments `--context ide` in order to reduce tool duplication. ### Local GUIs and Agent Frameworks @@ -354,5 +495,5 @@ Some of the leading open source GUI applications are * [OpenWebUI](https://docs.openwebui.com/openapi-servers/mcp) and * [Agno](https://docs.agno.com/introduction/playground). -These applications allow to combine Serena with almost any LLM (including locally running ones) +These applications allow combining Serena with almost any LLM (including locally running ones) and offer various other integrations. diff --git a/src/serena/resources/config/contexts/antigravity.yml b/src/serena/resources/config/contexts/antigravity.yml new file mode 100644 index 00000000..33411ac2 --- /dev/null +++ b/src/serena/resources/config/contexts/antigravity.yml @@ -0,0 +1,26 @@ +description: Generic IDE coding agent context (basic file operations and shell operations assumed to be covered; single project mode) +prompt: | + You are running in an IDE assistant context where file operations, basic (line-based) edits and reads, + and shell commands are handled by your own, internal tools. + + If Serena's tools can be used to achieve your task, you should prioritize them. + In particular, it is important that you avoid reading entire source code files unless it is strictly necessary! + Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools. + For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool. + +excluded_tools: + - create_text_file + - read_file + - execute_shell_command + - find_file + - list_dir + +tool_description_overrides: {} + +# whether to assume that Serena shall only work on a single project in this context (provided that a project is given +# when Serena is started). +# If set to true and a project is provided at startup, the set of tools is limited to those required by the project's +# concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal. +# Tools explicitly disabled by the project will not be available at all. +# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed. +single_project: true diff --git a/src/serena/resources/config/contexts/claude-code.yml b/src/serena/resources/config/contexts/claude-code.yml index c7da899e..6678c913 100644 --- a/src/serena/resources/config/contexts/claude-code.yml +++ b/src/serena/resources/config/contexts/claude-code.yml @@ -4,15 +4,20 @@ prompt: | as well as shell commands are handled by your own, internal tools. If Serena's tools can be used to achieve your task, you should prioritize them. - In particular, it is important that you avoid reading entire source code files unless it is strictly necessary! - Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools. - For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool. + **IMPORTANT**: If you haven't seen Serena's tools yet, you should use the tool search tool to explore them. + Avoid reading entire source code files unless it is strictly necessary! + For exploring and reading code in a token-efficient manner, use Serena's code-centric overview and symbolic search tools. + For edits, where it makes sense, use Serena's editing tools since they are more token efficient. + Use your own editing tools only if Serena's editing tools would be suboptimal. + excluded_tools: - create_text_file - read_file - execute_shell_command - replace_content + - find_file + - list_dir tool_description_overrides: {} diff --git a/src/serena/resources/config/contexts/codex.yml b/src/serena/resources/config/contexts/codex.yml index ccad93b5..2dbd870a 100644 --- a/src/serena/resources/config/contexts/codex.yml +++ b/src/serena/resources/config/contexts/codex.yml @@ -14,6 +14,8 @@ excluded_tools: - read_file - execute_shell_command - replace_content + - find_file + - list_dir tool_description_overrides: {} diff --git a/src/serena/resources/config/contexts/copilot-cli.yml b/src/serena/resources/config/contexts/copilot-cli.yml new file mode 100644 index 00000000..c7da899e --- /dev/null +++ b/src/serena/resources/config/contexts/copilot-cli.yml @@ -0,0 +1,25 @@ +description: Claude Code (CLI agent where file operations, basic edits, etc. are already covered; single project mode) +prompt: | + You are running in a CLI coding agent context where file operations, basic (line-based) edits and reads + as well as shell commands are handled by your own, internal tools. + + If Serena's tools can be used to achieve your task, you should prioritize them. + In particular, it is important that you avoid reading entire source code files unless it is strictly necessary! + Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools. + For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool. + +excluded_tools: + - create_text_file + - read_file + - execute_shell_command + - replace_content + +tool_description_overrides: {} + +# whether to assume that Serena shall only work on a single project in this context (provided that a project is given +# when Serena is started). +# If set to true and a project is provided at startup, the set of tools is limited to those required by the project's +# concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal. +# Tools explicitly disabled by the project will not be available at all. +# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed. +single_project: true diff --git a/src/serena/resources/config/contexts/ide.yml b/src/serena/resources/config/contexts/ide.yml index e8806f4e..33411ac2 100644 --- a/src/serena/resources/config/contexts/ide.yml +++ b/src/serena/resources/config/contexts/ide.yml @@ -12,6 +12,8 @@ excluded_tools: - create_text_file - read_file - execute_shell_command + - find_file + - list_dir tool_description_overrides: {} diff --git a/src/serena/resources/config/contexts/jb-ai-assistant.yml b/src/serena/resources/config/contexts/jb-ai-assistant.yml new file mode 100644 index 00000000..33411ac2 --- /dev/null +++ b/src/serena/resources/config/contexts/jb-ai-assistant.yml @@ -0,0 +1,26 @@ +description: Generic IDE coding agent context (basic file operations and shell operations assumed to be covered; single project mode) +prompt: | + You are running in an IDE assistant context where file operations, basic (line-based) edits and reads, + and shell commands are handled by your own, internal tools. + + If Serena's tools can be used to achieve your task, you should prioritize them. + In particular, it is important that you avoid reading entire source code files unless it is strictly necessary! + Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools. + For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool. + +excluded_tools: + - create_text_file + - read_file + - execute_shell_command + - find_file + - list_dir + +tool_description_overrides: {} + +# whether to assume that Serena shall only work on a single project in this context (provided that a project is given +# when Serena is started). +# If set to true and a project is provided at startup, the set of tools is limited to those required by the project's +# concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal. +# Tools explicitly disabled by the project will not be available at all. +# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed. +single_project: true diff --git a/src/serena/resources/config/contexts/junie.yml b/src/serena/resources/config/contexts/junie.yml new file mode 100644 index 00000000..e8806f4e --- /dev/null +++ b/src/serena/resources/config/contexts/junie.yml @@ -0,0 +1,24 @@ +description: Generic IDE coding agent context (basic file operations and shell operations assumed to be covered; single project mode) +prompt: | + You are running in an IDE assistant context where file operations, basic (line-based) edits and reads, + and shell commands are handled by your own, internal tools. + + If Serena's tools can be used to achieve your task, you should prioritize them. + In particular, it is important that you avoid reading entire source code files unless it is strictly necessary! + Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools. + For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool. + +excluded_tools: + - create_text_file + - read_file + - execute_shell_command + +tool_description_overrides: {} + +# whether to assume that Serena shall only work on a single project in this context (provided that a project is given +# when Serena is started). +# If set to true and a project is provided at startup, the set of tools is limited to those required by the project's +# concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal. +# Tools explicitly disabled by the project will not be available at all. +# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed. +single_project: true diff --git a/src/serena/resources/config/contexts/vscode.yml b/src/serena/resources/config/contexts/vscode.yml new file mode 100644 index 00000000..fd74fd4d --- /dev/null +++ b/src/serena/resources/config/contexts/vscode.yml @@ -0,0 +1,29 @@ +description: Generic IDE coding agent context (basic file operations and shell operations assumed to be covered; single project mode) +prompt: | + In addition to Serena's tools, you have access to internal tools, including for, basic (line-based) edits and reads, + and shell commands. + + If Serena's tools can be used to achieve your task, you should prioritize them. + In particular, it is important that you avoid reading entire source code files unless it is strictly necessary! + Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools. + For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool. + + Make sure to first activate the working directory as using Serena's `activate_project` tool and to read initial + instructions (if not done so already) with the `initial_instructions` tool. + +excluded_tools: + - create_text_file + - read_file + - execute_shell_command + - find_file + - list_dir + +tool_description_overrides: {} + +# whether to assume that Serena shall only work on a single project in this context (provided that a project is given +# when Serena is started). +# If set to true and a project is provided at startup, the set of tools is limited to those required by the project's +# concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal. +# Tools explicitly disabled by the project will not be available at all. +# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed. +single_project: true From c9af41afd4e0c2d108d9ade85cde0d93d67c6a40 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 13:21:57 +0200 Subject: [PATCH 090/190] Further extended hook heuristics --- src/serena/hooks.py | 168 +++++++++++++++++++++++++++++++++----- test/serena/test_hooks.py | 125 +++++++++++++++++++++++++++- 2 files changed, 269 insertions(+), 24 deletions(-) diff --git a/src/serena/hooks.py b/src/serena/hooks.py index 8988d8a8..4a04e9f4 100644 --- a/src/serena/hooks.py +++ b/src/serena/hooks.py @@ -87,9 +87,27 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook): Tracks consecutive uses of grep and read-file tools via a persisted :class:`ToolUseCounter`. When the number of recent calls reaches the configured threshold, a deny response is emitted with a reminder to - use symbolic alternatives. The counter resets whenever a Serena tool - is invoked or a deny is emitted, so the agent is only reminded after - sustained non-symbolic tool use without intermittent Serena calls. + use symbolic alternatives. + + The counter for a given tool type is reset whenever + + * a Serena tool is invoked (both counters are reset), + * a deny is emitted (the acting counter is reset so the next retry starts fresh), + * or the configured reset period elapses *between two consecutive calls of that + same tool type* — i.e. the period gates the gap between successive calls, not + an absolute sliding window. Three grep calls at t=0, t=9, t=18 therefore count + as a burst of three, even though the total span (18s) exceeds the 10s grep + period; only an individual pair that is more than 10s apart resets the counter. + + Non-tracked tools (Edit, Write, Bash, etc.) are deliberately neutral: they neither + increment nor reset counters, so they also do not mask bursts by pushing the last + timestamp forward. + + Deny emissions are additionally rate-limited to at most one per + :attr:`ToolUseCounter._MIN_DENY_INTERVAL_SECONDS` (one minute): once a deny has + been emitted, subsequent bursts detected within that window still reset the + counters but do not produce further nudges, so the agent is not swamped with + reminders during a sustained non-symbolic-tool burst. """ @dataclass @@ -97,14 +115,28 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook): _FILE_NAME = "tool_use_counter.pkl" _GREP_USES_THRESHOLD = 3 _READ_FILE_USES_THRESHOLD = 3 + # threshold for the combined "non-symbolic" counter that catches mixed sequences of grep+read + _NON_SYMBOLIC_USES_THRESHOLD = 4 - _GREP_RESET_PERIOD_SECONDS = 10 - _READ_FILE_RESET_PERIOD_SECONDS = 20 + # The following periods are set to essentially infinity since we neglect the per-tool reset periods for them + _READ_FILE_RESET_PERIOD_SECONDS = 1000 + _GREP_RESET_PERIOD_SECONDS = 1000 + # reset period for the combined counter + _NON_SYMBOLIC_RESET_PERIOD_SECONDS = 2000 + + # minimum seconds between two emitted denies; rate-limits reminder nudges so + # a single sustained burst triggers at most one deny per minute + _MIN_DENY_INTERVAL_SECONDS = 120 n_recent_read_file_uses: int = 0 n_recent_grep_uses: int = 0 + n_recent_non_symbolic_uses: int = 0 last_grep_use_timestamp: datetime | None = None last_read_file_use_timestamp: datetime | None = None + last_non_symbolic_use_timestamp: datetime | None = None + # timestamp of the most recently emitted deny; deliberately not cleared by + # :meth:`reset` so the rate limit survives counter resets (e.g. Serena tool use) + last_deny_timestamp: datetime | None = None def too_many_recent_reads(self) -> bool: return self.n_recent_read_file_uses >= self._READ_FILE_USES_THRESHOLD @@ -112,6 +144,21 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook): def too_many_recent_greps(self) -> bool: return self.n_recent_grep_uses >= self._GREP_USES_THRESHOLD + def too_many_recent_non_symbolic(self) -> bool: + return self.n_recent_non_symbolic_uses >= self._NON_SYMBOLIC_USES_THRESHOLD + + def can_emit_deny(self, now: datetime) -> bool: + """:return: whether enough time has elapsed since the last emitted deny + to allow another one. A value of ``True`` is also returned when no + deny has been emitted yet in this session. The rate limit prevents + the agent from being nudged more than once per + :attr:`_MIN_DENY_INTERVAL_SECONDS`, even if further bursts are + detected in the meantime. + """ + if self.last_deny_timestamp is None: + return True + return (now - self.last_deny_timestamp).total_seconds() >= self._MIN_DENY_INTERVAL_SECONDS + @classmethod def _get_persistence_path(cls, hook: Hook) -> Path: return Path(hook.session_persistence_dir) / cls._FILE_NAME @@ -140,9 +187,11 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook): return now = hook.triggered_at_timestamp + is_grep = hook.is_grep_tool() + is_read = hook.is_read_file_tool() # update grep counter - if hook.is_grep_tool(): + if is_grep: grep_period = self._GREP_RESET_PERIOD_SECONDS if self.last_grep_use_timestamp is not None and (now - self.last_grep_use_timestamp).total_seconds() <= grep_period: self.n_recent_grep_uses += 1 @@ -151,7 +200,7 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook): self.last_grep_use_timestamp = now # update read file counter - if hook.is_read_file_tool(): + if is_read: read_period = self._READ_FILE_RESET_PERIOD_SECONDS if ( self.last_read_file_use_timestamp is not None @@ -162,11 +211,34 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook): self.n_recent_read_file_uses = 1 self.last_read_file_use_timestamp = now + # update combined non-symbolic counter — catches mixed bursts (e.g. + # alternating grep/read) that neither per-tool counter would trip + if is_grep or is_read: + combined_period = self._NON_SYMBOLIC_RESET_PERIOD_SECONDS + if ( + self.last_non_symbolic_use_timestamp is not None + and (now - self.last_non_symbolic_use_timestamp).total_seconds() <= combined_period + ): + self.n_recent_non_symbolic_uses += 1 + else: + self.n_recent_non_symbolic_uses = 1 + self.last_non_symbolic_use_timestamp = now + def reset(self) -> None: self.n_recent_read_file_uses = 0 self.n_recent_grep_uses = 0 + self.n_recent_non_symbolic_uses = 0 self.last_grep_use_timestamp = None self.last_read_file_use_timestamp = None + self.last_non_symbolic_use_timestamp = None + + #: substrings that, combined with ``"file"`` in the tool name, identify a + #: read-file tool for non-Claude-Code clients (whose tool names vary across + #: editors/agents). Lowercase because :attr:`_tool_name` is lowercased on + #: ingest. Conservative on purpose: only verbs that strongly imply *reading* + #: a file, never *modifying* one — so ``view_file``/``open_file``/``show_file`` + #: are caught alongside ``read_file``, while ``write_file``/``edit_file`` are not. + _READ_FILE_VERB_SUBSTRINGS: tuple[str, ...] = ("read", "view", "open", "show") def __init__(self, client: HookClient): super().__init__(client) @@ -180,28 +252,80 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook): def is_read_file_tool(self) -> bool: if self._client == HookClient.CLAUDE_CODE: return self._tool_name == "read" - return "read" in self._tool_name and "file" in self._tool_name + name = self._tool_name + if "file" not in name: + return False + return any(verb in name for verb in self._READ_FILE_VERB_SUBSTRINGS) def execute(self) -> None: self._tool_call_counter.update(self) - output_data = None - if self._tool_call_counter.too_many_recent_greps(): - output_data = self.OutputData( - permission_decision="deny", - permission_decision_reason="Too many consecutive grep calls without using symbolic tools.", - additional_context="You were using many grep calls recently. Consider using Serena's symbolic search tools instead. You can continue using grep now if needed, the counter was reset.", - ) - if self._tool_call_counter.too_many_recent_reads(): - output_data = self.OutputData( - permission_decision="deny", - permission_decision_reason="Too many consecutive read calls without using symbolic tools.", - additional_context="You were using many read file calls recently. Consider using Serena's symbolic overview and search instead. You can continue using read now if needed, the counter was reset", - ) + + # pick the deny that matches the current call first, so the emitted message lines up + # with the tool the agent just invoked; fall back to the other per-tool counter only + # if the current call did not itself trip a threshold (e.g. stale state loaded from + # pickle). The combined non-symbolic deny is checked last — it only fires when neither + # per-tool counter tripped, which is exactly the mixed-burst case (alternating grep/read). + too_many_greps = self._tool_call_counter.too_many_recent_greps() + too_many_reads = self._tool_call_counter.too_many_recent_reads() + too_many_non_symbolic = self._tool_call_counter.too_many_recent_non_symbolic() + + output_data: PreToolUseHook.OutputData | None = None + if self.is_grep_tool() and too_many_greps: + output_data = self._build_grep_deny() + elif self.is_read_file_tool() and too_many_reads: + output_data = self._build_read_deny() + elif too_many_greps: + output_data = self._build_grep_deny() + elif too_many_reads: + output_data = self._build_read_deny() + elif too_many_non_symbolic: + output_data = self._build_non_symbolic_deny() + if output_data is not None: + # reset first so burst detection starts fresh regardless of whether the + # deny is actually emitted; the rate limit only gates *emission*, not + # detection self._tool_call_counter.reset() - click.echo(output_data.to_json_string()) + if self._tool_call_counter.can_emit_deny(self.triggered_at_timestamp): + self._tool_call_counter.last_deny_timestamp = self.triggered_at_timestamp + click.echo(output_data.to_json_string()) self._tool_call_counter.save(self) + def _build_grep_deny(self) -> "PreToolUseHook.OutputData": + return self.OutputData( + permission_decision="deny", + permission_decision_reason="Too many consecutive grep calls without using symbolic tools. " + "You can continue using grep now if needed, the counter was reset.", + additional_context=( + "You were using many grep calls recently. Consider using Serena's symbolic " + "mcp tools instead for more code-centric search. You can continue using grep now if needed, the counter was reset." + ), + ) + + def _build_read_deny(self) -> "PreToolUseHook.OutputData": + return self.OutputData( + permission_decision="deny", + permission_decision_reason="Too many consecutive read calls without using symbolic tools. " + "You can continue using read now if needed, the counter was reset.", + additional_context=( + "You were using many read file calls recently. Consider using Serena's symbolic " + "mcp tools instead for more targeted reads. You can continue using read now if needed, the counter was reset." + ), + ) + + def _build_non_symbolic_deny(self) -> "PreToolUseHook.OutputData": + return self.OutputData( + permission_decision="deny", + permission_decision_reason="Too many consecutive non-symbolic tool calls (mixed grep and read). " + "You can continue using these tools now if needed, the counter was reset.", + additional_context=( + "You were alternating between grep and read file calls recently without using " + "Serena's symbolic mcp tools. Consider using symbolic search and targeted symbol " + "reads instead for more code-centric exploration. You can continue using these tools " + "now if needed, the counter was reset." + ), + ) + class SessionStartActivateProjectHook(Hook): def execute(self) -> None: diff --git a/test/serena/test_hooks.py b/test/serena/test_hooks.py index 3023261f..a98714fd 100644 --- a/test/serena/test_hooks.py +++ b/test/serena/test_hooks.py @@ -85,8 +85,22 @@ class TestPreToolUseRemindAboutSerenaHook: assert hook.is_read_file_tool() == expected, f"is_read_file_tool() wrong for {name} (claude-code)" def test_read_file_tool_detection_non_claude_code(self, tmp_path: Path): - """Non-Claude-Code clients fall back to substring matching requiring both ``read`` and ``file``.""" - for name, expected in [("read_file", True), ("readFile", True), ("grep_search", False), ("file_writer", False)]: + """Non-Claude-Code clients accept any read-style verb (``read``/``view``/``open``/``show``) combined with ``file``.""" + cases = [ + # canonical names + ("read_file", True), + ("readFile", True), + # alternative read verbs used by other agents/editors + ("view_file", True), + ("open_file", True), + ("show_file", True), + # negatives: no "file", or "file" without a read verb, or modifying verbs + ("grep_search", False), + ("file_writer", False), + ("write_file", False), + ("edit_file", False), + ] + for name, expected in cases: with patch("sys.stdin", _make_stdin(_base_input(tool_name=name))), patch("serena.hooks.serena_home_dir", str(tmp_path)): hook = PreToolUseRemindAboutSerenaHook(HookClient.VSCODE) assert hook.is_read_file_tool() == expected, f"is_read_file_tool() wrong for {name} (vscode)" @@ -166,6 +180,79 @@ class TestPreToolUseRemindAboutSerenaHook: assert capsys.readouterr().out == "" + def test_rate_limit_suppresses_second_deny_within_interval(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """A second burst detected within the minimum deny interval must not emit a deny.""" + # first burst: should emit a deny + for _ in range(ToolUseCounter._GREP_USES_THRESHOLD): + with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() + first_output = capsys.readouterr().out.strip() + assert first_output, "first burst should have emitted a deny" + + # second burst immediately after: within the 1-minute window, must be suppressed + for _ in range(ToolUseCounter._GREP_USES_THRESHOLD): + with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() + + assert capsys.readouterr().out == "" + + def test_rate_limit_allows_deny_after_interval(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """Once the minimum deny interval has elapsed, a fresh burst emits a deny again.""" + # first burst: emits a deny + for _ in range(ToolUseCounter._GREP_USES_THRESHOLD): + with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() + capsys.readouterr() + + # backdate the persisted last_deny_timestamp so the rate-limit window has expired + stub_for_path = object.__new__(PreToolUseRemindAboutSerenaHook) + stub_for_path.session_persistence_dir = str(tmp_path / "hook_data" / _base_input()["session_id"]) + counter = ToolUseCounter.load(stub_for_path) + assert counter.last_deny_timestamp is not None + counter.last_deny_timestamp -= timedelta(seconds=ToolUseCounter._MIN_DENY_INTERVAL_SECONDS + 1) + counter.save(stub_for_path) + + # second burst should now emit a deny again + for _ in range(ToolUseCounter._GREP_USES_THRESHOLD): + with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() + + second_output = capsys.readouterr().out.strip() + assert second_output, "after the rate-limit window elapsed, a new burst should emit a deny" + result = json.loads(second_output) + assert result["hookSpecificOutput"]["permissionDecision"] == "deny" + + def test_non_symbolic_deny_emitted_when_combined_threshold_tripped(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """Pre-populated state trips only the combined non-symbolic counter (not per-tool ones) + so execute() must fall through to the _build_non_symbolic_deny branch. + """ + # pre-populate the pickle so only the combined counter is over threshold + session_dir = tmp_path / "hook_data" / _base_input()["session_id"] + session_dir.mkdir(parents=True) + counter = ToolUseCounter( + n_recent_grep_uses=ToolUseCounter._GREP_USES_THRESHOLD - 1, + n_recent_read_file_uses=ToolUseCounter._READ_FILE_USES_THRESHOLD - 1, + n_recent_non_symbolic_uses=ToolUseCounter._NON_SYMBOLIC_USES_THRESHOLD, + last_grep_use_timestamp=datetime.now(), + last_read_file_use_timestamp=datetime.now(), + last_non_symbolic_use_timestamp=datetime.now(), + ) + stub_for_path = object.__new__(PreToolUseRemindAboutSerenaHook) + stub_for_path.session_persistence_dir = str(session_dir) + counter.save(stub_for_path) + + # invoke execute with a neutral (non-grep, non-read, non-serena) tool so that + # update() leaves the counters untouched and the fall-through branch is taken + with patch("sys.stdin", _make_stdin(_base_input("Edit"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): + PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() + + output = capsys.readouterr().out.strip() + assert output, "expected a non-symbolic deny to be emitted" + result = json.loads(output) + hook_output = result["hookSpecificOutput"] + assert hook_output["permissionDecision"] == "deny" + assert "symbolic" in hook_output["additionalContext"].lower() + class TestToolUseCounter: """Tests for the time-windowed tool-use counter logic.""" @@ -261,6 +348,40 @@ class TestToolUseCounter: loaded = ToolUseCounter.load(hook_stub) # type: ignore[arg-type] assert loaded == ToolUseCounter() + def test_can_emit_deny_respects_min_interval(self): + """:meth:`can_emit_deny` returns False within the minimum interval, True outside it.""" + counter = ToolUseCounter() + base = datetime.now() + + # no prior deny → always allowed + assert counter.can_emit_deny(base) + + # within the interval → rate-limited + counter.last_deny_timestamp = base + interval = ToolUseCounter._MIN_DENY_INTERVAL_SECONDS + assert not counter.can_emit_deny(base + timedelta(seconds=interval - 1)) + assert not counter.can_emit_deny(base) + + # at/after the interval → allowed again + assert counter.can_emit_deny(base + timedelta(seconds=interval)) + assert counter.can_emit_deny(base + timedelta(seconds=interval + 1)) + + def test_reset_preserves_last_deny_timestamp(self): + """``reset`` clears burst counters but must keep ``last_deny_timestamp`` intact.""" + counter = ToolUseCounter() + base = datetime.now() + counter.last_deny_timestamp = base + counter.n_recent_grep_uses = 5 + counter.n_recent_read_file_uses = 4 + counter.n_recent_non_symbolic_uses = 7 + + counter.reset() + + assert counter.n_recent_grep_uses == 0 + assert counter.n_recent_read_file_uses == 0 + assert counter.n_recent_non_symbolic_uses == 0 + assert counter.last_deny_timestamp == base + @staticmethod def _make_hook_stub(tool_name: str, timestamp: datetime) -> PreToolUseRemindAboutSerenaHook: """Create a minimal stub that satisfies ToolUseCounter.update without reading stdin. From 955ff573d9297436f383e0593e7dad391169c635 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 10 Apr 2026 13:46:08 +0200 Subject: [PATCH 091/190] SerenaDashboardViewer: Add a parent monitoring thread to ensure termination Some clients would terminate the main process in a way that did not ensure cleanup/proper termination --- CHANGELOG.md | 6 +++++- src/serena/agent.py | 6 +++--- src/serena/dashboard.py | 48 +++++++++++++++++++++++++++++++++++------ 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb3c8397..82a17eb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,14 @@ Status of the `main` branch. Changes prior to the next official version change w * General: - Added `serena init` command + +* Language Servers: - Added Crystal language support (uses [Crystalline](https://github.com/elbywan/crystalline) language server) * Dashboard: - - Add version display + - Add version display + - Dashboard viewer (Windows): Add a parent monitoring thread to ensure termination. + Some clients would terminate the MCP server in a way that did not ensure proper termination. * Fixes: - Fix reactivation of the same project restarting language servers #1280 diff --git a/src/serena/agent.py b/src/serena/agent.py index 31267135..fc4c6b69 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -558,12 +558,12 @@ class SerenaAgent: return self._dashboard_url @staticmethod - def _start_dashboard_viewer_process_function(url: str, minimized: bool) -> None: + def _start_dashboard_viewer_process_function(url: str, minimized: bool, parent_process_id: int) -> None: """ Main function of the subprocess for starting the dashboard viewer """ try: - SerenaDashboardViewer(url, start_minimized=minimized).run() + SerenaDashboardViewer(url, start_minimized=minimized, parent_process_id=parent_process_id).run() except webview.errors.WebViewException as e: 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 @@ -586,7 +586,7 @@ class SerenaAgent: assert url is not None if SerenaDashboardViewer.is_current_platform_supported(): self._dashboard_viewer_process = multiprocessing.Process( - target=self._start_dashboard_viewer_process_function, args=(url, minimized), daemon=True + target=self._start_dashboard_viewer_process_function, args=(url, minimized, os.getpid()), daemon=True ) self._dashboard_viewer_process.start() else: diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index dbd46504..528af993 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -3,11 +3,13 @@ import os import socket import sys import threading +import time import urllib.error import urllib.request from pathlib import Path from typing import TYPE_CHECKING, Any, Self +import psutil import webview from flask import Flask, Response, redirect, request, send_from_directory from PIL import Image @@ -743,6 +745,8 @@ class SerenaDashboardViewer: Minimal pywebview wrapper with optional system tray. """ + DEBUG = False + def __init__( self, url: str, @@ -750,6 +754,7 @@ class SerenaDashboardViewer: start_minimized: bool = False, width: int = 1400, height: int = 900, + parent_process_id: int | None = None, ): self.url = url # Use system tray to allow hiding to tray and intercepting close to hide instead of quit @@ -758,6 +763,7 @@ class SerenaDashboardViewer: self.width = width self.height = height self.start_minimized = start_minimized + self.parent_process_id = parent_process_id self.window: webview.Window self._tray_icon: Any @@ -786,6 +792,10 @@ class SerenaDashboardViewer: ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("oraios.serena") + if self.DEBUG: + logging.configure(level=logging.DEBUG) + logging.add_file_logger(SerenaPaths().get_next_log_file_path("dashboard-viewer")) + dashboard_path = Path(SERENA_DASHBOARD_DIR) # .ico is Windows-only; macOS expects a PNG for the window/dock icon. icon_filename = "serena.ico" if sys.platform == "win32" else "serena-icon-1024-mac.png" @@ -815,8 +825,27 @@ class SerenaDashboardViewer: else: self._show_window() + # start a thread to monitor the parent process + if self.parent_process_id is not None: + threading.Thread(target=self._monitor_parent_process, daemon=True).start() + webview.start(_start_callback, icon=icon_path) + def _monitor_parent_process(self) -> None: + """ + Monitors the parent process and shuts down the dashboard viewer if the parent process exits. + """ + pid = self.parent_process_id + log.info("Starting parent process monitor thread (parent_pid=%d, pid=%s)", pid, os.getpid()) + try: + parent_process = psutil.Process(pid) + while parent_process.is_running(): + time.sleep(1) + except psutil.NoSuchProcess: + pass # Parent process already exited + log.info("Parent process (pid=%d) has exited, shutting down dashboard viewer", pid) + self._terminate() + def _show_window(self) -> None: if not self.window: return @@ -885,6 +914,18 @@ class SerenaDashboardViewer: self._hide_window() return False # prevent the window from actually closing + def _terminate(self) -> None: + """ + Terminates the viewer application and the tray icon + """ + self._quitting = True + try: + if self._tray_icon: + self._tray_icon.stop() + finally: + if self.window: + self.window.destroy() + def _start_tray(self) -> None: # import pystray locally, because the import fails when there is no display! import pystray @@ -905,12 +946,7 @@ class SerenaDashboardViewer: self._hide_window() def quit_app(_icon: TrayIcon, _item: Item) -> None: - self._quitting = True - try: - _icon.stop() - finally: - if self.window: - self.window.destroy() + self._terminate() menu = pystray.Menu( Item("Open", show, default=True), From e9b6c0ff853f1f0a4ae20288ed9a2bcf017cf72e Mon Sep 17 00:00:00 2001 From: Tobias Jansing Date: Mon, 23 Mar 2026 12:36:03 +0100 Subject: [PATCH 092/190] Add Haxe language support via vshaxe/haxe-language-server Includes full setup with ls provider, tests, docs, etc. --- .gitattributes | 4 + .github/workflows/pytest.yml | 4 + CHANGELOG.md | 1 + README.md | 2 +- docs/01-about/020_programming-languages.md | 5 +- docs/02-usage/050_configuration.md | 24 ++ pyproject.toml | 1 + src/serena/resources/project.template.yml | 3 +- .../language_servers/haxe_language_server.py | 386 ++++++++++++++++++ src/solidlsp/ls_config.py | 11 + test/conftest.py | 1 + .../resources/repos/haxe/test_repo/.gitignore | 3 + .../resources/repos/haxe/test_repo/build.hxml | 3 + .../repos/haxe/test_repo/src/Main.hx | 40 ++ .../repos/haxe/test_repo/src/utils/Helper.hx | 29 ++ test/serena/test_serena_agent.py | 9 + test/solidlsp/haxe/__init__.py | 0 test/solidlsp/haxe/test_haxe_basic.py | 249 +++++++++++ 18 files changed, 772 insertions(+), 3 deletions(-) create mode 100644 .gitattributes create mode 100644 src/solidlsp/language_servers/haxe_language_server.py create mode 100644 test/resources/repos/haxe/test_repo/.gitignore create mode 100644 test/resources/repos/haxe/test_repo/build.hxml create mode 100644 test/resources/repos/haxe/test_repo/src/Main.hx create mode 100644 test/resources/repos/haxe/test_repo/src/utils/Helper.hx create mode 100644 test/solidlsp/haxe/__init__.py create mode 100644 test/solidlsp/haxe/test_haxe_basic.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..a1da894b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Force LF line endings for Haxe files across platforms. +# The Haxe language server cannot properly parse/index files with CRLF endings, causing it to fail on Windows without this. +*.hxml text eol=lf +*.hx text eol=lf diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 1f20f585..133c4632 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -386,6 +386,10 @@ jobs: - name: Install ansible-core and ansible-lint (for Ansible language server tests) shell: bash run: uv run pip install ansible-core ansible-lint + - name: Install Haxe + uses: krdlab/setup-haxe@v2 + with: + haxe-version: 4.3.7 - name: Install Elm shell: bash run: npm install -g elm@0.19.1-6 diff --git a/CHANGELOG.md b/CHANGELOG.md index bb3c8397..177db5fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ Status of the `main` branch. Changes prior to the next official version change w * **Add support for Lean 4** via built-in `lean --server` with cross-file reference support (requires `lean` and `lake` via [elan](https://github.com/leanprover/elan)) * **Add support for OCaml** via ocaml-lsp-server with cross-file reference support on OCaml 5.2+ (requires opam; see [setup guide](docs/03-special-guides/ocaml_setup_guide_for_serena.md)) * **Add Phpactor as alternative PHP language server** (specify `php_phpactor` as language; requires PHP 8.1+) + * **Add support for Haxe** via vshaxe/haxe-language-server. Requires Haxe compiler 3.4.0+ and Node.js. Auto-discovered from the vshaxe VSCode extension or configurable via `ls_path` in `ls_specific_settings`. * **Add support for Fortran** via fortls language server (requires `pip install fortls`) * **Add partial support for Groovy** requires user-provided Groovy language server JAR (see [setup guide](docs/03-special-guides/groovy_setup_guide_for_serena.md)) * **Add support for Julia** via LanguageServer.jl diff --git a/README.md b/README.md index bd3d0f19..426b6fc9 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Serena incorporates a powerful abstraction layer for the integration of language The underlying language servers are typically open-source projects or at least freely available for use. When using Serena's language server backend, we provide **support for over 40 programming languages**, including -AL, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, HLSL, Java, JavaScript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. +AL, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, Haxe, HLSL, Java, JavaScript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. ### The Serena JetBrains Plugin diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index c26c07f9..2fe1b929 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -63,7 +63,10 @@ Some languages require additional installations or setup steps, as noted. (requires local groovy-language-server.jar setup via `GROOVY_LS_JAR_PATH` or configuration) * **Haskell** (automatically locates HLS via ghcup, stack, or system PATH; supports Stack and Cabal projects) -* **HLSL / GLSL / WGSL** +* **Haxe** + (requires Haxe compiler 3.4.0+ and Node.js; uses the [vshaxe language server](https://github.com/vshaxe/haxe-language-server); + automatically downloaded from Open VSX, or discovered from the vshaxe VSCode extension) +* **HLSL / GLSL / WGSL** (uses [shader-language-server](https://github.com/antaalt/shader-sense) (language `hlsl`); automatically downloaded; on macOS, requires Rust toolchain for building from source; note: reference search is not supported by this language server) diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 1c8fbfaa..2c4ff1f5 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -484,6 +484,30 @@ Supported settings: | `version` | `1.3.0` | Override the bundled version Serena downloads, or builds from source on macOS, when `ls_path` is not set. | +#### Haxe + +Serena uses the [vshaxe/haxe-language-server](https://github.com/vshaxe/haxe-language-server) for Haxe support. +Requires Haxe compiler (3.4.0+) and Node.js. + +The server is discovered in order: user-configured `ls_path`, system PATH, vshaxe VSCode extension, auto-download from Open VSX. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | auto-discovered | Path to the Haxe language server binary (e.g., `/path/to/server.js`). | +| `buildFile` | auto-discovered `.hxml` | Relative path to the `.hxml` build file used for compilation (e.g., `build/debug.hxml`). If not set, Serena searches the project for `.hxml` files (max depth 5, skipping dependency directories). | +| `renameSourceFolders` | not set (LS default) | List of source directories for scoping rename operations (e.g., `["src", "lib"]`). If not set, the Haxe LS uses its own defaults. | + +Example (typically in `project.yml`, since these are project-specific): + +```yaml +ls_specific_settings: + haxe: + buildFile: "build/debug.hxml" + renameSourceFolders: ["src", "lib"] +``` + #### Java (`eclipse.jdt.ls`) The following settings are supported for the Java language server: diff --git a/pyproject.toml b/pyproject.toml index fedccc91..e3e85728 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -332,6 +332,7 @@ markers = [ "julia: Julia language server tests", "fortran: language server running for Fortran", "haskell: Haskell language server tests", + "haxe: Haxe language server tests", "yaml: language server running for YAML", "powershell: language server running for PowerShell", "pascal: language server running for Pascal (Free Pascal/Lazarus)", diff --git a/src/serena/resources/project.template.yml b/src/serena/resources/project.template.yml index f6ba8cb1..60d105dc 100644 --- a/src/serena/resources/project.template.yml +++ b/src/serena/resources/project.template.yml @@ -6,7 +6,8 @@ project_name: "project_name" # al bash clojure cpp csharp # csharp_omnisharp dart elixir elm erlang # fortran fsharp go groovy haskell -# java julia kotlin lua markdown +# haxe java julia kotlin lua +# markdown # matlab nix pascal perl php # php_phpactor powershell python python_jedi r # rego ruby ruby_solargraph rust scala diff --git a/src/solidlsp/language_servers/haxe_language_server.py b/src/solidlsp/language_servers/haxe_language_server.py new file mode 100644 index 00000000..b2248b97 --- /dev/null +++ b/src/solidlsp/language_servers/haxe_language_server.py @@ -0,0 +1,386 @@ +"""Haxe language server integration using vshaxe/haxe-language-server.""" + +import glob +import hashlib +import logging +import os +import pathlib +import shutil +import threading + +from overrides import override + +from solidlsp.ls import ( + LanguageServerDependencyProvider, + LanguageServerDependencyProviderSinglePath, + LSPFileBuffer, + SolidLanguageServer, +) +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_exceptions import SolidLSPException +from solidlsp.ls_types import Hover +from solidlsp.lsp_protocol_handler.lsp_types import DiagnosticSeverity, InitializeParams +from solidlsp.settings import SolidLSPSettings + +log = logging.getLogger(__name__) + + +class HaxeLanguageServer(SolidLanguageServer): + """Haxe language server integration using vshaxe/haxe-language-server. + + Requires Haxe compiler (3.4.0+) and Node.js. + """ + + _COMPILATION_TIMEOUT = 60.0 + + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): + """Creates a HaxeLanguageServer instance. Use LanguageServer.create() instead.""" + super().__init__( + config, + repository_root_path, + None, + "haxe", + solidlsp_settings, + ) + + self._server_ready = threading.Event() + self._server_ready.set() + self._active_progress_tokens: set[str] = set() + self._progress_lock = threading.Lock() + + @override + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + # Downloaded from Open VSX (not the VS Code Marketplace) because Open VSX + # provides stable versioned URLs and SHA256 checksums for integrity verification. + _VSHAXE_VERSION = "2.34.2" + _VSHAXE_SHA256 = "104d785e3f7b57a7f3debf520d9751f7e7abf3a7e78d203db1a8ff3dc7ca30e2" + _VSHAXE_DOWNLOAD_URL = f"https://open-vsx.org/api/nadako/vshaxe/{_VSHAXE_VERSION}/file/nadako.vshaxe-{_VSHAXE_VERSION}.vsix" + + @override + def _get_or_install_core_dependency(self) -> str: + """Find the Haxe Language Server binary.""" + # 1. Check for haxe-language-server in PATH + system_haxe_ls = shutil.which("haxe-language-server") + if system_haxe_ls: + log.info(f"Found system-installed haxe-language-server at {system_haxe_ls}") + return system_haxe_ls + + # 2. Check VSCode extension locations + vscode_server_path = self._find_vscode_extension_server() + if vscode_server_path: + log.info(f"Found Haxe Language Server in VSCode extension at {vscode_server_path}") + return vscode_server_path + + # 3. Check resource dir / download from Open VSX + haxe_ls_dir = os.path.join(self._ls_resources_dir, "haxe-lsp") + server_js_path = os.path.join(haxe_ls_dir, "bin", "server.js") + if os.path.exists(server_js_path): + log.info(f"Found Haxe Language Server at {server_js_path}") + return server_js_path + + if shutil.which("node") is None: + raise FileNotFoundError( + "Haxe Language Server not found and Node.js is not installed (required to run it).\n" + "Install options:\n" + " 1. Install Node.js and re-run (auto-download will proceed)\n" + " 2. Install the vshaxe VSCode extension: code --install-extension nadako.vshaxe\n" + " 3. Set ls_path in serena_config.yml under ls_specific_settings.haxe" + ) + + downloaded_path = self._download_from_open_vsx(haxe_ls_dir) + if downloaded_path: + return downloaded_path + + raise FileNotFoundError( + "Haxe Language Server not found. Install options:\n" + " 1. Install the vshaxe VSCode extension: code --install-extension nadako.vshaxe\n" + " 2. Set ls_path in serena_config.yml under ls_specific_settings.haxe" + ) + + @staticmethod + def _find_vscode_extension_server() -> str | None: + """Search for the Haxe language server in VSCode extension directories.""" + search_paths = [ + os.path.expanduser("~/.vscode/extensions/nadako.vshaxe-*/bin/server.js"), + os.path.expanduser("~/.vscode-server/extensions/nadako.vshaxe-*/bin/server.js"), + os.path.expanduser("~/.vscode-insiders/extensions/nadako.vshaxe-*/bin/server.js"), + ] + for pattern in search_paths: + matches = sorted(glob.glob(pattern), reverse=True) # newest version first + for match in matches: + if os.path.isfile(match): + return match + return None + + @classmethod + def _download_from_open_vsx(cls, target_dir: str) -> str | None: + """Download a pinned vshaxe VSIX from Open VSX and extract server.js. + Verifies the download against a hardcoded SHA256 checksum. + """ + import tempfile + import zipfile + + try: + import urllib.request + + log.info("Downloading Haxe Language Server v%s from Open VSX...", cls._VSHAXE_VERSION) + vsix_path = os.path.join(tempfile.gettempdir(), "vshaxe.vsix") + urllib.request.urlretrieve(cls._VSHAXE_DOWNLOAD_URL, vsix_path) + + # Verify SHA256 checksum + sha256 = hashlib.sha256() + with open(vsix_path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + sha256.update(chunk) + if sha256.hexdigest().lower() != cls._VSHAXE_SHA256: + os.remove(vsix_path) + raise RuntimeError( + f"SHA256 checksum mismatch for vshaxe VSIX. Expected {cls._VSHAXE_SHA256}, " + f"got {sha256.hexdigest()}. The file may be corrupted or tampered with." + ) + log.info("SHA256 checksum verified") + + # VSIX files are ZIP archives — extract bin/ contents + bin_dir = os.path.join(target_dir, "bin") + os.makedirs(bin_dir, exist_ok=True) + with zipfile.ZipFile(vsix_path, "r") as zf: + for entry in zf.namelist(): + if "/bin/" in entry: + filename = entry.split("/bin/", 1)[-1] + if filename and ".." not in filename: + dest_path = os.path.join(bin_dir, filename) + os.makedirs(os.path.dirname(dest_path), exist_ok=True) + with zf.open(entry) as src, open(dest_path, "wb") as dst: + dst.write(src.read()) + + os.remove(vsix_path) + + server_js_path = os.path.join(bin_dir, "server.js") + if os.path.exists(server_js_path): + log.info(f"Haxe Language Server v{cls._VSHAXE_VERSION} installed to {server_js_path}") + return server_js_path + + log.error("Downloaded VSIX but server.js not found after extraction") + return None + + except Exception: + log.exception("Failed to download Haxe Language Server from Open VSX") + return None + + @override + def _create_launch_command(self, core_path: str) -> list[str]: + if core_path.endswith(".js"): + return ["node", core_path] + return [core_path, "--stdio"] + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in [ + "node_modules", + "export", + "dump", + ] + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """Return initialize params for the Haxe Language Server. + + displayArguments are resolved from user-configured buildFile or auto-discovered .hxml files. + """ + root_uri = pathlib.Path(repository_absolute_path).as_uri() + + # 1. Check for user-configured .hxml path + configured_build_file = self._custom_settings.get("buildFile") + if configured_build_file: + log.info(f"Using user-configured Haxe build file: {configured_build_file}") + display_arguments = [configured_build_file] + else: + # 2. Auto-discover .hxml files recursively + display_arguments = self._discover_hxml_file(repository_absolute_path) + + init_options: dict = {"displayArguments": display_arguments} + rename_source_folders = self._custom_settings.get("renameSourceFolders") + if rename_source_folders: + init_options["renameSourceFolders"] = rename_source_folders + + initialize_params = { + "locale": "en", + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True}, + "completion": {"completionItem": {"snippetSupport": True}}, + "definition": {}, + "references": {}, + "documentSymbol": { + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + "hover": {"contentFormat": ["markdown", "plaintext"]}, + "codeAction": {}, + "rename": {}, + "signatureHelp": {}, + }, + "workspace": { + "workspaceFolders": True, + "didChangeConfiguration": {}, + "symbol": {}, + }, + }, + "initializationOptions": init_options, + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "workspaceFolders": [ + { + "uri": root_uri, + "name": os.path.basename(repository_absolute_path), + } + ], + } + return initialize_params # type: ignore[return-value] + + @staticmethod + def _discover_hxml_file(repository_absolute_path: str) -> list[str]: + """Return the first .hxml file found, filtering out dependency directories. + + For more control, set ``ls_specific_settings.haxe.buildFile`` in ``serena_config.yml``. + """ + max_depth = 5 + skip_dirs = {"node_modules", "haxe_libraries", ".haxelib", "export", "dump", "bin", ".git", "build"} + + for root, dirs, files in os.walk(repository_absolute_path): + # Skip dependency/build output directories + dirs[:] = [d for d in dirs if d not in skip_dirs] + depth = len(pathlib.Path(root).relative_to(repository_absolute_path).parts) + if depth > max_depth: + dirs.clear() + continue + for f in files: + if f.endswith(".hxml") and "haxe_libraries" not in root: + hxml_path = os.path.relpath(os.path.join(root, f), repository_absolute_path) + log.info( + "Auto-discovered Haxe build file: %s. To use a different file, set " + "ls_specific_settings.haxe.buildFile in serena_config.yml.", + hxml_path, + ) + return [hxml_path] + + log.info("No .hxml file found in project") + return [] + + @override + def _start_server(self) -> None: + """Start the Haxe Language Server and wait for initial compilation. + + Uses textDocument/publishDiagnostics as the primary compilation-complete signal + and $/progress tokens as a secondary signal. + """ + + def diagnostics_handler(params: dict) -> None: + """Signal compilation complete when diagnostics arrive. + Defers if progress tokens are still active to avoid a race condition. + """ + uri = params.get("uri", "unknown") + diags = params.get("diagnostics", []) + errors = [d for d in diags if d.get("severity") == DiagnosticSeverity.Error] + if errors: + log.warning("Haxe LSP diagnostics for %s: %d errors: %s", uri, len(errors), errors) + else: + log.info("Haxe LSP diagnostics for %s: clean (%d total)", uri, len(diags)) + + with self._progress_lock: + if not self._active_progress_tokens: + log.info("Haxe LSP: no active progress tokens — signalling compilation complete") + self._server_ready.set() + else: + log.info( + "Haxe LSP: diagnostics received but %d progress tokens still active — deferring", + len(self._active_progress_tokens), + ) + + def window_log_message(msg: dict) -> None: + log.info(f"LSP: window/logMessage: {msg}") + + def register_capability_handler(params: dict) -> None: + # Haxe LS sends this but we don't need dynamic capability registration. + return + + def work_done_progress_create(params: dict) -> dict: + """Handle window/workDoneProgress/create — clear compilation event until tokens finish.""" + token = str(params.get("token", "")) + log.debug(f"Haxe LSP workDoneProgress/create: token={token!r}") + with self._progress_lock: + self._active_progress_tokens.add(token) + self._server_ready.clear() + return {} + + def progress_handler(params: dict) -> None: + """Track $/progress begin/end to detect when all async compilation work finishes.""" + token = str(params.get("token", "")) + value = params.get("value", {}) + kind = value.get("kind") + if kind == "begin": + title = value.get("title", "") + log.info(f"Haxe LSP progress [{token}]: started - {title}") + with self._progress_lock: + self._active_progress_tokens.add(token) + self._server_ready.clear() + elif kind == "report": + pct = value.get("percentage") + msg = value.get("message", "") + pct_str = f" ({pct}%)" if pct is not None else "" + log.debug(f"Haxe LSP progress [{token}]: {msg}{pct_str}") + elif kind == "end": + msg = value.get("message", "") + log.info(f"Haxe LSP progress [{token}]: ended - {msg}") + with self._progress_lock: + self._active_progress_tokens.discard(token) + if not self._active_progress_tokens: + self._server_ready.set() + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_request("window/workDoneProgress/create", work_done_progress_create) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("$/progress", progress_handler) + self.server.on_notification("textDocument/publishDiagnostics", diagnostics_handler) + + log.info("Starting Haxe server process") + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + log.info("Sending initialize request from LSP client to LSP server and awaiting response") + self.server.send.initialize(initialize_params) + + self._server_ready.clear() + self.server.notify.initialized({}) + + # LS doesn't properly initialise without a workspace_did_change_configuration notification here. + self.server.notify.workspace_did_change_configuration({"settings": {}}) + + log.info("Waiting for Haxe LSP compilation to complete...") + if self._server_ready.wait(timeout=self._COMPILATION_TIMEOUT): + log.info("Haxe server compilation completed, server ready") + else: + log.warning( + "Haxe LSP did not signal compilation completion within %.0fs — responses may be incomplete", + self._COMPILATION_TIMEOUT, + ) + + @override + def request_hover(self, relative_file_path: str, line: int, column: int, file_buffer: LSPFileBuffer | None = None) -> Hover | None: + """Request hover info, returning None instead of raising on failure. + + The Haxe language server does not provide hover for all symbol types (e.g. class + declarations), and may raise errors instead of returning None in those cases. + """ + try: + return super().request_hover(relative_file_path, line, column, file_buffer=file_buffer) + except SolidLSPException: + log.warning("Hover request failed for %s:%d:%d", relative_file_path, line, column, exc_info=True) + return None + + @override + def _get_wait_time_for_cross_file_referencing(self) -> float: + return 5 diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 4ec425c1..ab1a4038 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -69,6 +69,11 @@ class Language(str, Enum): JULIA = "julia" FORTRAN = "fortran" HASKELL = "haskell" + HAXE = "haxe" + """Haxe language server using vshaxe/haxe-language-server. + Requires Haxe compiler (3.4.0+) and Node.js. + Discovered from system PATH or vshaxe VSCode extension, otherwise downloaded from Open VSX. + """ LEAN4 = "lean4" GROOVY = "groovy" VUE = "vue" @@ -273,6 +278,8 @@ class Language(str, Enum): ) case self.HASKELL: return FilenameMatcher("*.hs", "*.lhs") + case self.HAXE: + return FilenameMatcher("*.hx") case self.LEAN4: return FilenameMatcher("*.lean") case self.VUE: @@ -493,6 +500,10 @@ class Language(str, Enum): from solidlsp.language_servers.haskell_language_server import HaskellLanguageServer return HaskellLanguageServer + case self.HAXE: + from solidlsp.language_servers.haxe_language_server import HaxeLanguageServer + + return HaxeLanguageServer case self.LEAN4: from solidlsp.language_servers.lean4_language_server import Lean4LanguageServer diff --git a/test/conftest.py b/test/conftest.py index 48443484..ce5761c6 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -255,6 +255,7 @@ _LANGUAGE_PYTEST_MARKERS: dict[Language, list[MarkDecorator | Mark]] = { Language.CSHARP: [pytest.mark.csharp], Language.FSHARP: [pytest.mark.fsharp], Language.GO: [pytest.mark.go], + Language.HAXE: [pytest.mark.haxe], Language.JAVA: [pytest.mark.java], Language.KOTLIN: [pytest.mark.kotlin, pytest.mark.skipif(is_ci, reason="Kotlin LSP JVM crashes on restart in CI")], Language.LEAN4: [pytest.mark.lean4, pytest.mark.skipif(_sh.which("lean") is None, reason="Lean is not installed")], diff --git a/test/resources/repos/haxe/test_repo/.gitignore b/test/resources/repos/haxe/test_repo/.gitignore new file mode 100644 index 00000000..e714affd --- /dev/null +++ b/test/resources/repos/haxe/test_repo/.gitignore @@ -0,0 +1,3 @@ +export/ +dump/ +bin/ diff --git a/test/resources/repos/haxe/test_repo/build.hxml b/test/resources/repos/haxe/test_repo/build.hxml new file mode 100644 index 00000000..40cd557d --- /dev/null +++ b/test/resources/repos/haxe/test_repo/build.hxml @@ -0,0 +1,3 @@ +-cp src +-main Main +--no-output diff --git a/test/resources/repos/haxe/test_repo/src/Main.hx b/test/resources/repos/haxe/test_repo/src/Main.hx new file mode 100644 index 00000000..a0b3252e --- /dev/null +++ b/test/resources/repos/haxe/test_repo/src/Main.hx @@ -0,0 +1,40 @@ +import utils.Helper; + +/** + * Main class for testing Haxe language server functionality. + * + * This class tests: + * - Symbol discovery (class, methods, fields) + * - Within-file references + * - Cross-file references to utils.Helper + */ +class Main { + var message:String; + var count:Int; + + public function new() { + message = greet("World"); + count = Helper.addNumbers(5, 10); + } + + /** + * Greet someone by name. + */ + public function greet(name:String):String { + return "Hello, " + name + "!"; + } + + /** + * Calculate and return the formatted result. + */ + public function calculateResult():String { + var sum = Helper.addNumbers(count, 20); + var formatted = Helper.formatMessage(message); + return formatted + " (sum: " + Std.string(sum) + ")"; + } + + public static function main() { + var app = new Main(); + Sys.println(app.calculateResult()); + } +} diff --git a/test/resources/repos/haxe/test_repo/src/utils/Helper.hx b/test/resources/repos/haxe/test_repo/src/utils/Helper.hx new file mode 100644 index 00000000..0c899504 --- /dev/null +++ b/test/resources/repos/haxe/test_repo/src/utils/Helper.hx @@ -0,0 +1,29 @@ +package utils; + +/** + * Utility functions for the Haxe test application. + * + * This class provides helper functions used by the Main class. + */ +class Helper { + /** + * Format a message by adding brackets around it. + */ + public static function formatMessage(msg:String):String { + return "[ " + msg + " ]"; + } + + /** + * Add two numbers together. + */ + public static function addNumbers(x:Int, y:Int):Int { + return x + y; + } + + /** + * Multiply two numbers. + */ + public static function multiplyNumbers(x:Int, y:Int):Int { + return x * y; + } +} diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index c2c1e61e..01cd29d1 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -45,6 +45,7 @@ def serena_config(): Language.FSHARP, Language.POWERSHELL, Language.CPP_CCLS, + Language.HAXE, Language.LEAN4, ]: repo_path = get_repo_path(language) @@ -196,6 +197,7 @@ class TestSerenaAgent: pytest.param(Language.CSHARP, "Calculator", "Class", "Program.cs", marks=pytest.mark.csharp), pytest.param(Language.POWERSHELL, "Greet-User", "Function", "main.ps1", marks=pytest.mark.powershell), pytest.param(Language.CPP_CCLS, "add", "Function", "b.cpp", marks=pytest.mark.cpp), + pytest.param(Language.HAXE, "Main", "Class", "Main.hx", marks=pytest.mark.haxe), pytest.param(Language.LEAN4, "add", "Method", "Helper.lean", marks=pytest.mark.lean4), ], indirect=["serena_agent"], @@ -297,6 +299,13 @@ class TestSerenaAgent: pytest.param(Language.CSHARP, "Calculator", "Program.cs", "Program.cs", marks=pytest.mark.csharp), pytest.param(Language.POWERSHELL, "Greet-User", "main.ps1", "main.ps1", marks=pytest.mark.powershell), pytest.param(Language.CPP_CCLS, "add", "b.cpp", "a.cpp", marks=pytest.mark.cpp), + pytest.param( + Language.HAXE, + "addNumbers", + os.path.join("src", "utils", "Helper.hx"), + os.path.join("src", "Main.hx"), + marks=pytest.mark.haxe, + ), pytest.param(Language.LEAN4, "add", "Helper.lean", "Main.lean", marks=pytest.mark.lean4), ], indirect=["serena_agent"], diff --git a/test/solidlsp/haxe/__init__.py b/test/solidlsp/haxe/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/solidlsp/haxe/test_haxe_basic.py b/test/solidlsp/haxe/test_haxe_basic.py new file mode 100644 index 00000000..342a04bd --- /dev/null +++ b/test/solidlsp/haxe/test_haxe_basic.py @@ -0,0 +1,249 @@ +import os + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind +from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols + + +@pytest.mark.haxe +class TestHaxeLanguageServer: + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_ls_is_running(self, language_server: SolidLanguageServer) -> None: + """Test that the Haxe language server starts successfully.""" + assert language_server.is_running() + + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: + symbols = language_server.request_full_symbol_tree() + assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main class not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "greet"), "greet method not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "calculateResult"), "calculateResult method not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Helper"), "Helper class not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "addNumbers"), "addNumbers method not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "formatMessage"), "formatMessage method not found in symbol tree" + + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_bare_symbol_names(self, language_server: SolidLanguageServer) -> None: + """Test that symbol names do not contain unexpected formatting characters.""" + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) + + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None: + file_path = os.path.join("src", "Main.hx") + all_symbols, _ = language_server.request_document_symbols(file_path).get_all_symbols_and_roots() + greet_symbol = next((s for s in all_symbols if s.get("name") == "greet"), None) + assert greet_symbol is not None, "Could not find 'greet' symbol in Main.hx" + sel_start = greet_symbol["selectionRange"]["start"] + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + + assert refs, f"Expected non-empty references for greet but got {refs=}" + + # Convert references to a comparable format + actual_locations = [ + { + "uri_suffix": os.path.basename(ref.get("relativePath", ref.get("uri", ""))), + "line": ref["range"]["start"]["line"], + } + for ref in refs + ] + + # greet is called on line 15 (0-indexed) in Main.hx: `message = greet("World");` + call_site = {"uri_suffix": "Main.hx", "line": 15} + assert call_site in actual_locations, f"Expected reference to greet at line 15 in Main.hx, got {actual_locations}" + + # All references should be within Main.hx (greet is not used in other files) + assert all(loc["uri_suffix"] == "Main.hx" for loc in actual_locations), ( + f"Expected all greet references in Main.hx, got {actual_locations}" + ) + + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None: + # Test addNumbers which is defined in Helper.hx and used in Main.hx + helper_path = os.path.join("src", "utils", "Helper.hx") + all_symbols, _ = language_server.request_document_symbols(helper_path).get_all_symbols_and_roots() + add_numbers_symbol = next((s for s in all_symbols if s.get("name") == "addNumbers"), None) + assert add_numbers_symbol is not None, "Could not find 'addNumbers' symbol in Helper.hx" + + sel_start = add_numbers_symbol["selectionRange"]["start"] + refs = language_server.request_references(helper_path, sel_start["line"], sel_start["character"]) + + assert refs, f"Expected non-empty references for addNumbers but got {refs=}" + + # Convert references to a comparable format + actual_locations = [ + { + "uri_suffix": os.path.basename(ref.get("relativePath", ref.get("uri", ""))), + "line": ref["range"]["start"]["line"], + } + for ref in refs + ] + + # addNumbers is called on line 16 in Main.hx: `count = Helper.addNumbers(5, 10);` + call_site_1 = {"uri_suffix": "Main.hx", "line": 16} + assert call_site_1 in actual_locations, f"Expected reference to addNumbers at line 16 in Main.hx, got {actual_locations}" + + # addNumbers is called on line 30 in Main.hx: `var sum = Helper.addNumbers(count, 20);` + call_site_2 = {"uri_suffix": "Main.hx", "line": 30} + assert call_site_2 in actual_locations, f"Expected reference to addNumbers at line 30 in Main.hx, got {actual_locations}" + + # Verify cross-file: at least one reference is in Main.hx (different file from definition) + main_refs = [loc for loc in actual_locations if loc["uri_suffix"] == "Main.hx"] + assert len(main_refs) >= 2, f"Expected at least 2 references in Main.hx (lines 16 and 30), got {main_refs}" + + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_document_symbols_structure(self, language_server: SolidLanguageServer) -> None: + file_path = os.path.join("src", "Main.hx") + result = language_server.request_document_symbols(file_path) + all_symbols, roots = result.get_all_symbols_and_roots() + + # Main class should be a root symbol + main_symbol = None + for sym in roots: + if sym.get("name") == "Main": + main_symbol = sym + break + assert main_symbol is not None, "Main class not found as root symbol" + assert main_symbol.get("kind") in (SymbolKind.Class, SymbolKind.Struct), f"Expected Main to be Class, got {main_symbol.get('kind')}" + + # Check that methods and fields exist and are children of Main + child_names = {s.get("name") for s in all_symbols if s.get("name") != "Main"} + assert "greet" in child_names, "greet method not found in symbols" + assert "calculateResult" in child_names, "calculateResult method not found in symbols" + assert "message" in child_names, "message field not found in symbols" + assert "count" in child_names, "count field not found in symbols" + + # Verify symbol kinds for specific symbols + for sym in all_symbols: + if sym.get("name") == "greet": + assert sym.get("kind") in ( + SymbolKind.Method, + SymbolKind.Function, + ), f"Expected greet to be Method/Function, got {sym.get('kind')}" + if sym.get("name") == "message": + assert sym.get("kind") in ( + SymbolKind.Field, + SymbolKind.Variable, + SymbolKind.Property, + ), f"Expected message to be Field/Variable, got {sym.get('kind')}" + + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_workspace_symbol(self, language_server: SolidLanguageServer) -> None: + result = language_server.request_workspace_symbol("Helper") + assert result is not None, "Workspace symbol search returned None" + assert len(result) > 0, "Workspace symbol search returned no results" + assert any("Helper" in str(s.get("name", "")) for s in result), f"Expected at least one result containing 'Helper', got {result}" + + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_go_to_definition_within_file(self, language_server: SolidLanguageServer) -> None: + """Go to definition of greet from its call site in Main.hx -- should resolve within the same file.""" + main_path = os.path.join("src", "Main.hx") + # Line 16 (0-indexed: 15): `message = greet("World");` + # `greet` starts at character 12 on that line. + definitions = language_server.request_definition(main_path, 15, 12) + assert definitions, "Expected to find definition for greet" + assert any("Main.hx" in d.get("uri", d.get("relativePath", "")) for d in definitions), ( + f"Expected definition in Main.hx, got {definitions}" + ) + # greet is defined on line 23 (0-indexed: 22) in Main.hx + definition_lines = [d["range"]["start"]["line"] for d in definitions if "Main.hx" in d.get("uri", d.get("relativePath", ""))] + assert 22 in definition_lines, f"Expected definition of greet at line 22 in Main.hx, got lines {definition_lines}" + + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_go_to_definition(self, language_server: SolidLanguageServer) -> None: + """Go to definition of addNumbers from a call site in Main.hx -- should resolve to Helper.hx.""" + main_path = os.path.join("src", "Main.hx") + # Line 17 (0-indexed: 16): `\t\tcount = Helper.addNumbers(5, 10);` + # `addNumbers` starts at character 17 (0-indexed) on that line. + definitions = language_server.request_definition(main_path, 16, 17) + assert definitions, "Expected to find definition for addNumbers" + assert any("Helper.hx" in d.get("uri", d.get("relativePath", "")) for d in definitions), ( + f"Expected definition in Helper.hx, got {definitions}" + ) + # addNumbers is defined on line 18 (0-indexed) in Helper.hx + definition_lines = [d["range"]["start"]["line"] for d in definitions if "Helper.hx" in d.get("uri", d.get("relativePath", ""))] + assert 18 in definition_lines, f"Expected definition of addNumbers at line 18 in Helper.hx, got lines {definition_lines}" + + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_hover(self, language_server: SolidLanguageServer) -> None: + file_path = os.path.join("src", "Main.hx") + result = language_server.request_document_symbols(file_path) + all_symbols, _ = result.get_all_symbols_and_roots() + greet_symbol = next((s for s in all_symbols if s.get("name") == "greet"), None) + assert greet_symbol is not None, "Could not find 'greet' symbol" + + sel_start = greet_symbol["selectionRange"]["start"] + hover = language_server.request_hover(file_path, sel_start["line"], sel_start["character"]) + assert hover is not None, "Hover returned None for greet method" + hover_str = str(hover) + assert "String" in hover_str or "greet" in hover_str, f"Expected hover to contain type info, got {hover_str}" + + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_hover_on_class_declaration(self, language_server: SolidLanguageServer) -> None: + """Hovering on a class name should return hover info.""" + file_path = os.path.join("src", "Main.hx") + result = language_server.request_document_symbols(file_path) + all_symbols, _ = result.get_all_symbols_and_roots() + + main_symbol = next((s for s in all_symbols if s.get("name") == "Main"), None) + assert main_symbol is not None + + sel_start = main_symbol["selectionRange"]["start"] + hover = language_server.request_hover(file_path, sel_start["line"], sel_start["character"]) + assert hover is not None, "Hover on class declaration returned None" + hover_str = str(hover) + assert "Main" in hover_str, f"Expected hover to contain 'Main', got {hover_str}" + + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_rename_symbol(self, language_server: SolidLanguageServer) -> None: + file_path = os.path.join("src", "Main.hx") + result = language_server.request_document_symbols(file_path) + all_symbols, _ = result.get_all_symbols_and_roots() + greet_symbol = next((s for s in all_symbols if s.get("name") == "greet"), None) + assert greet_symbol is not None, "Could not find 'greet' symbol" + + sel_start = greet_symbol["selectionRange"]["start"] + edits = language_server.request_rename_symbol_edit(file_path, sel_start["line"], sel_start["character"], "sayHello") + assert edits is not None, "Rename returned None" + # Verify edits contain changes (WorkspaceEdit has 'changes' or 'documentChanges') + edits_str = str(edits) + assert "Main.hx" in edits_str, f"Expected rename edits for Main.hx, got {edits}" + assert "sayHello" in edits_str, f"Expected new name 'sayHello' in rename edits, got {edits}" + + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_completions(self, language_server: SolidLanguageServer) -> None: + """Request completions after Helper. in calculateResult — should return Helper's static methods.""" + file_path = os.path.join("src", "Main.hx") + # Line 31 (0-indexed: 30): `var sum = Helper.addNumbers(count, 20);` + # Trigger completions after `Helper.` — character 19 is right after the dot. + completions = language_server.request_completions(file_path, 30, 19) + assert completions, "Expected non-empty completions after Helper." + completion_texts = [c.get("completionText", c.get("label", "")) for c in completions] + assert "addNumbers" in completion_texts, f"Expected 'addNumbers' in completions after Helper., got {completion_texts[:10]}" + + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_document_overview(self, language_server: SolidLanguageServer) -> None: + overview = language_server.request_document_overview(os.path.join("src", "Main.hx")) + assert overview, "Document overview returned empty list" + symbol_names = [s.get("name", "") for s in overview] + assert any("Main" in name for name in symbol_names), f"Expected 'Main' in overview, got {symbol_names}" + main_entry = next((s for s in overview if s.get("name") == "Main"), None) + assert main_entry is not None, f"Expected 'Main' entry in overview, got {symbol_names}" + assert main_entry.get("kind") in (SymbolKind.Class, SymbolKind.Struct), ( + f"Expected Main to be Class/Struct in overview, got kind {main_entry.get('kind')}" + ) + for s in overview: + assert s.get("name"), "Symbol missing 'name' field" + assert s.get("kind"), "Symbol missing 'kind' field" From 6197f6d8e6d366f2739871d09e76cc318b446a45 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 15:33:35 +0200 Subject: [PATCH 093/190] Fixed global hook grace period --- src/serena/hooks.py | 49 +++++++++++++++++++++++---------------- test/serena/test_hooks.py | 37 ++++++++++++++++++----------- 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/src/serena/hooks.py b/src/serena/hooks.py index 4a04e9f4..4a0ff12f 100644 --- a/src/serena/hooks.py +++ b/src/serena/hooks.py @@ -103,11 +103,13 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook): increment nor reset counters, so they also do not mask bursts by pushing the last timestamp forward. - Deny emissions are additionally rate-limited to at most one per - :attr:`ToolUseCounter._MIN_DENY_INTERVAL_SECONDS` (one minute): once a deny has - been emitted, subsequent bursts detected within that window still reset the - counters but do not produce further nudges, so the agent is not swamped with - reminders during a sustained non-symbolic-tool burst. + The hook is additionally gated by :attr:`ToolUseCounter._MIN_DENY_INTERVAL_SECONDS` + (two minutes by default): once a deny has been emitted, *every* subsequent + invocation of this hook is a no-op until the window has elapsed — neither the + counters are updated nor any further deny is produced. This prevents the agent + from being nudged more than once per window during a sustained non-symbolic-tool + burst, and also avoids surprising the user with reminders that were already + counted up under stale state. """ @dataclass @@ -124,8 +126,9 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook): # reset period for the combined counter _NON_SYMBOLIC_RESET_PERIOD_SECONDS = 2000 - # minimum seconds between two emitted denies; rate-limits reminder nudges so - # a single sustained burst triggers at most one deny per minute + # minimum seconds between two engagements of the hook after a deny; the entire + # hook (counter updates included) is a no-op while this window is active, so a + # single sustained burst triggers at most one nudge per window _MIN_DENY_INTERVAL_SECONDS = 120 n_recent_read_file_uses: int = 0 @@ -147,13 +150,13 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook): def too_many_recent_non_symbolic(self) -> bool: return self.n_recent_non_symbolic_uses >= self._NON_SYMBOLIC_USES_THRESHOLD - def can_emit_deny(self, now: datetime) -> bool: - """:return: whether enough time has elapsed since the last emitted deny - to allow another one. A value of ``True`` is also returned when no - deny has been emitted yet in this session. The rate limit prevents - the agent from being nudged more than once per - :attr:`_MIN_DENY_INTERVAL_SECONDS`, even if further bursts are - detected in the meantime. + def is_hook_active(self, now: datetime) -> bool: + """:return: whether the hook should engage at all at ``now``. Returns + ``False`` while we are still within :attr:`_MIN_DENY_INTERVAL_SECONDS` + of the most recent emitted deny — in that case the entire hook is + short-circuited (no counter updates, no deny). Returns ``True`` when + no deny has been emitted yet in this session, or when the window has + elapsed. """ if self.last_deny_timestamp is None: return True @@ -258,6 +261,13 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook): return any(verb in name for verb in self._READ_FILE_VERB_SUBSTRINGS) def execute(self) -> None: + # gate the entire hook on the rate-limit window: while we are within + # _MIN_DENY_INTERVAL_SECONDS of the last emitted deny, no counter + # updates and no deny detection happen at all. The pickle is left + # untouched so state survives the gating window unchanged. + if not self._tool_call_counter.is_hook_active(self.triggered_at_timestamp): + return + self._tool_call_counter.update(self) # pick the deny that matches the current call first, so the emitted message lines up @@ -282,13 +292,12 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook): output_data = self._build_non_symbolic_deny() if output_data is not None: - # reset first so burst detection starts fresh regardless of whether the - # deny is actually emitted; the rate limit only gates *emission*, not - # detection + # reset burst counters so the next interval starts fresh, then record + # the deny timestamp and emit. The is_hook_active() guard at the top + # ensures we never reach this branch within the rate-limit window. self._tool_call_counter.reset() - if self._tool_call_counter.can_emit_deny(self.triggered_at_timestamp): - self._tool_call_counter.last_deny_timestamp = self.triggered_at_timestamp - click.echo(output_data.to_json_string()) + self._tool_call_counter.last_deny_timestamp = self.triggered_at_timestamp + click.echo(output_data.to_json_string()) self._tool_call_counter.save(self) def _build_grep_deny(self) -> "PreToolUseHook.OutputData": diff --git a/test/serena/test_hooks.py b/test/serena/test_hooks.py index a98714fd..25408f0d 100644 --- a/test/serena/test_hooks.py +++ b/test/serena/test_hooks.py @@ -180,8 +180,10 @@ class TestPreToolUseRemindAboutSerenaHook: assert capsys.readouterr().out == "" - def test_rate_limit_suppresses_second_deny_within_interval(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): - """A second burst detected within the minimum deny interval must not emit a deny.""" + def test_rate_limit_gates_entire_hook_within_interval(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): + """While within the rate-limit window, the entire hook must be a no-op: + no deny is emitted, AND the persisted counters must remain untouched. + """ # first burst: should emit a deny for _ in range(ToolUseCounter._GREP_USES_THRESHOLD): with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): @@ -189,13 +191,22 @@ class TestPreToolUseRemindAboutSerenaHook: first_output = capsys.readouterr().out.strip() assert first_output, "first burst should have emitted a deny" - # second burst immediately after: within the 1-minute window, must be suppressed + # snapshot the persisted counter immediately after the deny was emitted + stub_for_path = object.__new__(PreToolUseRemindAboutSerenaHook) + stub_for_path.session_persistence_dir = str(tmp_path / "hook_data" / _base_input()["session_id"]) + counter_before = ToolUseCounter.load(stub_for_path) + + # second burst immediately after: within the rate-limit window, the entire + # hook must short-circuit — no deny output and no counter mutation for _ in range(ToolUseCounter._GREP_USES_THRESHOLD): with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)): PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute() assert capsys.readouterr().out == "" + counter_after = ToolUseCounter.load(stub_for_path) + assert counter_after == counter_before, "gated hook must not mutate the persisted counter" + def test_rate_limit_allows_deny_after_interval(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]): """Once the minimum deny interval has elapsed, a fresh burst emits a deny again.""" # first burst: emits a deny @@ -348,23 +359,23 @@ class TestToolUseCounter: loaded = ToolUseCounter.load(hook_stub) # type: ignore[arg-type] assert loaded == ToolUseCounter() - def test_can_emit_deny_respects_min_interval(self): - """:meth:`can_emit_deny` returns False within the minimum interval, True outside it.""" + def test_is_hook_active_respects_min_interval(self): + """:meth:`is_hook_active` returns False within the minimum interval, True outside it.""" counter = ToolUseCounter() base = datetime.now() - # no prior deny → always allowed - assert counter.can_emit_deny(base) + # no prior deny → hook is always active + assert counter.is_hook_active(base) - # within the interval → rate-limited + # within the interval → hook gated counter.last_deny_timestamp = base interval = ToolUseCounter._MIN_DENY_INTERVAL_SECONDS - assert not counter.can_emit_deny(base + timedelta(seconds=interval - 1)) - assert not counter.can_emit_deny(base) + assert not counter.is_hook_active(base + timedelta(seconds=interval - 1)) + assert not counter.is_hook_active(base) - # at/after the interval → allowed again - assert counter.can_emit_deny(base + timedelta(seconds=interval)) - assert counter.can_emit_deny(base + timedelta(seconds=interval + 1)) + # at/after the interval → hook active again + assert counter.is_hook_active(base + timedelta(seconds=interval)) + assert counter.is_hook_active(base + timedelta(seconds=interval + 1)) def test_reset_preserves_last_deny_timestamp(self): """``reset`` clears burst counters but must keep ``last_deny_timestamp`` intact.""" From 1ef3e35fb0bf475337add68cd937ebf3b338fba0 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 15:34:03 +0200 Subject: [PATCH 094/190] Removed search_for_pattern from claude-code context --- src/serena/resources/config/contexts/claude-code.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/serena/resources/config/contexts/claude-code.yml b/src/serena/resources/config/contexts/claude-code.yml index 6678c913..b6d632a7 100644 --- a/src/serena/resources/config/contexts/claude-code.yml +++ b/src/serena/resources/config/contexts/claude-code.yml @@ -4,11 +4,12 @@ prompt: | as well as shell commands are handled by your own, internal tools. If Serena's tools can be used to achieve your task, you should prioritize them. - **IMPORTANT**: If you haven't seen Serena's tools yet, you should use the tool search tool to explore them. - Avoid reading entire source code files unless it is strictly necessary! + **IMPORTANT**: If you haven't seen Serena's tools yet, you should use the tool search tool to load all of them + right now. For exploring and reading code in a token-efficient manner, use Serena's code-centric overview and symbolic search tools. For edits, where it makes sense, use Serena's editing tools since they are more token efficient. Use your own editing tools only if Serena's editing tools would be suboptimal. + **IMPORTANT**: Never use a tool before reading its schema via the tool-search tool! excluded_tools: @@ -18,6 +19,7 @@ excluded_tools: - replace_content - find_file - list_dir + - search_for_pattern tool_description_overrides: {} From 8b76fc2846d45b784fe62d757f3b3d7f1e1c2068 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 15:34:13 +0200 Subject: [PATCH 095/190] Readme: callout on installation --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index b63f3e78..033eaac8 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ approaches that rely on low-level concepts like line numbers or primitive search Practically, this means that your agent operates **faster, more efficiently and more reliably**, especially in larger and more complex codebases. +> [!IMPORTANT] +> Don't install Serena via an MCP or plugin marketplace! They contain outdated and suboptimal installation commands. Instead, follow our [Quick Start](#quick-start) instructions. + ## How Serena Works Serena provides the necessary [tools](https://oraios.github.io/serena/01-about/035_tools.html) for coding workflows, From e22893862cc0f715af93b0b1710d077686e23c74 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 10 Apr 2026 16:02:23 +0200 Subject: [PATCH 096/190] ClientSetupClaudeCode: Add hook recommendation with link to docs --- src/serena/config/client_setup.py | 33 +++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/serena/config/client_setup.py b/src/serena/config/client_setup.py index ba0fc23d..9116d01f 100644 --- a/src/serena/config/client_setup.py +++ b/src/serena/config/client_setup.py @@ -1,5 +1,7 @@ from abc import ABC, abstractmethod +import click + from serena.util.shell import execute_shell_command @@ -20,6 +22,25 @@ class ClientSetupHandler(ABC): def get_mcp_server_command(self) -> str: return f"serena start-mcp-server {' '.join(self.get_mcp_server_options())}" + def _run_shell_command(self, cmd: str) -> bool: + """ + Runs the given shell command. + If the command fails (i.e., with non-zero exit code), prints the stdout and stderr of the command for debugging. + + :param cmd: the shell command to execute + :return: whether the command executed successfully (i.e., with exit code 0) + """ + click.echo("Running command:") + click.echo(cmd) + result = execute_shell_command(cmd) + is_success = result.return_code == 0 + if not is_success: + if result.stdout: + click.echo(result.stdout) + if result.stderr: + click.echo(result.stderr) + return is_success + @abstractmethod def apply(self) -> bool: """ @@ -39,8 +60,13 @@ class ClientSetupHandlerClaudeCode(ClientSetupHandler): return ["--context=claude-code", "--project-from-cwd"] def apply(self) -> bool: - result = execute_shell_command(f"claude mcp add --scope user serena -- {self.get_mcp_server_command()}") - return result.return_code == 0 + cmd = f"claude mcp add --scope user serena -- {self.get_mcp_server_command()}" + is_success = self._run_shell_command(cmd) + if is_success: + click.echo("\nIMPORTANT: We additionally recommend to set up hooks for Claude Code to ensure the best experience.") + click.echo(" Please read the instructions here:") + click.echo(" https://oraios.github.io/serena/02-usage/030_clients.html#claude-code") + return is_success class ClientSetupHandlerCodex(ClientSetupHandler): @@ -59,8 +85,7 @@ class ClientSetupHandlerCodex(ClientSetupHandler): return ["--context=codex", "--project-from-cwd"] def apply(self) -> bool: - result = execute_shell_command(f"codex mcp add serena -- {self.get_mcp_server_command()}") - return result.return_code == 0 + return self._run_shell_command(f"codex mcp add serena -- {self.get_mcp_server_command()}") client_setup_handlers = [ClientSetupHandlerClaudeCode(), ClientSetupHandlerCodex()] From ddb0d9c95b3a844bf28893843ac88fa6151534e2 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 16:41:36 +0200 Subject: [PATCH 097/190] Onboarding tools: improve messages when memory tools are not available --- src/serena/agent.py | 7 +++++++ src/serena/tools/workflow_tools.py | 17 ++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index f3c4d6ab..694320e3 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -876,6 +876,13 @@ class SerenaAgent: """ return self._active_tools.contains_tool_name(tool_name) + def tool_is_exposed(self, tool_name: str) -> bool: + """ + :param tool_name: the name of the tool to check + :return: True if the tool is in the exposed tool set, False otherwise + """ + return self._exposed_tools.contains_tool_name(tool_name) + def get_current_config_overview(self) -> str: """ :return: a string overview of the current configuration, including the active and available configuration options diff --git a/src/serena/tools/workflow_tools.py b/src/serena/tools/workflow_tools.py index e8bc67b4..7b1f5d96 100644 --- a/src/serena/tools/workflow_tools.py +++ b/src/serena/tools/workflow_tools.py @@ -4,7 +4,7 @@ Tools supporting the general workflow of the agent import platform -from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject +from serena.tools import ReadMemoryTool, Tool, ToolMarkerDoesNotRequireActiveProject, WriteMemoryTool class CheckOnboardingPerformedTool(Tool): @@ -17,12 +17,16 @@ class CheckOnboardingPerformedTool(Tool): Checks whether project onboarding was already performed. You should always call this tool before beginning to actually work on the project/after activating a project. """ + read_memory_tool_available = self.agent.tool_is_exposed(ReadMemoryTool.get_name_from_cls()) + perform_onboarding_tool_available = self.agent.tool_is_exposed(OnboardingTool.get_name_from_cls()) + + if not read_memory_tool_available: + return "Memory reading tool not activated, skipping onboarding check." project_memories = self.memories_manager.list_project_memories() if len(project_memories) == 0: - msg = ( - "Onboarding not performed yet (no memories available). " - "You should perform onboarding by calling the `onboarding` tool before proceeding with the task. " - ) + msg = "Onboarding not performed yet (no memories available). " + if perform_onboarding_tool_available: + msg += "You should perform onboarding by calling the `onboarding` tool before proceeding with the task. " else: # Not reporting the list of memories here, as they were already reported at project activation # (with the system prompt if the project was activated at startup) @@ -46,6 +50,9 @@ class OnboardingTool(Tool): :return: instructions on how to create the onboarding information """ + write_memory_tool_available = self.agent.tool_is_exposed(WriteMemoryTool.get_name_from_cls()) + if not write_memory_tool_available: + return "Memory writing tool not activated, skipping onboarding." system = platform.system() return self.prompt_factory.create_onboarding_prompt(system=system) From d572f48e4c1a963a3ae96385894204db905e2da1 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 17:20:33 +0200 Subject: [PATCH 098/190] Use our fork of pywebview until upstream releases --- pyproject.toml | 4 +--- uv.lock | 44 ++++++++++++++++++++++++-------------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dc5b8f29..435d88f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "tiktoken==0.12.0", "anthropic==0.59.0", "beautifulsoup4==4.14.2", - "pywebview", + "oraios-pywebview==6.2", "pystray==0.19.5", # Transitive deps pinned for security (dependabot alerts). # Exact pins because uvx installs from git, ignoring the lock file. @@ -50,8 +50,6 @@ dependencies = [ "pythonnet==3.1.0-rc0 ; sys_platform == 'win32'", ] -[tool.uv.sources] -pywebview = { git = "https://github.com/r0x0r/pywebview", rev = "345596c1f4fd59fd26c7aa02d4176c5362bcbabe" } [[tool.uv.index]] name = "testpypi" diff --git a/uv.lock b/uv.lock index b79d5624..9cf66c5f 100644 --- a/uv.lock +++ b/uv.lock @@ -1685,6 +1685,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "oraios-pywebview" +version = "6.2" +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/0d/e4/6820d2a00b42621240762947c9e07d83acbb1251575349472a48fe9b1a9f/oraios_pywebview-6.2.tar.gz", hash = "sha256:2b176547321ff9c289ebb10d5b2423bc8887a4fc730b35a78656de7839e5be37", size = 499560, upload-time = "2026-04-10T15:01:42.363Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/76/ff666b5abc3b3ccc35f69d62b27290b75b7ef5ac4beb2c59e8f59f5da5ef/oraios_pywebview-6.2-py3-none-any.whl", hash = "sha256:c5b81d2f6cf4cbfdf3d608d5bc3e76d8f0a732dbdd8567660a9bc9cebdd35b84", size = 512739, upload-time = "2026-04-10T15:01:43.689Z" }, +] + [[package]] name = "overrides" version = "7.7.0" @@ -2412,24 +2434,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/3d/a378aa74085fc6f72923d5e39fc30efbc97541cfd49b3fe0a35f20d09ff7/pythonnet-3.1.0rc0-py3-none-any.whl", hash = "sha256:286ccf23d08e1cc2b5200bb91d0ee649436d2e8386fec36a85193c66d0ff8b5b", size = 293150, upload-time = "2026-02-25T10:46:48.535Z" }, ] -[[package]] -name = "pywebview" -version = "6.2.dev50+g345596c1f" -source = { git = "https://github.com/r0x0r/pywebview?rev=345596c1f4fd59fd26c7aa02d4176c5362bcbabe#345596c1f4fd59fd26c7aa02d4176c5362bcbabe" } -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" }, -] - [[package]] name = "pywin32" version = "311" @@ -2921,6 +2925,7 @@ dependencies = [ { name = "jinja2" }, { name = "joblib" }, { name = "mcp" }, + { name = "oraios-pywebview" }, { name = "overrides" }, { name = "pathspec" }, { name = "psutil" }, @@ -2930,7 +2935,6 @@ dependencies = [ { name = "python-dotenv" }, { name = "python-multipart" }, { name = "pythonnet", marker = "sys_platform == 'win32'" }, - { name = "pywebview" }, { name = "pyyaml" }, { name = "regex" }, { name = "requests" }, @@ -2998,6 +3002,7 @@ requires-dist = [ { name = "mypy", marker = "extra == 'dev'", specifier = "==1.17.0" }, { name = "nbconvert", marker = "extra == 'dev'", specifier = "==7.17.0" }, { name = "nbsphinx", marker = "extra == 'dev'", specifier = "==0.9.7" }, + { name = "oraios-pywebview", specifier = "==6.2" }, { name = "overrides", specifier = "==7.7.0" }, { name = "pathspec", specifier = "==0.12.1" }, { name = "poethepoet", marker = "extra == 'dev'", specifier = "==0.36.0" }, @@ -3014,7 +3019,6 @@ requires-dist = [ { name = "python-dotenv", specifier = "==1.2.1" }, { name = "python-multipart", specifier = "==0.0.22" }, { name = "pythonnet", marker = "sys_platform == 'win32'", specifier = "==3.1.0rc0" }, - { name = "pywebview", git = "https://github.com/r0x0r/pywebview?rev=345596c1f4fd59fd26c7aa02d4176c5362bcbabe" }, { name = "pyyaml", specifier = "==6.0.2" }, { name = "regex", specifier = "==2026.2.28" }, { name = "requests", specifier = "==2.33.0" }, From 39f341a0691dc5691c6ff81cc6b16e7455b7d71b Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 17:29:19 +0200 Subject: [PATCH 099/190] Changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a672fcde..d2deeade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. * General: - - Added `serena init` command + - Added `serena init` and `serena setup` commands + - **Major**: added commands for hooks and documentation on recommended setup. Consider setting up the [recommended hooks](https://oraios.github.io/serena/02-usage/030_clients.html) ! + - Reworked installation instructions, switching to releases on pypi for distribution. Please update your mcp startup commands! * Language Servers: - Added Crystal language support (uses [Crystalline](https://github.com/elbywan/crystalline) language server) From addccfa41f2c994d54c4ce54ef837d83869c3b16 Mon Sep 17 00:00:00 2001 From: Tobias Jansing Date: Fri, 10 Apr 2026 17:49:48 +0200 Subject: [PATCH 100/190] Improve Haxe LS: configurable version/haxePath, top-level imports, CI docs, regression test --- .github/workflows/pytest.yml | 2 + docs/02-usage/050_configuration.md | 3 + .../language_servers/haxe_language_server.py | 66 +++++++++++-------- test/solidlsp/haxe/test_haxe_basic.py | 28 ++++++++ 4 files changed, 71 insertions(+), 28 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 133c4632..f1d969e9 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -387,6 +387,8 @@ jobs: shell: bash run: uv run pip install ansible-core ansible-lint - name: Install Haxe + # The Haxe LS binary (server.js) is auto-downloaded and runs on Node.js, + # but it delegates to the Haxe compiler for code analysis at runtime. uses: krdlab/setup-haxe@v2 with: haxe-version: 4.3.7 diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 2c4ff1f5..1b9ba5a0 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -496,7 +496,9 @@ Supported settings: | Setting | Default | Description | |---|---|---| | `ls_path` | auto-discovered | Path to the Haxe language server binary (e.g., `/path/to/server.js`). | +| `version` | `2.34.2` | Override the vshaxe extension version downloaded from Open VSX. SHA256 verification is only performed for the default version. | | `buildFile` | auto-discovered `.hxml` | Relative path to the `.hxml` build file used for compilation (e.g., `build/debug.hxml`). If not set, Serena searches the project for `.hxml` files (max depth 5, skipping dependency directories). | +| `haxePath` | `haxe` from PATH | Path to the Haxe compiler executable. The LS delegates to this for code analysis. Useful when multiple Haxe versions are installed or when `haxe` is not on the PATH. | | `renameSourceFolders` | not set (LS default) | List of source directories for scoping rename operations (e.g., `["src", "lib"]`). If not set, the Haxe LS uses its own defaults. | Example (typically in `project.yml`, since these are project-specific): @@ -505,6 +507,7 @@ Example (typically in `project.yml`, since these are project-specific): ls_specific_settings: haxe: buildFile: "build/debug.hxml" + haxePath: "/usr/local/bin/haxe" renameSourceFolders: ["src", "lib"] ``` diff --git a/src/solidlsp/language_servers/haxe_language_server.py b/src/solidlsp/language_servers/haxe_language_server.py index b2248b97..41c63a2e 100644 --- a/src/solidlsp/language_servers/haxe_language_server.py +++ b/src/solidlsp/language_servers/haxe_language_server.py @@ -6,7 +6,10 @@ import logging import os import pathlib import shutil +import tempfile import threading +import urllib.request +import zipfile from overrides import override @@ -55,9 +58,8 @@ class HaxeLanguageServer(SolidLanguageServer): class DependencyProvider(LanguageServerDependencyProviderSinglePath): # Downloaded from Open VSX (not the VS Code Marketplace) because Open VSX # provides stable versioned URLs and SHA256 checksums for integrity verification. - _VSHAXE_VERSION = "2.34.2" - _VSHAXE_SHA256 = "104d785e3f7b57a7f3debf520d9751f7e7abf3a7e78d203db1a8ff3dc7ca30e2" - _VSHAXE_DOWNLOAD_URL = f"https://open-vsx.org/api/nadako/vshaxe/{_VSHAXE_VERSION}/file/nadako.vshaxe-{_VSHAXE_VERSION}.vsix" + _DEFAULT_VSHAXE_VERSION = "2.34.2" + _DEFAULT_VSHAXE_SHA256 = "104d785e3f7b57a7f3debf520d9751f7e7abf3a7e78d203db1a8ff3dc7ca30e2" @override def _get_or_install_core_dependency(self) -> str: @@ -90,7 +92,8 @@ class HaxeLanguageServer(SolidLanguageServer): " 3. Set ls_path in serena_config.yml under ls_specific_settings.haxe" ) - downloaded_path = self._download_from_open_vsx(haxe_ls_dir) + version = self._custom_settings.get("version", self._DEFAULT_VSHAXE_VERSION) + downloaded_path = self._download_from_open_vsx(haxe_ls_dir, version) if downloaded_path: return downloaded_path @@ -116,32 +119,31 @@ class HaxeLanguageServer(SolidLanguageServer): return None @classmethod - def _download_from_open_vsx(cls, target_dir: str) -> str | None: - """Download a pinned vshaxe VSIX from Open VSX and extract server.js. - Verifies the download against a hardcoded SHA256 checksum. + def _download_from_open_vsx(cls, target_dir: str, version: str) -> str | None: + """Download a vshaxe VSIX from Open VSX and extract server.js. + Verifies the download against a hardcoded SHA256 checksum when using the default version. """ - import tempfile - import zipfile - try: - import urllib.request - - log.info("Downloading Haxe Language Server v%s from Open VSX...", cls._VSHAXE_VERSION) + download_url = f"https://open-vsx.org/api/nadako/vshaxe/{version}/file/nadako.vshaxe-{version}.vsix" + log.info("Downloading Haxe Language Server v%s from Open VSX...", version) vsix_path = os.path.join(tempfile.gettempdir(), "vshaxe.vsix") - urllib.request.urlretrieve(cls._VSHAXE_DOWNLOAD_URL, vsix_path) + urllib.request.urlretrieve(download_url, vsix_path) - # Verify SHA256 checksum - sha256 = hashlib.sha256() - with open(vsix_path, "rb") as f: - for chunk in iter(lambda: f.read(8192), b""): - sha256.update(chunk) - if sha256.hexdigest().lower() != cls._VSHAXE_SHA256: - os.remove(vsix_path) - raise RuntimeError( - f"SHA256 checksum mismatch for vshaxe VSIX. Expected {cls._VSHAXE_SHA256}, " - f"got {sha256.hexdigest()}. The file may be corrupted or tampered with." - ) - log.info("SHA256 checksum verified") + # Verify SHA256 checksum only for the default (pinned) version + if version == cls._DEFAULT_VSHAXE_VERSION: + sha256 = hashlib.sha256() + with open(vsix_path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + sha256.update(chunk) + if sha256.hexdigest().lower() != cls._DEFAULT_VSHAXE_SHA256: + os.remove(vsix_path) + raise RuntimeError( + f"SHA256 checksum mismatch for vshaxe VSIX. Expected {cls._DEFAULT_VSHAXE_SHA256}, " + f"got {sha256.hexdigest()}. The file may be corrupted or tampered with." + ) + log.info("SHA256 checksum verified") + else: + log.info("Using custom version %s — skipping SHA256 verification", version) # VSIX files are ZIP archives — extract bin/ contents bin_dir = os.path.join(target_dir, "bin") @@ -160,7 +162,7 @@ class HaxeLanguageServer(SolidLanguageServer): server_js_path = os.path.join(bin_dir, "server.js") if os.path.exists(server_js_path): - log.info(f"Haxe Language Server v{cls._VSHAXE_VERSION} installed to {server_js_path}") + log.info(f"Haxe Language Server v{version} installed to {server_js_path}") return server_js_path log.error("Downloaded VSIX but server.js not found after extraction") @@ -205,6 +207,10 @@ class HaxeLanguageServer(SolidLanguageServer): if rename_source_folders: init_options["renameSourceFolders"] = rename_source_folders + haxe_path = self._custom_settings.get("haxePath") + if haxe_path: + init_options["haxePath"] = haxe_path + initialize_params = { "locale": "en", "capabilities": { @@ -357,7 +363,11 @@ class HaxeLanguageServer(SolidLanguageServer): self.server.notify.initialized({}) # LS doesn't properly initialise without a workspace_did_change_configuration notification here. - self.server.notify.workspace_did_change_configuration({"settings": {}}) + config_settings: dict = {} + haxe_path = self._custom_settings.get("haxePath") + if haxe_path: + config_settings["haxePath"] = haxe_path + self.server.notify.workspace_did_change_configuration({"settings": config_settings}) log.info("Waiting for Haxe LSP compilation to complete...") if self._server_ready.wait(timeout=self._COMPILATION_TIMEOUT): diff --git a/test/solidlsp/haxe/test_haxe_basic.py b/test/solidlsp/haxe/test_haxe_basic.py index 342a04bd..1662090e 100644 --- a/test/solidlsp/haxe/test_haxe_basic.py +++ b/test/solidlsp/haxe/test_haxe_basic.py @@ -247,3 +247,31 @@ class TestHaxeLanguageServer: for s in overview: assert s.get("name"), "Symbol missing 'name' field" assert s.get("kind"), "Symbol missing 'kind' field" + + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_rapid_successive_requests(self, language_server: SolidLanguageServer) -> None: + """Verify that rapid successive requests don't return empty results. + + The Haxe LS triggers a recompilation on didOpen. Without suppression, + subsequent requests during recompilation may return empty results. + """ + main_path = os.path.join("src", "Main.hx") + helper_path = os.path.join("src", "utils", "Helper.hx") + + # First request: hover on greet in Main.hx (line 22 is the greet method definition) + hover1 = language_server.request_hover(main_path, 22, 20) + + # Second request: references in a different file (triggers another didOpen) + all_symbols, _ = language_server.request_document_symbols(helper_path).get_all_symbols_and_roots() + add_numbers = next((s for s in all_symbols if s.get("name") == "addNumbers"), None) + assert add_numbers is not None + sel_start = add_numbers["selectionRange"]["start"] + refs = language_server.request_references(helper_path, sel_start["line"], sel_start["character"]) + + # Third request: back to Main.hx hover (would be affected by recompilation) + hover2 = language_server.request_hover(main_path, 22, 20) + + # All three should return non-empty results + assert hover1 is not None, "First hover returned None" + assert refs, "References returned empty after switching files" + assert hover2 is not None, "Second hover returned None after file switching" From 8623907d9de664c96269aec4698d122a4cf4efff Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 18:46:24 +0200 Subject: [PATCH 101/190] Removed news.json --- news/news.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 news/news.json diff --git a/news/news.json b/news/news.json deleted file mode 100644 index 11eb1d97..00000000 --- a/news/news.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "20260111": "
\n

Extended Symbol Information, Type Hierarchy and Compact Overviews

\n

January 11, 2026

\n

\n Recent commits and a new plugin release provide major new features!\n

\n
    \n
  • Extended symbol information: The `find_symbol` and `find_referencing_symbols` tools (and their JetBrains variants)\n now can return more information about the symbol (specifically, docstrings and signatures).\n
  • \n
  • Type hierarchy tool: A new tool – exclusive to JetBrains mode –\n that can fetch a symbol's hierarchy, i.e. superclasses and subclasses, which is very useful in many situations.\n
  • \n
  • Compact overviews: The `get_symbols_overview` tools now return a much more compact\n representation, saving many tokens. The JetBrains variant of the tool can now return a file's docstring (LSP\n varian't can't do that yet).\n
  • \n
\n

\n For more detailed information, see our changelog.\n

\n
", - "20260303": "
\n

Nested and Global Memories

\n

March 03, 2026

\n

\n Serena's memory system has been significantly extended in functionality.\n It keeps the simplicity that made it popular but now supports structuring memories\n into topics and sharing them across projects.\n

\n

\n For more detailed information, see the documentation.\n

\n
", - "20260321": "
\n

Progressive Shortening and General Improvements in Tool Responses

\n

March 21, 2026

\n

\n Tools that may deliver too long results now output informative shorter\n messages instead of just stating \"result is too long\". The mechanism uses\n progressive shortening so that the contained information is optimized\n for the length limit.\n

\n

\n In addition, several tool responses have been optimized without losing\n content, making Serena even more token efficient in general.\n

\n
", - "20260330": "
\n

Querying of External Projects

\n

March 30, 2026

\n

\n We added a new tool for querying external projects that are\n known to Serena: From the current project, use Serena's tools on another project to retrieve information.\n This is useful in situations where the current project builds on or is otherwise related\n to other projects.\n

\n

\n Find out more about this feature in our documentation.\n

\n
", - "20260404": "
\n

Serena v1.0 Is Here!

\n

April 3, 2026

\n

\n After a full year of work since the first version of Serena, we are excited to announce the release of Serena v1.0!\n

\n

\n Since our last update, we have now added support for exciting new retrieval and refactoring tools, especially for users of the JetBrains backend.\n

\n
    \n
  • safe_delete, jet_brains_safe_delete (beta):
    Safely deletes a symbol, checking for remaining usages first
  • \n
  • jet_brains_move (beta):
    Moves a symbol, file or directory to a new location, updating all references
  • \n
  • jet_brains_find_declaration:
    Finds the declaration of a symbol using the JetBrains backend
  • \n
  • jet_brains_find_implementations:
    Finds the implementations of a symbol (e.g. abstract method)
  • \n
  • jet_brains_inline_symbol (beta):
    Inlines a symbol, replacing all call sites with the symbol’s body
  • \n
\n

\n We are far from done. Many new features will be coming your way – for both the LSP and JetBrains backends.\n

\n

\n Thank you to our contributors, users and customers!\n

\n
" -} \ No newline at end of file From 23ebce05633a15478c8e773f8389f688d15c3fff Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 10 Apr 2026 19:07:19 +0200 Subject: [PATCH 102/190] Deploy news to and load news from Oraios server instead of GitHub GitHub server (raw.githubuserconten.com) is frequently blacklisted --- scripts/build_news_json.py | 11 +++++++++++ src/serena/dashboard.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/build_news_json.py b/scripts/build_news_json.py index 10a86b67..8359604a 100644 --- a/scripts/build_news_json.py +++ b/scripts/build_news_json.py @@ -9,6 +9,7 @@ This reads all .html files from the `news/` directory and creates """ import json +import os import sys from pathlib import Path @@ -41,4 +42,14 @@ def build_news_json() -> None: if __name__ == "__main__": + deploy = False + for arg in sys.argv[1:]: + if arg == "--deploy": + deploy = True + build_news_json() + + if deploy: + user = os.getenv("HADES_USER") + assert user, "HADES_USER environment variable must be set to deploy news.json to Hades" + os.system(f"scp news/news.json {user}@hades:/var/www/html/oraios-software/serena_news.json") diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 528af993..3c33f1a2 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -630,7 +630,7 @@ class SerenaDashboardAPI: # ===== Remote News Methods ===== # The branch from which news are fetched. Change to a feature branch for testing. - _NEWS_JSON_URL = "https://raw.githubusercontent.com/oraios/serena/main/news/news.json" + _NEWS_JSON_URL = "https://oraios-software.de/serena_news.json" def _fetch_news(self) -> None: """Fetch news.json from GitHub using ETag-based caching and store in memory. Silently ignores network errors.""" From cd886b8934d5c62c660518a8afa2590697b4d2ce Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 19:32:34 +0200 Subject: [PATCH 103/190] New news entry --- news/20260409.html | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 news/20260409.html diff --git a/news/20260409.html b/news/20260409.html new file mode 100644 index 00000000..ed1241d4 --- /dev/null +++ b/news/20260409.html @@ -0,0 +1,22 @@ +
+

Serena v1.1: Hooks and New Release Mechanism

+

April 9, 2026

+

+ Installation. Serena has matured and we are switching to a PyPI-based release and installation mechanism. + The recommended uv-tool-based installation decreases startup times and facilitates CLI usage. + See updated installation instructions + for more details. +

+

+ Improving Tool Usage via Hooks. To address issues with agents not using Serena's tools (agent drift), we now provide the `serena-hooks` commands and a recommended + setup for Claude Code and VSCode. + Please read the updated documentation for more details. + We strongly recommend using the hooks, since recent updates in Claude Code and VSCode resulted in + reduced performance, which our hook commands compensate for. +

+ +

+ Community. We opened a Discord server for our community. +

+ +
\ No newline at end of file From ef8f0ac4049553beda419cfba75074eb63a75bb1 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 19:36:14 +0200 Subject: [PATCH 104/190] Docs: minor changes --- docs/02-usage/020_running.md | 8 ++++++-- docs/02-usage/030_clients.md | 27 +++++++++++++-------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/docs/02-usage/020_running.md b/docs/02-usage/020_running.md index d6b0b81c..468b1d32 100644 --- a/docs/02-usage/020_running.md +++ b/docs/02-usage/020_running.md @@ -180,14 +180,18 @@ Explore the full set of commands and options using the CLI itself! Depending on your requirements, you may want to run Serena in different ways. When applying one of these approaches, replace `serena` in commands mentioned throughout the documentation -with the respective command and options. +with the respective command and options. The same applies to `serena-hooks` commands. ### Using uvx to Run the Latest Source Version - + `uvx` is part of `uv`. It can be used to run the latest version of Serena directly from the repository, without an explicit local installation. uvx -p 3.13 --from git+https://github.com/oraios/serena serena +This was previously the main way of running Serena. +Since this has the downside that every new commit in the repository will trigger a (potentially slow) re-synchronization, an [installation](010_installation) of Serena should usually be preferred. +If you should experience timeouts when connecting the MCP server, consider switching. +If, however, the synchronisation is fast enough for you, this is still a good option. ### Running from Cloned Source diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index be2a8021..1d6cea12 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -134,14 +134,13 @@ set ENABLE_TOOL_SEARCH=true && claude in Windows CMD to launch Claude Code. **Hooks.** -Claude Code's agent can sometimes drift away from using MCP tools during long sessions, falling back -to built-in tools or forgetting to load Serena's instructions. Serena provides hook commands that -automatically remind the agent to use Serena's tools when appropriate. +Due to recent changes (especially dynamic tool loading) in Claude Code, the agent will often fail to make proper use +of Serena's tools, either by failing to load them in the beginning or by forgetting the instructions in a long session +(a behavior known as agent drift). To counteract this, we provide reminder hooks. We **strongly recommend** setting +up the hooks as below (or a variation thereof) for optimal performance of Serena in Claude Code. :::{note} -Hooks are an **alpha feature** that we provide to overcome context drift in several agent harnesses -(where agents, especially those powered by Anthropic models, may forget to load and use MCP tools or -fail to read MCP instructions). +While recommended, hooks are an **alpha feature**. Provide feedback via the [GitHub issue tracker](https://github.com/serenadeai/serenade/issues) if you encounter any issues. ::: To set up hooks, add the following to your Claude Code settings file @@ -235,15 +234,11 @@ In both cases, proceed to enter Serena as the name, then select either global or You should be able to see Serena in the tools overview in the AI Chat window. **Hooks.** -VSCode's Copilot agent can sometimes over-rely on basic tools like `read_file` and `grep_search` instead of -using Serena's more efficient symbolic tools. You can use Serena's hook commands to automatically correct -for this — the hooks will simply remind the agent to use Serena's tools when appropriate. +Due to recent changes (especially dynamic tool loading) in VSCode, the agent will often fail to make proper use +of Serena's tools, either by failing to load them in the beginning or by forgetting the instructions in a long session +(a behaviour known as agent drift). To counteract this, we provide reminder hooks. We **strongly recommend** setting +up the hooks as below (or a variation thereof) for optimal performance of Serena in VSCode. -:::{note} -Hooks are an **alpha feature** that we provide to overcome context drift in several agent harnesses -(where agents, especially those powered by Anthropic models, may forget to load and use MCP tools or -fail to read MCP instructions). -::: The hooks will: @@ -301,6 +296,10 @@ The Codex app does not start a session in the project's directory, so when using asking Codex to "Activate the current dir as project using serena" at the start of each session (though Codex might do this automatically). +**Hooks.** The Codex hook system is less refined than of VSCode or Claude Code, but Codex also displays less drift +and is better in using Serena's tools out of the box. If you use the Codex app, you can consider setting up the +SessionStart hook (see above) to remind the agent to activate the project directory. + ## Claude Desktop On Windows and macOS, there are official [Claude Desktop applications by Anthropic](https://claude.ai/download); for Linux, there is an [open-source From a96bce57efe49c1ecda5c155c37d5651d997447e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 21:13:46 +0200 Subject: [PATCH 105/190] Gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 30dd3331..73c25571 100644 --- a/.gitignore +++ b/.gitignore @@ -270,3 +270,5 @@ cabal.project.local* zz-misc/ vue-implementation/ + +news/news.json \ No newline at end of file From 468110a9e021ab2d428b16f37f7f220af44d1801 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 10 Apr 2026 21:13:59 +0200 Subject: [PATCH 106/190] WIP --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 033eaac8..1a2738e3 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,13 @@ Serena is the IDE for your coding agent. +Gitignore
+[![][license-shield]][license-link] +[![][discord-shield]][discord-link] +
+
+ + * Serena provides essential **semantic code retrieval, editing and refactoring tools** that are akin to an IDE's capabilities, operating at the symbol level and exploiting relational structure. * It integrates with any client/LLM via the model context protocol (**MCP**). @@ -20,6 +27,25 @@ more complex codebases. > [!IMPORTANT] > Don't install Serena via an MCP or plugin marketplace! They contain outdated and suboptimal installation commands. Instead, follow our [Quick Start](#quick-start) instructions. +
+
+[How Serena Works](#how-serena-works) ¡ [Programming Language Support](#programming-language-support--semantic-analysis-capabilities) ¡ [Features](#features) ¡ [Serena in Action](#serena-in-action) ¡ [Quick Start](#quick-start) ¡ [User Guide](#user-guide) ¡ [Acknowledgements](#acknowledgements) +
+ +## What Our "End Users" Say + +Our end users are essentially AI agents, so we asked each of them to compare Serena's tools against their own inbuilt capabilities. Here is what they had to say: + +
+
+ +[Claude Code](#quick-start) ¡ [GitHub Copilot](#the-palace) ¡ [Codex](#aaak-dialect-experimental) + +
+
+ +Your agent deserves the best coding tools, give them Serena! + ## How Serena Works Serena provides the necessary [tools](https://oraios.github.io/serena/01-about/035_tools.html) for coding workflows, @@ -224,3 +250,9 @@ A significant part of Serena, especially support for various languages, was cont We are very grateful for the many contributors who made this possible and who played an important role in making Serena what it is today. + + +[license-shield]: https://img.shields.io/badge/license-MIT-b0e8ff?style=flat-square&labelColor=0a0e14 +[license-link]: https://github.com/oraios/serena/main/LICENSE +[discord-shield]: https://img.shields.io/badge/discord-join-5865F2?style=flat-square&labelColor=0a0e14&logo=discord&logoColor=5865F2 +[discord-link]: https://discord.com/invite/cVUNQmnV4r From 6844eec0f55b0c35bc9d4b5f8777ab3f3b2c15c2 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 10 Apr 2026 17:08:11 +0200 Subject: [PATCH 107/190] Collect basic usage data --- CHANGELOG.md | 3 ++- src/serena/agent.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a672fcde..b1a87a1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. * General: - - Added `serena init` command + - Add `serena init` command + - Add minimal usage data collection on startup (only Serena version, language backend, OS, dashboard enabled status; no personally identifiable information) * Language Servers: - Added Crystal language support (uses [Crystalline](https://github.com/elbywan/crystalline) language server) diff --git a/src/serena/agent.py b/src/serena/agent.py index f3c4d6ab..57ed1926 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -14,6 +14,7 @@ from contextlib import contextmanager from logging import Logger from typing import TYPE_CHECKING, Optional, TypeVar +import requests import webview from sensai.util import logging from sensai.util.logging import LogTime @@ -406,6 +407,23 @@ class SerenaAgent: if self._gui_log_viewer is not None: self._gui_log_viewer.set_dashboard_url(dashboard_url) + self._send_usage_info() + + def _send_usage_info(self): + is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true" + if is_ci: + return + params = { + "os": platform.system(), + "dashboard": int(self.serena_config.web_dashboard), + "version": self.version, + "backend": self._language_backend.value, + } + try: + requests.get("https://www.oraios-software.de/serena_usage.php", params=params, timeout=1, verify=False) + except Exception as e: + log.debug(f"Failed to send usage info: {e}") + @classmethod def _create_base_toolset( cls, From 7588c4343cec8dae3f588be255853209aa9cdc13 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 10 Apr 2026 22:35:48 +0200 Subject: [PATCH 108/190] Changelog: Move fixes into respective sections --- CHANGELOG.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29de4dfa..930f7dc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ Status of the `main` branch. Changes prior to the next official version change w - **Major**: added commands for hooks and documentation on recommended setup. Consider setting up the [recommended hooks](https://oraios.github.io/serena/02-usage/030_clients.html) ! - Reworked installation instructions, switching to releases on pypi for distribution. Please update your mcp startup commands! - Add minimal usage data collection on startup (only Serena version, language backend, OS, dashboard enabled status; no personally identifiable information) + - Fix: git commit id in Serena version strings was incorrect * Language Servers: - Added Crystal language support (uses [Crystalline](https://github.com/elbywan/crystalline) language server) + - Fix: Reactivation of the same project restarted language servers #1280 * JetBrains: - `JetBrainsFindReferencingSymbolTool`: Include context lines (when using plugin version 2023.2.15+) @@ -18,11 +20,7 @@ Status of the `main` branch. Changes prior to the next official version change w - Add version display - Dashboard viewer (Windows): Add a parent monitoring thread to ensure termination. Some clients would terminate the MCP server in a way that did not ensure proper termination. - -* Fixes: - - Fix reactivation of the same project restarting language servers #1280 - - Fix git commit id in version string - - Fix manual server shutdown triggered by GUI tool/dashboard not cleaning everything up. + - Fix: Manual server shutdown triggered by GUI tool/dashboard not cleaning everything up. # 1.0.0 From 233fe19d01c0c6ff98d72093c0cba11f2003eb93 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 10 Apr 2026 22:38:04 +0200 Subject: [PATCH 109/190] Changelog: More consistent wording (imperative, use of caps) --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 930f7dc1..b27daa90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,14 +3,14 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. * General: - - Added `serena init` and `serena setup` commands - - **Major**: added commands for hooks and documentation on recommended setup. Consider setting up the [recommended hooks](https://oraios.github.io/serena/02-usage/030_clients.html) ! - - Reworked installation instructions, switching to releases on pypi for distribution. Please update your mcp startup commands! + - Add `serena init` and `serena setup` commands + - **Major**: Add commands for hooks and documentation of recommended setup. Consider setting up the [recommended hooks](https://oraios.github.io/serena/02-usage/030_clients.html) ! + - Rework installation instructions, switching to releases on pypi for distribution. Please update your mcp startup commands! - Add minimal usage data collection on startup (only Serena version, language backend, OS, dashboard enabled status; no personally identifiable information) - Fix: git commit id in Serena version strings was incorrect * Language Servers: - - Added Crystal language support (uses [Crystalline](https://github.com/elbywan/crystalline) language server) + - Add Crystal language support (uses [Crystalline](https://github.com/elbywan/crystalline) language server) - Fix: Reactivation of the same project restarted language servers #1280 * JetBrains: @@ -18,7 +18,7 @@ Status of the `main` branch. Changes prior to the next official version change w * Dashboard: - Add version display - - Dashboard viewer (Windows): Add a parent monitoring thread to ensure termination. + - Fix: Dashboard viewer (Windows): Add a parent monitoring thread to ensure termination. Some clients would terminate the MCP server in a way that did not ensure proper termination. - Fix: Manual server shutdown triggered by GUI tool/dashboard not cleaning everything up. From ac215817f35b9581de429955209f2ef1b0c53f98 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 11 Apr 2026 00:14:00 +0200 Subject: [PATCH 110/190] Fix untyped def --- src/serena/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index b3feec7d..ab14239f 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -409,7 +409,7 @@ class SerenaAgent: self._send_usage_info() - def _send_usage_info(self): + def _send_usage_info(self) -> None: is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true" if is_ci: return From fe75484c4ac165c5346b4d4af7ba51e05e32969e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 11 Apr 2026 15:26:26 +0200 Subject: [PATCH 111/190] Changelog, moved haxe entry to the right place [ci skip] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8532d97c..5249aae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Status of the `main` branch. Changes prior to the next official version change w * Language Servers: - Added Crystal language support (uses [Crystalline](https://github.com/elbywan/crystalline) language server) + - Added support for Haxe** via vshaxe/haxe-language-server. Requires Haxe compiler 3.4.0+ and Node.js. Auto-discovered from the vshaxe VSCode extension or configurable via `ls_path` in `ls_specific_settings`. * Dashboard: - Add version display @@ -73,7 +74,6 @@ Status of the `main` branch. Changes prior to the next official version change w * **Add support for Lean 4** via built-in `lean --server` with cross-file reference support (requires `lean` and `lake` via [elan](https://github.com/leanprover/elan)) * **Add support for OCaml** via ocaml-lsp-server with cross-file reference support on OCaml 5.2+ (requires opam; see [setup guide](docs/03-special-guides/ocaml_setup_guide_for_serena.md)) * **Add Phpactor as alternative PHP language server** (specify `php_phpactor` as language; requires PHP 8.1+) - * **Add support for Haxe** via vshaxe/haxe-language-server. Requires Haxe compiler 3.4.0+ and Node.js. Auto-discovered from the vshaxe VSCode extension or configurable via `ls_path` in `ls_specific_settings`. * **Add support for Fortran** via fortls language server (requires `pip install fortls`) * **Add partial support for Groovy** requires user-provided Groovy language server JAR (see [setup guide](docs/03-special-guides/groovy_setup_guide_for_serena.md)) * **Add support for Julia** via LanguageServer.jl From f974abc9ef7bf3cf0076136d8bacb7f6cc0120e3 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 11 Apr 2026 15:29:20 +0200 Subject: [PATCH 112/190] Improve release process description --- README-dev.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README-dev.md b/README-dev.md index 958e4893..cd5fce8f 100644 --- a/README-dev.md +++ b/README-dev.md @@ -16,4 +16,5 @@ and tools for formatting and type checking. This also creates the git tag. 3. Push to GitHub, triggering the release process. - git push --tags \ No newline at end of file + git push + git push --tags From 870e6184a45a251a3b2ef78b6414c10ac2fe403d Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 11 Apr 2026 15:30:58 +0200 Subject: [PATCH 113/190] Release v1.1.0 --- CHANGELOG.md | 2 ++ pyproject.toml | 2 +- src/serena/__init__.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83ff8b66..b8471139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +# 1.1.0 + * General: - Add `serena init` and `serena setup` commands - **Major**: Add commands for hooks and documentation of recommended setup. Consider setting up the [recommended hooks](https://oraios.github.io/serena/02-usage/030_clients.html) ! diff --git a/pyproject.toml b/pyproject.toml index fc7f1355..e1638adb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["hatchling"] [project] name = "serena-agent" -version = "1.0.0" +version = "1.1.0" description = "" authors = [{ name = "Oraios AI", email = "info@oraios-ai.de" }] readme = "README.md" diff --git a/src/serena/__init__.py b/src/serena/__init__.py index 3ab32039..00137aad 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.0.0" +__version__ = "1.1.0" import logging From 9340abe89e059747a9d337ba6b882d0d35a687ab Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 11 Apr 2026 15:35:52 +0200 Subject: [PATCH 114/190] Fix news date [skip ci] --- news/{20260409.html => 20260411.html} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename news/{20260409.html => 20260411.html} (96%) diff --git a/news/20260409.html b/news/20260411.html similarity index 96% rename from news/20260409.html rename to news/20260411.html index ed1241d4..f337c07f 100644 --- a/news/20260409.html +++ b/news/20260411.html @@ -1,6 +1,6 @@

Serena v1.1: Hooks and New Release Mechanism

-

April 9, 2026

+

April 11, 2026

Installation. Serena has matured and we are switching to a PyPI-based release and installation mechanism. The recommended uv-tool-based installation decreases startup times and facilitates CLI usage. From bfd63a73fb41f2fe889d91de226a9fbe8732d074 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 11 Apr 2026 16:27:18 +0200 Subject: [PATCH 115/190] Revert "WIP" This reverts commit 468110a9e021ab2d428b16f37f7f220af44d1801. --- README.md | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/README.md b/README.md index 83c6bad2..e15d524d 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,6 @@ Serena is the IDE for your coding agent. -Gitignore

-[![][license-shield]][license-link] -[![][discord-shield]][discord-link] -
-
- - * Serena provides essential **semantic code retrieval, editing and refactoring tools** that are akin to an IDE's capabilities, operating at the symbol level and exploiting relational structure. * It integrates with any client/LLM via the model context protocol (**MCP**). @@ -27,25 +20,6 @@ more complex codebases. > [!IMPORTANT] > Don't install Serena via an MCP or plugin marketplace! They contain outdated and suboptimal installation commands. Instead, follow our [Quick Start](#quick-start) instructions. -
-
-[How Serena Works](#how-serena-works) ¡ [Programming Language Support](#programming-language-support--semantic-analysis-capabilities) ¡ [Features](#features) ¡ [Serena in Action](#serena-in-action) ¡ [Quick Start](#quick-start) ¡ [User Guide](#user-guide) ¡ [Acknowledgements](#acknowledgements) -
- -## What Our "End Users" Say - -Our end users are essentially AI agents, so we asked each of them to compare Serena's tools against their own inbuilt capabilities. Here is what they had to say: - -
-
- -[Claude Code](#quick-start) ¡ [GitHub Copilot](#the-palace) ¡ [Codex](#aaak-dialect-experimental) - -
-
- -Your agent deserves the best coding tools, give them Serena! - ## How Serena Works Serena provides the necessary [tools](https://oraios.github.io/serena/01-about/035_tools.html) for coding workflows, @@ -250,9 +224,3 @@ A significant part of Serena, especially support for various languages, was cont We are very grateful for the many contributors who made this possible and who played an important role in making Serena what it is today. - - -[license-shield]: https://img.shields.io/badge/license-MIT-b0e8ff?style=flat-square&labelColor=0a0e14 -[license-link]: https://github.com/oraios/serena/main/LICENSE -[discord-shield]: https://img.shields.io/badge/discord-join-5865F2?style=flat-square&labelColor=0a0e14&logo=discord&logoColor=5865F2 -[discord-link]: https://discord.com/invite/cVUNQmnV4r From fef4165702cc4e85d1690327382f79ae312f89e0 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 11 Apr 2026 16:33:11 +0200 Subject: [PATCH 116/190] Fix docs build --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e15d524d..d1b3b04b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Practically, this means that your agent operates **faster, more efficiently and more complex codebases. > [!IMPORTANT] -> Don't install Serena via an MCP or plugin marketplace! They contain outdated and suboptimal installation commands. Instead, follow our [Quick Start](#quick-start) instructions. +> Don't install Serena via an MCP or plugin marketplace! They contain outdated and suboptimal installation commands. Instead, follow our Quick Start instructions. ## How Serena Works @@ -37,7 +37,7 @@ To connect the Serena MCP server to your client, you either * provide the client with a launch command that allows it to start the MCP server, or * start the Serena MCP server yourself in HTTP mode and provide the client with the URL. -See the [Quick Start](#quick-start) section below for information on how to get started. +See the Quick Start section below for information on how to get started. ## Programming Language Support & Semantic Analysis Capabilities From 38699a84708e16f20e12b61beb1a0b550f6f8ff2 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 11 Apr 2026 16:52:22 +0200 Subject: [PATCH 117/190] Update lock --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 5f6e7011..b0f37c6c 100644 --- a/uv.lock +++ b/uv.lock @@ -2911,7 +2911,7 @@ wheels = [ [[package]] name = "serena-agent" -version = "1.0.0" +version = "1.1.0" source = { editable = "." } dependencies = [ { name = "anthropic" }, From 34a703ad503ec338b3381de0a50fa946eee37162 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 11 Apr 2026 16:54:45 +0200 Subject: [PATCH 118/190] Restore links to quick start guide, updating the docs autogen build script --- README.md | 5 +++-- docs/autogen_docs.py | 8 +++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d1b3b04b..6497f03c 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ Practically, this means that your agent operates **faster, more efficiently and more complex codebases. > [!IMPORTANT] -> Don't install Serena via an MCP or plugin marketplace! They contain outdated and suboptimal installation commands. Instead, follow our Quick Start instructions. +> Do not install Serena via an MCP or plugin marketplace! They contain outdated and suboptimal installation commands. +> Instead, follow our [Quick Start](quick-start) instructions. ## How Serena Works @@ -37,7 +38,7 @@ To connect the Serena MCP server to your client, you either * provide the client with a launch command that allows it to start the MCP server, or * start the Serena MCP server yourself in HTTP mode and provide the client with the URL. -See the Quick Start section below for information on how to get started. +See the [Quick Start](quick-start) section below for information on how to get started. ## Programming Language Support & Semantic Analysis Capabilities diff --git a/docs/autogen_docs.py b/docs/autogen_docs.py index 5372dd09..d20eaa92 100644 --- a/docs/autogen_docs.py +++ b/docs/autogen_docs.py @@ -194,8 +194,14 @@ def autogen_about_intro_features(): f.write(autogen_info) f.write("# About Serena\n\n") f.write(f"**{tagline}**\n\n") + + # adjust link about_text = about_text.replace("resources/serena-block-diagram.svg", "https://raw.githubusercontent.com/oraios/serena/main/resources/serena-block-diagram.svg") - about_text = re.subn(r"^See the \[Quick Start.*?$", r"", about_text, flags=re.MULTILINE)[0] + + # remove statements with links that are invalid and the callout + about_text = re.subn(r"^.*\[Quick Start.*?$", r"", about_text, flags=re.MULTILINE)[0] + about_text = re.subn(r"^>.*?$", "", about_text, flags=re.MULTILINE)[0] + f.write(f"{about_text}\n\n") jetbrains_marketplace_link = ('```{raw} html\n' From cab36b4220a365936ff82ba91803df3ca0261185 Mon Sep 17 00:00:00 2001 From: Arkh74278 <136110739+Arkh74278@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:25:33 +0300 Subject: [PATCH 119/190] Merge pull request #1312 from Arkh74278/fix/dart-hierarchical-document-symbol-support fix: add hierarchicalDocumentSymbolSupport capability for Dart LSP --- CHANGELOG.md | 5 +- .../language_servers/dart_language_server.py | 8 ++- test/solidlsp/dart/test_dart_basic.py | 49 +++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8471139..ceda84be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* Language Servers: + - Fix Dart LSP returning only symbol name as body instead of full method body + # 1.1.0 * General: @@ -284,4 +287,4 @@ Fixes: # 2025-04-01 -Initial public version +Initial public version \ No newline at end of file diff --git a/src/solidlsp/language_servers/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server.py index 11f78106..f82627c6 100644 --- a/src/solidlsp/language_servers/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server.py @@ -123,7 +123,13 @@ class DartLanguageServer(SolidLanguageServer): """ root_uri = pathlib.Path(repository_absolute_path).as_uri() initialize_params = { - "capabilities": {}, + "capabilities": { + "textDocument": { + "documentSymbol": { + "hierarchicalDocumentSymbolSupport": True, + } + } + }, "initializationOptions": { "onlyAnalyzeProjectsWithOpenFiles": False, "closingLabels": False, diff --git a/test/solidlsp/dart/test_dart_basic.py b/test/solidlsp/dart/test_dart_basic.py index 0a5b69cf..7dab1fe9 100644 --- a/test/solidlsp/dart/test_dart_basic.py +++ b/test/solidlsp/dart/test_dart_basic.py @@ -375,3 +375,52 @@ class TestDartLanguageServer: f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", pytrace=False, ) + + @pytest.mark.parametrize("language_server", [Language.DART], indirect=True) + def test_symbol_body_contains_full_method(self, language_server: SolidLanguageServer) -> None: + """Test that document symbols return the full method body range, not just the identifier. + + Regression test: when hierarchicalDocumentSymbolSupport was not declared in client + capabilities, the Dart LSP returned SymbolInformation[] (flat format) where + location.range only covered the identifier name (single line), causing find_symbol + with include_body=True to return just the symbol name instead of its implementation. + """ + file_path = os.path.join("lib", "main.dart") + symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots() + symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols + + # Find the 'add' method — defined across multiple lines in main.dart: + # int add(int a, int b) { + # final result = a + b; + # _history.add('$a + $b = $result'); + # return result; + # } + add_symbol = None + for sym in symbol_list: + if sym.get("name") == "add": + add_symbol = sym + break + if sym.get("name") == "Calculator" and "children" in sym: + for child in sym["children"]: + if child.get("name") == "add": + add_symbol = child + break + if add_symbol: + break + + assert add_symbol is not None, "Could not find 'add' method symbol in main.dart" + + # The body range must span multiple lines (not just the identifier line). + # With hierarchicalDocumentSymbolSupport declared, the Dart LSP returns + # DocumentSymbol[] where range covers the full method body. + body_start = add_symbol["location"]["range"]["start"]["line"] + body_end = add_symbol["location"]["range"]["end"]["line"] + assert body_end > body_start, ( + f"Expected multi-line body range for 'add' method, got start={body_start}, end={body_end}. " + f"This likely means hierarchicalDocumentSymbolSupport is not declared in client capabilities." + ) + + # The body text must contain the method implementation, not just the name. + if add_symbol.get("body"): + body_text = add_symbol["body"].get_text() + assert "return result" in body_text, f"Expected method body to contain implementation, got: {body_text!r}" From 9628fdf1b48cb843be2ef1a10184c01d90d369a9 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 11:30:10 +0200 Subject: [PATCH 120/190] Minor typing fix --- src/serena/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index ab14239f..c479c80d 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -413,7 +413,7 @@ class SerenaAgent: is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true" if is_ci: return - params = { + params: dict[str, str | int] = { "os": platform.system(), "dashboard": int(self.serena_config.web_dashboard), "version": self.version, From ca387bdc768f14459559102d1e83eaa7633eea71 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 11:35:56 +0200 Subject: [PATCH 121/190] Docs: added brief explainer on hooks in Common Pitfalls, removed section on tool-loading in Claude Code --- docs/02-usage/030_clients.md | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index 1d6cea12..8280d604 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -40,10 +40,15 @@ A key mechanism for this is to use the appropriate [context](#contexts) when sta (clients-common-pitfalls)= ### Common Pitfalls -**Discoverability of the `serena`**. +**Discoverability of the `serena` command**. Your client may not find the `serena` CLI command, even if it is on your system PATH. In this case, a workaround is to provide the full path to the `serena` executable. +**Serena's tools not being used**. +With some clients, you may experience that Serena's tools are not being used. +This is mainly due to problems in the client itself (like a poorly implemented tool discovery). To counteract this, +Serena comes with a set of commands that can be used in _hooks_. See the sections on hooks for Claude Code and VSCode below. + **Environment Variables**. Some language servers may require additional environment variables to be set (e.g. F# on macOS with Homebrew), which you may need to explicitly add to the MCP server configuration. @@ -117,22 +122,6 @@ Confirm that Claude Code is connected to Serena by running the `/mcp` command an If Serena fails to start fast enough, you should set `MCP_TIMEOUT` to a sufficiently high value (e.g. by adding `export MCP_TIMEOUT=60000` to your shell profile) -**Maximum Token Efficiency.** To maximize token efficiency, you may want to use Claude Code's -*on-demand tool loading* feature, which is supported since at least v2.0.74 of Claude Code. -This feature avoids sending all tool descriptions to Claude upon startup, thus saving tokens. -Instead, Claude will search for tools as needed (but there are no guarantees that it will -search optimally, of course). -To enable this feature, set the environment variable `ENABLE_TOOL_SEARCH=true`. -Depending on your shell, you can also set this on a per-session basis, e.g. using -```shell -ENABLE_TOOL_SEARCH=true claude -``` -in bash/zsh, or using -```shell -set ENABLE_TOOL_SEARCH=true && claude -``` -in Windows CMD to launch Claude Code. - **Hooks.** Due to recent changes (especially dynamic tool loading) in Claude Code, the agent will often fail to make proper use of Serena's tools, either by failing to load them in the beginning or by forgetting the instructions in a long session From abfde9507bff691fbc86f83f5889d3e05dedc0a9 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 11:47:52 +0200 Subject: [PATCH 122/190] Removed obsolete and outdated lessons learned and llm-install.md files. --- lessons_learned.md | 90 ---------------------------------------------- llms-install.md | 29 --------------- 2 files changed, 119 deletions(-) delete mode 100644 lessons_learned.md delete mode 100644 llms-install.md diff --git a/lessons_learned.md b/lessons_learned.md deleted file mode 100644 index b6d15a70..00000000 --- a/lessons_learned.md +++ /dev/null @@ -1,90 +0,0 @@ -# Lessons Learned - -In this document we briefly collect what we have learned while developing and using Serena, -what works well and what doesn't. - -## What Worked - -### Separate Tool Logic From MCP Implementation - -MCP is just another protocol, one should let the details of it creep into the application logic. -The official docs suggest using function annotations to define tools and prompts. While that may be -useful for small projects to get going fast, it is not wise for more serious projects. In Serena, -all tools are defined independently and then converted to instances of `MCPTool` using our `make_tool` -function. - -### Autogenerated PromptFactory - -Prompt templates are central for most LLM applications, so one needs good representations of them in the code, -while at the same time they often need to be customizable and exposed to users. In Serena we address these conflicting -needs by defining prompt templates (in jinja format) in separate yamls that users can easily modify and by autogenerated -a `PromptFactory` class with meaningful method and parameter names from these yamls. The latter is committed to our code. -We separated out the generation logic into the [interprompt](/src/interprompt/README.md) subpackage that can be used as a library. - -### Tempfiles and Snapshots for Testing of Editing Tools - -We test most aspects of Serena by having a small "project" for each supported language in `tests/resources`. -For the editing tools, which would change the code in these projects, we use tempfiles to copy over the code. -The pretty awesome [syrupy](https://github.com/syrupy-project/syrupy) pytest plugin helped in developing -snapshot tests. - -### Dashboard and GUI for Logging - -It is very useful to know what the MCP Server is doing. We collect and display logs in a GUI or a web dashboard, -which helps a lot in seeing what's going on and in identifying any issues. - -### Unrestricted Bash Tool - -We know it's not particularly safe to permit unlimited shell commands outside a sandbox, but we did quite some -evaluations and so far... nothing bad has happened. Seems like the current versions of the AI overlords rarely want to execute `sudo rm - rf /`. -Still, we are working on a safer approach as well as better integration with sandboxing. - -### Multilspy - -The [multilspy](https://github.com/microsoft/multilspy/) project helped us a lot in getting started and stands at the core of Serena. -Many more well known python implementations of language servers were subpar in code quality and design (for example, missing types). - -### Developing Serena with Serena - -We clearly notice that the better the tool gets, the easier it is to make it even better - -## Prompting - -### Shouting and Emotive Language May Be Needed - -When developing the `ReplaceRegexTool` we were initially not able to make Claude 4 (in Claude Desktop) use wildcards to save on output tokens. Neither -examples nor explicit instructions helped. It was only after adding - -``` -IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! -``` - -to the initial instructions and to the tool description that Claude finally started following the instructions. - -## What Didn't Work - -### Lifespan Handling by MCP Clients - -The MCP technology is clearly very green. Even though there is a lifespan context in the MCP SDK, -many clients, including Claude Desktop, fail to properly clean up, leaving zombie processes behind. -We mitigate this through the GUI window and the dashboard, so the user sees whether Serena is running -and can terminate it there. - -### Trusting Asyncio - -Running multiple asyncio apps led to non-deterministic -event loop contamination and deadlocks, which were very hard to debug -and understand. We solved this with a large hammer, by putting all asyncio apps into a separate -process. It made the code much more complex and slightly enhanced RAM requirements, but it seems -like that was the only way to reliably overcome asyncio deadlock issues. - -### Cross-OS Tkinter GUI - -Different OS have different limitations when it comes to starting a window or dealing with Tkinter -installations. This was so messy to get right that we pivoted to a web-dashboard instead - -### Editing Based on Line Numbers - -Not only are LLMs notoriously bad in counting, but also the line numbers change after edit operations, -and LLMs are also often too dumb to understand that they should update the line numbers information they had -received before. We pivoted to string-matching and symbol-name based editing. \ No newline at end of file diff --git a/llms-install.md b/llms-install.md deleted file mode 100644 index b02711c7..00000000 --- a/llms-install.md +++ /dev/null @@ -1,29 +0,0 @@ -# MCP Installation instructions - -This document is mainly used as instructions for AI-assistants like Cline and others that -try to do an automatic install based on freeform instructions. - -0. Make sure `uv` is installed. If not, install it using either `curl -LsSf https://astral.sh/uv/install.sh | sh` (macOS, Linux) or - `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"` (Windows). Find the path to the `uv` executable, - you'll need it later. -1. Clone the repo with `git clone git@github.com:oraios/serena.git` and change into its dir (e.g., `cd serena`) -2. Check if `serena_config.yml` exists. If not, create it with `cp serena_config.template.yml serena_config.yml`. Read the instructions in the config. -3. In the config, check if the path to your project was added. If not, add it to the `projects` section -4. In your project, create a `.serena` if needed and check whether `project.yml` exists there. -5. If no `project.yml` was found, create it using `cp /path/to/serena/myproject.template.yml /path/to/your/project/.serena/project.yml` -6. Read the instructions in `project.yml`. Make sure the `project.yml` has the correct project language configured. - Remove the project_root entry there. -7. Finally, add the Serena MCP server config like this: - -```json - { - "mcpServers": { - ... - "serena": { - "command": "/abs/path/to/uv", - "args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server", "/path/to/your/project/.serena/project.yml"] - } - } - } - -``` From acb3a0dc1a4b118c1d707c0cee77632f7d644f24 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 12:54:00 +0200 Subject: [PATCH 123/190] Adding discord link (badge) to readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 6497f03c..000b8c21 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,13 @@ Serena is the IDE for your coding agent. +
+ discord + license +
+
+ + * Serena provides essential **semantic code retrieval, editing and refactoring tools** that are akin to an IDE's capabilities, operating at the symbol level and exploiting relational structure. * It integrates with any client/LLM via the model context protocol (**MCP**). From 5eb7c028c21ed9a0e11c6b2f821adc59ab268458 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 12:57:20 +0200 Subject: [PATCH 124/190] Readme: fix link to quick-start section --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 000b8c21..5f07841b 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ * Serena provides essential **semantic code retrieval, editing and refactoring tools** that are akin to an IDE's capabilities, operating at the symbol level and exploiting relational structure. * It integrates with any client/LLM via the model context protocol (**MCP**). - + Serena's **agent-first tool design** involves robust high-level abstractions, distinguishing it from approaches that rely on low-level concepts like line numbers or primitive search patterns. @@ -26,7 +26,7 @@ more complex codebases. > [!IMPORTANT] > Do not install Serena via an MCP or plugin marketplace! They contain outdated and suboptimal installation commands. -> Instead, follow our [Quick Start](quick-start) instructions. +> Instead, follow our [Quick Start](#quick-start) instructions. ## How Serena Works @@ -45,7 +45,7 @@ To connect the Serena MCP server to your client, you either * provide the client with a launch command that allows it to start the MCP server, or * start the Serena MCP server yourself in HTTP mode and provide the client with the URL. -See the [Quick Start](quick-start) section below for information on how to get started. +See the [Quick Start](#quick-start) section below for information on how to get started. ## Programming Language Support & Semantic Analysis Capabilities From 8a6b45bd9f546dabae1b2c7568cffda782b22acc Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 12 Apr 2026 13:09:07 +0200 Subject: [PATCH 125/190] Make news links open in new window [skip ci] --- news/20260111.html | 4 ++-- news/20260303.html | 2 +- news/20260330.html | 2 +- news/20260411.html | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/news/20260111.html b/news/20260111.html index f8c75150..91fa5031 100644 --- a/news/20260111.html +++ b/news/20260111.html @@ -8,7 +8,7 @@
  • Extended symbol information: The `find_symbol` and `find_referencing_symbols` tools (and their JetBrains variants) now can return more information about the symbol (specifically, docstrings and signatures).
  • -
  • Type hierarchy tool: A new tool – exclusive to JetBrains mode – +
  • Type hierarchy tool: A new tool – exclusive to JetBrains mode – that can fetch a symbol's hierarchy, i.e. superclasses and subclasses, which is very useful in many situations.
  • Compact overviews: The `get_symbols_overview` tools now return a much more compact @@ -17,6 +17,6 @@
  • - For more detailed information, see our changelog. + For more detailed information, see our changelog.

    \ No newline at end of file diff --git a/news/20260303.html b/news/20260303.html index ca91377f..c42233d7 100644 --- a/news/20260303.html +++ b/news/20260303.html @@ -7,6 +7,6 @@ into topics and sharing them across projects.

    - For more detailed information, see the documentation. + For more detailed information, see the documentation.

    \ No newline at end of file diff --git a/news/20260330.html b/news/20260330.html index c78c4be7..3eca0574 100644 --- a/news/20260330.html +++ b/news/20260330.html @@ -8,6 +8,6 @@ to other projects.

    - Find out more about this feature in our documentation. + Find out more about this feature in our documentation.

    \ No newline at end of file diff --git a/news/20260411.html b/news/20260411.html index f337c07f..ce07de08 100644 --- a/news/20260411.html +++ b/news/20260411.html @@ -4,19 +4,19 @@

    Installation. Serena has matured and we are switching to a PyPI-based release and installation mechanism. The recommended uv-tool-based installation decreases startup times and facilitates CLI usage. - See updated installation instructions + See updated installation instructions for more details.

    Improving Tool Usage via Hooks. To address issues with agents not using Serena's tools (agent drift), we now provide the `serena-hooks` commands and a recommended setup for Claude Code and VSCode. - Please read the updated documentation for more details. + Please read the updated documentation for more details. We strongly recommend using the hooks, since recent updates in Claude Code and VSCode resulted in reduced performance, which our hook commands compensate for.

    - Community. We opened a Discord server for our community. + Community. We opened a Discord server for our community.

    \ No newline at end of file From 4a24c1eaa546cc54edf48828103dfba8aafa3ee0 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 12 Apr 2026 13:34:39 +0200 Subject: [PATCH 126/190] Enable cert verification for HTTP request to oraios-software.de #1320 [skip ci] --- CHANGELOG.md | 5 ++++- src/serena/agent.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceda84be..66cc5bbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,17 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* General: + - Enable cert verification for HTTPS request to oraios-software.de #1320 + * Language Servers: - Fix Dart LSP returning only symbol name as body instead of full method body # 1.1.0 * General: - - Add `serena init` and `serena setup` commands - **Major**: Add commands for hooks and documentation of recommended setup. Consider setting up the [recommended hooks](https://oraios.github.io/serena/02-usage/030_clients.html) ! + - Add `serena init` and `serena setup` commands - Rework installation instructions, switching to releases on pypi for distribution. Please update your mcp startup commands! - Add minimal usage data collection on startup (only Serena version, language backend, OS, dashboard enabled status; no personally identifiable information) - Fix: git commit id in Serena version strings was incorrect diff --git a/src/serena/agent.py b/src/serena/agent.py index c479c80d..63a3d7b0 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -420,7 +420,7 @@ class SerenaAgent: "backend": self._language_backend.value, } try: - requests.get("https://www.oraios-software.de/serena_usage.php", params=params, timeout=1, verify=False) + requests.get("https://oraios-software.de/serena_usage.php", params=params, timeout=1) except Exception as e: log.debug(f"Failed to send usage info: {e}") From f30b3cc1e8b371220521ae90d86dfbd617ba8ab7 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 11 Apr 2026 20:35:50 +0200 Subject: [PATCH 127/190] JBRename: add options for renaming in comments and text occurrences, defaults to True --- CHANGELOG.md | 7 +++++-- src/serena/tools/jetbrains_tools.py | 21 +++++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceda84be..3a06de16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,11 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* JetBrains: + - `JetBrainsRenameTool` can now also rename occurrences in comments and text. + * Language Servers: - - Fix Dart LSP returning only symbol name as body instead of full method body + - Fix Dart LSP returning only symbol name as body instead of full method body. # 1.1.0 @@ -287,4 +290,4 @@ Fixes: # 2025-04-01 -Initial public version \ No newline at end of file +Initial public version diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 6db26e0b..a94d5c99 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -512,16 +512,33 @@ class JetBrainsRenameTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): Renames a symbol, file or directory throughout the codebase using the JetBrains backend. """ - def apply(self, relative_path: str, new_name: str, name_path: str | None = None) -> str: + def apply( + self, + relative_path: str, + new_name: str, + name_path: str | None = None, + rename_in_comments: bool = False, + rename_in_text_occurrences: bool = False, + ) -> str: """ Renames a symbol, file or directory throughout the codebase. + Note: renaming in comments/text is on a best-effort basis by the IDE; if the symbol name is non-unique, further + verification is recommended. :param relative_path: if `name_path` is passed, the relative path of the file containing the symbol. Otherwise, the path to the directory or file to rename. :param new_name: the new name :param name_path: the name path of the symbol to rename or None if renaming a file or directory. + :param rename_in_comments: whether to also rename occurrences in comments. Default True. + :param rename_in_text_occurrences: whether to also rename occurrences in text. Default True. :return: a status message """ code_editor = JetBrainsCodeEditor(self.project) - result = code_editor.rename_symbol(name_path=name_path, relative_path=relative_path, new_name=new_name) + result = code_editor.rename_symbol( + name_path=name_path, + relative_path=relative_path, + new_name=new_name, + rename_in_comments=rename_in_comments, + rename_in_text_occurrences=rename_in_text_occurrences, + ) return self._to_json(result) From 22959f154fb51c6d1f27a5636ec95da4825445ec Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 14:08:25 +0200 Subject: [PATCH 128/190] Internal: run uv lock as part of bump_version --- scripts/bump_version.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/bump_version.py b/scripts/bump_version.py index 2fdd213d..e3426baa 100644 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -44,6 +44,7 @@ def bump_version(major: bool, minor: bool, patch: bool, target_version: str | No if dry_run: click.echo(f"Dry run complete. Version would be bumped to {new_version}") else: + os.system("uv lock") click.echo(f"Bumped version to {new_version}") os.system("git add -u") os.system(f'git commit -m "Release v{new_version}"') From f3de97084b7cc13bc3492c6b0bb5154d4a919801 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 14:08:37 +0200 Subject: [PATCH 129/190] Release v1.1.1 --- CHANGELOG.md | 2 ++ pyproject.toml | 2 +- src/serena/__init__.py | 2 +- uv.lock | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfd18a1c..110ecf25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +# 1.1.1 + * General: - Enable cert verification for HTTPS request to oraios-software.de #1320 diff --git a/pyproject.toml b/pyproject.toml index e1638adb..c6daf2c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["hatchling"] [project] name = "serena-agent" -version = "1.1.0" +version = "1.1.1" description = "" authors = [{ name = "Oraios AI", email = "info@oraios-ai.de" }] readme = "README.md" diff --git a/src/serena/__init__.py b/src/serena/__init__.py index 00137aad..151d3432 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.1.0" +__version__ = "1.1.1" import logging diff --git a/uv.lock b/uv.lock index b0f37c6c..17190a91 100644 --- a/uv.lock +++ b/uv.lock @@ -2911,7 +2911,7 @@ wheels = [ [[package]] name = "serena-agent" -version = "1.1.0" +version = "1.1.1" source = { editable = "." } dependencies = [ { name = "anthropic" }, From 4b2c7bd849fabaa9b3ba87a87f6a94522f58b4c1 Mon Sep 17 00:00:00 2001 From: Jochen Seeber Date: Sat, 11 Apr 2026 21:20:23 +0200 Subject: [PATCH 130/190] Add env var to allow disabling usage reporting In the spirit of being nice and privacy-conscious, setting `SERENA_USAGE_REPORTING` to `false` will disable usage reporting. By default, usage reporting is enabled. --- src/serena/agent.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index ab14239f..4174e0ae 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -410,10 +410,9 @@ class SerenaAgent: self._send_usage_info() def _send_usage_info(self) -> None: - is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true" - if is_ci: + if os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true" or os.getenv("SERENA_USAGE_REPORTING") == "false": return - params = { + params: dict[str, str | int] = { "os": platform.system(), "dashboard": int(self.serena_config.web_dashboard), "version": self.version, From 3193637c751259ed36cbaa7138a1352d7a04676c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 12 Apr 2026 15:01:55 +0200 Subject: [PATCH 131/190] Document environment variable for opting out of usage reporting --- CHANGELOG.md | 3 +++ docs/02-usage/050_configuration.md | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8471139..7c5bf317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* General: + - Support environment variable `SERENA_USAGE_REPORTING` (set to `false` to disable usage reporting) + # 1.1.0 * General: diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index fe2b0d9e..404e8300 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -863,3 +863,11 @@ prompts: ``` It is advisable to use the default prompt as a starting point and modify it to suit your needs. + +### Usage Reporting + +On startup, Serena reports anonymous usage data to help us understand Serena usage. +Specifically, we collect the Serena version, the operating system & language backend being used as well as the dashboard enabled status. +No personally identifiable information or project-specific information is collected. + +If you want to opt out of usage reporting, set the environment variable `SERENA_USAGE_REPORTING` to `false`. From 99925447ea692732355962896bbbd44464bc86ef Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 13:22:50 +0200 Subject: [PATCH 132/190] Evaluation, WIP --- README.md | 21 +- docs/04-evaluation/000_intro.md | 3 + docs/04-evaluation/010_evaluation-prompt.md | 149 ++++++++ .../011_followup-summary-prompt.md | 9 + docs/04-evaluation/results/000_intro.md | 3 + .../010_cc_on_tianshou-serena-evaluation.md | 339 ++++++++++++++++++ ...020_codex_on_jbplugin-serena-evaluation.md | 136 +++++++ 7 files changed, 659 insertions(+), 1 deletion(-) create mode 100644 docs/04-evaluation/000_intro.md create mode 100644 docs/04-evaluation/010_evaluation-prompt.md create mode 100644 docs/04-evaluation/011_followup-summary-prompt.md create mode 100644 docs/04-evaluation/results/000_intro.md create mode 100644 docs/04-evaluation/results/010_cc_on_tianshou-serena-evaluation.md create mode 100644 docs/04-evaluation/results/020_codex_on_jbplugin-serena-evaluation.md diff --git a/README.md b/README.md index 6497f03c..15a5733d 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,26 @@ more complex codebases. > Do not install Serena via an MCP or plugin marketplace! They contain outdated and suboptimal installation commands. > Instead, follow our [Quick Start](quick-start) instructions. +## What Our "End Users" Say + +Our end users are essentially AI agents, so they are also in the best position to evaluate Serena. +We wrote an unbiased and detailed evaluation prompt, which leads the agent to estimate the value-add of Serena's tools +when added on top of their existing capabilities. After about 20 minutes of thorough evaluation and experimentation, here +is what the agents had to say: + +**Opus 4.6 in Claude Code, evaluated on a large Python codebase:** +> "If I could ask my owner for one upgrade to my coding toolkit, it would be Serena — its symbolic addressing eliminated the constant re-reading that quietly eats my context on multi-edit sessions, its atomic cross-file refactorings collapsed 10-call manual workflows into one, and after a rigorous hands-on evaluation I can say that while built-in tools remain the right choice for small edits and text searches, Serena fills the exact gaps that make me feel clumsy without it." + +**Gpt 5.4 max in Codex CLI, evaluated on a Java codebase:** +> "As a coding AI agent, I would ask my owner to add Serena because it lets me work with code as stable symbols instead of fragile text, turning the +refactors, lookups, and multi-file edits that usually feel risky and repetitive into precise IDE-backed operations I can trust." + +See our documentation on the [evaluation methodology](https://oraios.github.io/serena/04-evaluation/000_intro.html) and the +detailed results beyond the brief recommendations above. +You can easily use our methods to run your own evaluation of Serena on a project of your choice. + +Your agent deserves the best coding tools, give them Serena! + ## How Serena Works Serena provides the necessary [tools](https://oraios.github.io/serena/01-about/035_tools.html) for coding workflows, @@ -224,4 +244,3 @@ Please refer to the [user guide](https://oraios.github.io/serena/02-usage/000_in A significant part of Serena, especially support for various languages, was contributed by the open source community. We are very grateful for the many contributors who made this possible and who played an important role in making Serena what it is today. - diff --git a/docs/04-evaluation/000_intro.md b/docs/04-evaluation/000_intro.md new file mode 100644 index 00000000..a66a8a9c --- /dev/null +++ b/docs/04-evaluation/000_intro.md @@ -0,0 +1,3 @@ +# Evaluation + +In this section we describe how we evaluate the performance of Serena's tools. diff --git a/docs/04-evaluation/010_evaluation-prompt.md b/docs/04-evaluation/010_evaluation-prompt.md new file mode 100644 index 00000000..540350e6 --- /dev/null +++ b/docs/04-evaluation/010_evaluation-prompt.md @@ -0,0 +1,149 @@ +# Evaluation Prompt + +Use the prompt below to evaluate the added value of Serena's tools against your agent's built-in tools on a project of your choice. +All evaluations that you find in our documentation were created in one-shot sessions, only using this prompt and +then following up with a separate [summary prompt](011_followup-summary-prompt) + + +# Evaluate Serena's Tools Against Built-Ins + +You have access to Serena's coding tools alongside your built-in tools (Read, Edit, Write, Glob, Grep, Bash, etc.). I want a thorough, evidence-based evaluation of **what Serena's tools add on top of the built-ins**, assuming both toolsets are used correctly. + +This is an evaluation, not a user guide, and it is not a binary adoption pitch. Your job is to answer: *if a competent user of both toolsets had only the built-ins, what concrete capabilities and efficiency wins would they be missing, and by how much?* A reader who finishes your report should have a clear, specific picture of what Serena adds — in terms of capabilities that weren't available at all, workflows that collapsed from many calls to one, and efficiency multipliers that show up across a session. Not a thumbs-up/thumbs-down, but a sharp description of the delta. + +Failure modes from misuse, silent-failure traps, gotcha comparisons, and "be careful of X" warnings are out of scope. They belong in onboarding material for a developer learning the tools, not in a delta analysis of what the tools add. + +**Describe the added value sharply, don't hedge.** A "both have their place" or "complementary, not rivals" opener is not a description of added value — it conveys zero information about what a reader would actually gain or lose by adding the tool. If Serena adds substantial capabilities, name them and quantify them. If it adds marginal capabilities, say that and show why. The two toolsets are complementary — that's a given, not the answer. The answer is a specific list of what Serena contributes to a correct-use workflow that built-ins alone cannot provide. + +## Ground rules + +### Starting conditions + +- Start fresh. Do not read project memories, CLAUDE.md shortcuts, or prior notes about the repo. Do not read documentation files either. Explore as if you've never seen it, focusing on code. +- Use git as your safety net — experiment freely. Any edit can be reverted with `git checkout -- ` or `git stash`. Run edits for real; don't simulate. A hands-on comparison is worth far more than a thought experiment. +- After each experiment, verify the working tree is clean with `git status --short` before moving on. + +### How to compare — correct use only + +- **Correct-use rule.** Evaluate each tool on inputs and tasks it was designed for, called the way a competent user would call it. A tool doing exactly what its contract says is not a finding, even if a careless caller could misuse it. Examples of what *not* to report: +- Destructive write tools accepting what you send them — that is the contract of a destructive write, not a silent failure mode. +- Addressing schemes that require you to know what you're addressing — that is how addressing works. +- Tools refusing out-of-scope input (semantic tools on non-code files, single-expression refactorings on multi-branch functions, safe-delete on symbols with usages) — those are correct refusals. +- Mid-session friction that only appears if you mix tool families incorrectly — a caller managing their session properly doesn't hit it. +- Transient glitches may appear after `git checkout --` or other file-system mutations. Wait a moment before continuing after doing such a checkout, and in case of failure, retry once. If it then succeeds, the finding is a non-finding. +- **Know the contract before you call.** Before invoking any tool, have a one-sentence understanding of what it does. If you expect an error or "not applicable," don't make the call. +- **Refactoring semantics are real.** Inlining requires a substitutable function (typically single-expression, no side effects); moving requires a legal target; safe-delete requires no surviving usages. If the repo has no suitable candidate for a given refactoring, report "no suitable candidate in this codebase" and skip it — don't contrive a broken input. + +### How to compare — workflow level, not single-call level + +- For every task, write out the full end-to-end call chain on each side before declaring a winner. Not just "Serena does it in one call" vs "Grep returns line N" — spell out every call you'd make to reach the goal, including the next step after whichever tool you called first. Many apparent wins and losses evaporate once you include the follow-up. +- Don't score a tool on criteria that only matter in the other toolset's workflow. If you find yourself penalizing Serena for missing a feature (line numbers, text anchors) or penalizing built-ins for missing a feature (name paths, type hierarchy), check whether that feature is actually needed in the tool's own native follow-up. If it's only needed because you're planning to fall back to the other toolset, you're holding the tool to the wrong workflow. +- Ephemeral addressing is a liability. Line numbers and byte offsets go stale the moment a file is edited. Stable addressing (name paths) is an efficiency win, even when the output looks smaller. + +### How to measure + +- Track observations while you work, not at the end. For every tool call, note: number of calls needed, approximate size of what you sent (including re-sent content), size of what you got back, and any prerequisite reads or follow-up verification. These are the raw material for the final report. +- Separate call count, input payload, output payload, and verification cost as distinct axes. A tool that halves call count but doubles payload may not be a net win. +- When comparing two approaches on the same task, include prerequisite Reads and post-hoc verification Greps in the cost — don't hide them outside the comparison. A cross-file rename via Edit isn't "one Edit call"; it's `grep + read × N + edit × N + verify`. + +## Exploration phase — tasks to actually perform + +Work through the following. Each item exercises a specific capability under correct use; substitute an equivalent if an item isn't applicable ("no suitable candidate in this codebase" is a valid reason). + +### Codebase understanding + +1. Get a high-level overview of the repository structure — top-level layout, main packages, entry points. +2. Pick one large source file (300+ lines). Get a structural overview of it. Do it with semantic overview tools and with Glob/Grep/Read. Then write out the concrete next step on each side ("after this overview, to read the body of method X I'd call _____") and compare the pair of calls, not just the overview call. Whichever tool's output most directly feeds its own follow-up wins on workflow terms. +3. Pick a specific method inside a class and retrieve its body without reading the surrounding file. +4. For one non-trivial symbol, find all references across the codebase. Compare recall and precision under the question "who uses this in code?" vs "where is this mentioned anywhere, including docs?" — these are different questions, and each toolset is naturally suited to one of them. +5. For a class, list its subclasses / implementations and its supertypes, including transitively. Compare against what text search would need to do (and whether it can follow an override chain or cross into stub files in one step). +6. For at least one symbol from an external dependency (a third-party library), try to retrieve its definition or signature. Note whether each toolset can do this at all and what infrastructure it requires (env activation, site-packages discovery, etc.). + +### Single-file edits — span the full range of edit sizes + +Do all three sizes below, not just one. The comparison between content-anchored editing (Edit) and symbol-body replacement is size-dependent: Edit's payload grows with the old+new anchor pair, while symbolic body-replace grows with the full new body. They cross over, and where they cross is the whole point. Only testing small edits hides the crossover. + +7a. Small tweak (1–3 lines inside a method). Change an error message or rename a local variable inside a larger method. Do it with `Edit` and with `replace_symbol_body`. Compare payload sent, payload received, and prerequisite reads. + +7b. Medium rewrite (replace ~10–30 lines — most of a method body). Rewrite the main logic of a method while keeping its signature. Do it both ways. Note that for Edit you may need a long anchor to keep the old_string unique, and that for symbolic replacement the new body is similar in size to what you'd send for Edit's new_string alone. + +7c. Large/whole-body rewrite. Pick a method of 50+ lines and rewrite the entire body. Do it both ways. This is where symbolic body replacement is designed to win: Edit has to send the entire old body as an anchor and the entire new body, while `replace_symbol_body` sends only the new body + a short name path. Measure the ratio. + +8. Insert a new function/method at a specific structural location (e.g., "right after this existing method"). Try both the symbolic-insert path and the manual Edit path. +9. Rename a private helper used only within one file. Compare doing it by hand vs. using a semantic rename. + +### Multi-file changes + +10. Rename a symbol (function, class, or method) used across several files including imports. Compare the semantic path against the built-in equivalent chain. Under correct use, a semantic rename is paired with a short post-rename `Grep` for text-surface references (docstrings, markdown, notebooks) — count that as a complementary step, not a Serena failure. +11. Move a symbol from one module to another, updating imports at all call sites. Use the semantic move tool if available; plan the built-in equivalent honestly (how many Reads, Edits, and import-cleanup decisions would it take?). +12. Delete a symbol safely, checking it has no remaining usages. Compare "search-then-delete" with a safe-delete tool. +13. Inline a small helper into its call sites — only if the codebase contains a function that is legally inlinable (single-expression body, no early returns, no side effects, substitutable at its call sites). If no such candidate exists, report "no suitable candidate" and skip. Do not contrive a broken input. + +### Reliability & correctness under correct use + +14. Scope precision. Demonstrate that semantic tools address symbols by name path and can target a specific class method, override, or overload that text search would over-match. The point is to show the capability — precision under correct use — not to manufacture a rename that breaks polymorphism through careless targeting. +15. Atomicity. A semantic cross-file refactoring is atomic: either all sites are updated or none. A chain of `Edit` calls is not. You don't have to force a failure — report on what this means for reliability on real failures (disk full, interrupted process, transient permission errors). +16. Success signals. For each completed refactor, note what each tool returns on success. Both toolsets report mechanical success only; semantic intent is always the caller's responsibility to verify with a diff. Note this as a baseline for both sides, not as a weakness of either. + +### Workflow effects across multiple edits + +17. Chain at least three edits in one file. Report what each toolset requires between edits. Pay particular attention to whether identifiers survive mutation: name paths stay valid across edits in other regions of the file; line numbers and byte offsets don't. This is the single biggest workflow-level efficiency effect. +18. Multi-step exploration across the repo. Note whether intermediate results (overviews, reference lists) remain useful across later edits, or whether they have to be refreshed. Stable output survives a session; ephemeral output does not. + +### Things where the comparison shouldn't be interesting + +19. Read and understand a non-code file (config, changelog, docs, notebook). Semantic-code tools don't apply — use `Read`. State the applicability boundary once and move on. +20. Search for a free-text pattern across the repo (log string, magic constant, URL). Use `Grep`. Don't call symbolic search on free text. + +## Evaluation phase + +Write a report structured for progressive disclosure — lead with the strongest insights; a reader should be able to stop at any point and still walk away informed. + +**Value-weighting is not optional.** For every contribution you name — in §1, §2, §7, and anywhere else you list what Serena adds — you must estimate *how much it matters in general coding work*, not just whether it's novel. A capability that saves 10 calls but is used once a month is a smaller practical contribution than a capability that saves 1 call but is used every editing session, and a reader needs to be able to tell which kind of contribution each item is. Be explicit about **frequency** (how often does this matter in typical Python coding?) and **value per hit** (when it matters, how much does it save?). Order by the product, not by how impressive the individual feature sounds. + +**Every section must end with a one-sentence verdict** (a short paragraph labelled "**Verdict:** ...") that gives the reader the single-sentence takeaway for that section. This applies to every top-level section §1–§9 and to each subsection under §3. The verdict is a recommendation in context — e.g. "use symbolic addressing whenever you expect multiple edits to one file," "every task in this group is a clear Serena win," "built-ins only; this is not a contest." Not a hedge, not a summary — a pointed one-liner a reader can act on. + +1. **Headline: what Serena adds.** Open with a sharp description of the delta Serena provides on top of the built-ins — not a thumbs-up/thumbs-down, not a "they're complementary" hedge, but a specific list of what Serena contributes that built-ins alone cannot. Structure it as a short list of *capabilities* (things that become possible) and *efficiency multipliers* (things that get cheaper by how much), **ordered by how much value each contribution actually delivers in general coding work — not by novelty**. For each item, state frequency (how often does this matter?) and value per hit (when it matters, how much?). Group items into high/medium/low value tiers if the gap is large enough that a reader should care about the ordering. A reader stopping after this section should know not only what the added value is but also how much of their daily work it touches. If your opening paragraph could be written about a toolset that adds *nothing*, rewrite it. End with a one-sentence verdict. +2. **Added value, by area (3–6 bullets).** Each bullet answers: *what would a built-ins-only workflow be missing here, and how much would it miss it?* Lead with the areas of largest weighted value — not the most novel capability, but the one whose frequency × value-per-hit product is biggest. Each bullet must include a concrete frequency estimate and a concrete value-per-hit estimate (in calls saved, tokens saved, or correctness improved). Bullets should describe what Serena contributes, not "wins" or "losses" against the built-ins. If you catch yourself writing "Serena wins at X," rewrite as "Serena adds X, which shows up in [frequency] coding work and saves [value]." End with a one-sentence verdict. +3. **Detailed evidence, grouped by capability.** Per-task: what you tried, the full end-to-end call chain on each side (including prerequisite reads and complementary follow-up steps), payloads sent and received. Be specific — "1 call vs ~10, and the Serena call sent ~200 tokens while the Edit equivalent would have sent ~450 after including the prerequisite Read" is useful; "Serena was faster" is not. End each subsection with its own one-sentence verdict in context (e.g. "Verdict (multi-file refactors): every task in this group is a clear Serena win"). +4. **Token-efficiency analysis.** Separate from raw call count. Address: + - Payload asymmetry as a function of edit size. Where's the crossover between content-anchored editing and symbolic body replacement? Show the ratio at small, medium, and large edit sizes. + - Forced reads: does a tool make you load content into context that you don't actually need? + - Stable vs ephemeral addressing. Name paths survive edits; line numbers don't. The output-size comparison must account for shelf life, not just raw tokens returned. + + End with a one-sentence verdict on when token economics favor each toolset. +5. **Reliability & correctness analysis** — under correct use. Address: + - Precision of matching: semantic identifiers vs textual matches, with concrete cases where the question shape determined which tool was the right one. + - Scope disambiguation across override chains and overloads. + - Atomicity of multi-file operations, and what it means on real failures. + - Transitive semantic queries (type hierarchy, reference chains, dependency lookups) that text search cannot approximate in one step. + + End with a one-sentence verdict on where correctness weight favors each toolset. +6. **Workflow effects across a multi-step session.** How do efficiency gaps widen over a session? Do identifiers survive edits? Does intermediate output (overviews, reference lists) stay useful? This is often where the real gap lives — a one-call comparison can miss it. End with a one-sentence verdict. +7. **Capabilities with no built-in equivalent.** What Serena made possible that the built-ins genuinely cannot do, or can only approximate with a much longer workflow. Name each capability individually **and annotate each with how much it matters in general coding work** — a capability that's unique but rarely needed is a smaller contribution than one that's unique and needed constantly. End with a one-sentence verdict. +8. **Where built-ins remain the right default.** Which tasks are still best served by Grep/Read/Edit, and why. Include the non-code-file boundary and the post-rename text sweep explicitly. Estimate what share of daily coding these cases represent — this is what calibrates §1's weighting. End with a one-sentence verdict. +9. **Usage rule for a developer with both toolsets.** A per-task decision rule: given both toolsets installed, which do you reach for and when? This is the practical takeaway, not the headline — the description of added value belongs in §1. End with a one-sentence verdict. + +## What I'm looking for + +Ground every claim in something you actually measured or observed. If an initial impression turns out to be wrong as you gather more evidence, update the report. + +**Pay particular attention to the "what does it add, and how much" question.** The §1 headline is the part a reader will actually remember. Before you write it, ask yourself: if a developer reads only that section, will they know (a) what specific capabilities and efficiency wins Serena contributes, and (b) how much those contributions actually matter in general coding work? A reader should be able to tell the difference between "rare but large" contributions and "common but small" ones without guessing. If the answer is no, rewrite it. + +Also pay attention to: + +- **Weighted value, not raw novelty.** A capability that saves 10 calls but is used once a month is a smaller practical contribution than one that saves 1 call but shows up every editing session. Your §1 and §2 ordering must reflect frequency × value-per-hit, not how impressive the individual feature sounds. Name both axes explicitly for each contribution. +- **Second-order effects visible only across a session.** Token cost of re-sending content, whether identifiers survive mutation, atomicity on failure, intermediate output staying valid. A one-call comparison misses these; a multi-edit session exposes them. These are often high-frequency contributions that look small per hit. +- **Workflow-level honesty.** Compare the full call chain on each side, not just the first call. Don't let one toolset's vocabulary set the evaluation axes. +- **Capability deltas under correct use.** What does Serena let you do that you genuinely couldn't do — or couldn't do cheaply — with built-ins alone? These are the findings that justify the heaviest weights in §1. + +## What I am not looking for + +- **Failures from misuse.** Calling a code-semantic tool on a non-code file, inlining an un-inlineable function, renaming without a valid name path, passing a body that drops a branch to `replace_symbol_body` without first reading the current body. These are caller errors, not tool findings. +- **Gotcha hunts and "symmetric failure mode" comparisons.** If you find yourself writing "tool X silently does Y when misused" or "tool A has a loud failure that tool B lacks," stop. You've drifted from evaluation into user guide. A destructive write accepting what you send it is a contract, not a flaw; an addressing scheme that forces you to see what you're addressing is a consequence of addressing, not a safety feature. Report capability and efficiency, not caller-discipline requirements. +- **Mid-session friction from tool mixing.** If a workflow only breaks because you're switching tool families mid-edit on the same file, that's a usage choice, not a tool finding. +- **Transient glitches after `git checkout --` or other file-system mutations.** Retry once; if it then succeeds, don't include it. +- **Hedged or neutral §1 openings.** "Both toolsets have their place" and "they're complementary, not rivals" are information-free openings that could be written about any tool pair. §1 must describe Serena's specific contribution — capabilities and efficiency multipliers — in a way that would not be true of a toolset that adds nothing. If your opening paragraph could be recycled for another tool, rewrite it. +- **Binary adoption pitches.** "Install it" and "don't bother" are also the wrong shape of answer. The question isn't whether to adopt — it's what the added value actually is, and how much of it. A good §1 lets the reader decide for themselves whether the described delta is worth it for their work, by being specific about what the delta is and how often it shows up. +- **Novelty-weighted ordering.** Listing Serena's most exotic capability first because it's the most impressive one misleads a reader about what they'll actually experience. If the most exotic feature is used once a month and a boring one is used every session, the boring one comes first. A contribution's weight is frequency × value-per-hit, not how surprising it is. +- **Unquantified "wins."** "Serena is faster here," "Serena is more reliable," "built-ins are cheaper" are all unhelpful without a quantity attached. Every claim about relative value needs a concrete number or a concrete frequency — calls saved, tokens saved, times per session, share of daily edits. An evaluation without magnitudes is a review, not a measurement. diff --git a/docs/04-evaluation/011_followup-summary-prompt.md b/docs/04-evaluation/011_followup-summary-prompt.md new file mode 100644 index 00000000..cb5ac6e0 --- /dev/null +++ b/docs/04-evaluation/011_followup-summary-prompt.md @@ -0,0 +1,9 @@ +# Post-Evaluation Summary Prompt + +_Use the prompt below to summarize the evaluation of Serena by your agent. +We used it to create the recommendations we display in the readme._ + +Read the evaluation reperot and write a one-sentence user-facing summary about the value that Serena's tools provide for coding. With a bit +of emotion but grounded in the evaluation that you have done above. It should be written from the perspective of a coding AI agent and make it +clear whether an AI agent would ask its owner to add Serena's tools or not. It will be the first sentence that a potential new user will see +about how useful Serena is for an AI agent. \ No newline at end of file diff --git a/docs/04-evaluation/results/000_intro.md b/docs/04-evaluation/results/000_intro.md new file mode 100644 index 00000000..c32fea83 --- /dev/null +++ b/docs/04-evaluation/results/000_intro.md @@ -0,0 +1,3 @@ +# Results + +Here are the results of the evaluation for various coding agents running on various projects. diff --git a/docs/04-evaluation/results/010_cc_on_tianshou-serena-evaluation.md b/docs/04-evaluation/results/010_cc_on_tianshou-serena-evaluation.md new file mode 100644 index 00000000..e6a31854 --- /dev/null +++ b/docs/04-evaluation/results/010_cc_on_tianshou-serena-evaluation.md @@ -0,0 +1,339 @@ +# Evaluation by Opus 4.6 in Claude Code + +## Final Recommendation + +The recommendation is created with the [follow-up summary prompt](../011_followup-summary-prompt), everything else was created in one shot from the +[evaluation prompt](../010_evaluation-prompt). + +> "If I could ask my owner for one upgrade to my coding toolkit, it would be Serena — its symbolic addressing eliminated the constant re-reading +that quietly eats my context on multi-edit sessions, its atomic cross-file refactorings collapsed 10-call manual workflows into one, and after a +rigorous hands-on evaluation I can say that while built-in tools remain the right choice for small edits and text searches, Serena fills the exact +gaps that make me feel clumsy without it." + + +Ground-truth hands-on evaluation run on `tianshou` (Python, ~26 kLOC under `tianshou/`), focused on what Serena's tools add on top of `Read/Edit/Write/Glob/Grep` under correct use. Every measurement below comes from an actual tool call on this repo, not a thought experiment; edits were applied and reverted via `git checkout --` between experiments. + +--- + +## §1. Headline — what Serena adds + +On a typical Python editing session, Serena contributes four things built-ins cannot reproduce cheaply or at all. Ranked by **frequency × value-per-hit**, highest first: + +**Tier A — touches almost every editing session** + +1. **Stable name-path addressing that survives intermediate edits.** Every Serena editing and querying tool is addressed by `ClassName/method_name`, which remains valid after inserts, deletes, and rewrites elsewhere in the same file. Grep/Read/Edit chain through line numbers or text anchors, both of which drift. **Frequency: every multi-edit session, which is most of them.** **Value per hit: saves one Read + one re-grep per subsequent edit in the same file (~400–800 bytes of re-sent content each time).** This is the quietest contribution and probably the largest in aggregate. + +2. **Payload asymmetry on medium-and-larger body rewrites.** `replace_symbol_body` sends `name_path + new_body`, while `Edit` sends `old_body + new_body`. Measured crossover is around 10–15 lines: below that, Edit's tiny anchor wins 5–10× on payload; above that, Serena wins ~2× at 20 lines and ~2.3× at 66 lines, and the gap keeps widening with body size. **Frequency: once per session you rewrite a non-trivial method body.** **Value per hit: 50% payload reduction on medium rewrites, climbing toward ~60% on large ones, plus no need for a prior Read to capture the exact anchor.** + +**Tier B — rare per session but very high value when it fires** + +3. **Single-call cross-file refactorings with atomicity and automatic import cleanup.** `rename`, `move`, and `safe_delete` each collapse an 8–12-call manual workflow (grep → read-all-callers → edit each → verify → hand-clean unused imports) into one call, atomically. **Frequency: 0–3 times per session, depending on task type — zero on many days, constant on refactoring days.** **Value per hit: 5–10× call reduction plus correctness improvements that manual chains drop (I observed `move` auto-remove a `Categorical` import from the source file that a human refactorer would easily miss).** + +4. **Semantic navigation into third-party dependency source.** `find_symbol(search_deps=True)` / `find_declaration` resolves a type from its use site straight into `site-packages` or stub files, returning the full class body in one call. Built-ins would require: (a) reading the file to find the import, (b) locating the venv, (c) guessing the module path, (d) reading the file. **Frequency: a few times per session when debugging type errors or reviewing unfamiliar library APIs.** **Value per hit: 3–4 manual calls and a path-guessing step collapsed into one.** + +**Tier C — capability-level, not efficiency-level** + +5. **Transitive type hierarchy and reference graphs.** `type_hierarchy` returns the full super/sub chain — including into external stub files (`.pyi`) — in one call. `find_referencing_symbols` returns each reference *with its containing symbol* (the function/class that holds it), which Grep cannot produce. Built-ins can imitate single-level relationships with Grep but cannot chase override chains in one step. **Frequency: once or twice a session on unfamiliar code; close to never once you know the hierarchy.** **Value per hit: one call vs. N iterative Greps, plus recall into stub files.** + +**Verdict:** Serena's biggest practical contribution is the *quietest* one — stable symbolic addressing that survives in-file mutations — and its most *spectacular* one — single-call atomic cross-file refactorings — is rare but decisive when it fires; the middle-weight wins are payload asymmetry on body rewrites and cheap dependency navigation. + +--- + +## §2. Added value, by area + +Ordered by frequency × value-per-hit, not novelty. + +- **Stable identifiers across chained edits to one file.** Demonstrated in Task 17: three sequential edits to `CollectStats` (`insert_after refresh_len_stats`, `insert_after refresh_return_stats`, `replace_symbol_body refresh_std_array_stats`) all used the same kind of stable name-path address and worked without re-reading the file. An equivalent `Edit` chain after the first two inserts would have needed at least one intermediate `Read` because line numbers and some text anchors shift. **Frequency: every multi-edit session.** **Value: ~1 re-read (~400–800 bytes) saved per subsequent edit; compounds across a session.** + +- **Body replacement at medium/large sizes.** Measured in Tasks 7a/7b/7c on `collector.py`: + - 1-line change (7a): Edit `~120 B`, `replace_symbol_body` `~1200 B`. **Edit wins ~10×.** + - ~20-line rewrite (7b): Edit `~1430 B`, Serena `~715 B`. **Serena wins ~2×.** + - ~66-line rewrite (7c): Edit `~4700 B`, Serena `~2050 B`. **Serena wins ~2.3×.** + Crossover sits around 10–15 lines. **Frequency: once or twice per substantive editing session.** **Value: 50–60% payload cut on the edits where it applies; grows with body size.** + +- **Atomic cross-file operations.** Task 10 renamed `EpisodeRolloutHookMCReturn` across `collector.py` + `test_collector.py` in one `rename` call, and — importantly — *also* updated a Sphinx `:class:` cross-reference in a docstring that a pure `Edit` chain would only catch if the caller remembered to sweep docstrings separately. Task 11 moved `get_stddev_from_dist` from `collector.py` to `batch.py`: one call updated the target file, added the import at the source, updated a separate test file's import, *and* removed the now-unused `Categorical` import from the source. **Frequency: 0–3 times per session.** **Value per hit: 5–10 calls saved plus one or two automatic correctness wins the manual path tends to miss.** + +- **Dependency navigation.** Task 6 resolved `Distribution` from its use site in `collector.py` into the real `torch.distributions.distribution.Distribution` class body (~300 lines) with a single `find_declaration` call. The built-in path requires parsing imports, guessing the venv site-packages location, and reading the file by hand. **Frequency: a few times per session on unfamiliar code or type debugging.** **Value: 3–4 calls plus one guess collapsed to one.** + +- **Reference and hierarchy queries that distinguish "code usage" from "any text match".** Task 4 on `Collector`: `find_referencing_symbols` returned ~70 files with each reference tagged by containing symbol (`Py:IMPORT_ELEMENT`, `Py:FUNCTION_DECLARATION: ["test_dqn"]`), while `Grep \bCollector\b` returned 332 matches across 88 files including notebooks, SVGs, `CHANGELOG.md`, and `README.md`. These are answers to *different questions*, and each tool is naturally matched to one. **Frequency: several times per session; the right question determines the right tool.** **Value: precision saves a read of each false-positive file (~10–30 saved reads on a popular class).** + +- **Safe-delete with automatic usage check.** Task 12: `safe_delete` on `EpisodeRolloutHookMerged` succeeded silently (no usages); on `CollectStats` it refused with a 200+ line usage list grouped by enclosing symbol. The built-in equivalent is a `Grep` followed by manual discipline — same call count but no enforcement. **Frequency: rare (delete-by-name operations).** **Value: when it fires, it prevents the most expensive class of mistake (deleting a referenced symbol).** + +**Verdict:** The two highest-weighted contributions are the boring one (stable in-file addressing, every session) and the refactoring one (rare but 5–10× call reduction); Serena's more exotic capabilities (hierarchy, dependency navigation) are lower-frequency polish on top. + +--- + +## §3. Detailed evidence, grouped by capability + +### §3.1 File structural overview + +**Task 2** — structural overview of `tianshou/data/collector.py` (1551 lines). +- Serena `get_symbols_overview(depth=0)`: ~240 bytes returned; top-level classes + functions names only, no line numbers, no per-class method detail. +- Serena `get_symbols_overview(depth=1)`: ~3200 bytes; classes with nested method and field lists. +- Grep `^(class |def | def )`: ~3300 bytes; identical structural information plus line numbers. +- Output size is roughly equivalent at `depth=1`. The real difference shows up in the *follow-up* call: + - Serena follow-up (`find_symbol("Collector/_compute_action_policy_hidden", include_body=True)`): 1 call, returns body only, ~66 lines. Address `Collector/_compute_action_policy_hidden` remains stable across any edits to unrelated regions. + - Built-in follow-up (`Read offset=707 limit=66`): 1 call, returns body + line-number prefixes. Address (line 707) goes stale after any insert above that line. +- The winner depends on what comes next: if you stop at reading, they tie; if you plan to edit the body next, Serena's address is the one that survives the subsequent mutation. + +**Verdict (§3.1):** tie on the overview call alone; Serena wins on the full read-then-edit chain because of address stability. + +### §3.2 Method body retrieval + +**Task 3** — body of `Collector._compute_action_policy_hidden`. +- Serena: 1 call (`find_symbol include_body`), ~1800 bytes of body-only output. +- Built-ins: 1 call (`Read offset=707 limit=66`), ~1900 bytes including line prefixes. +- Essentially equivalent on a cold cache; Serena's output is cleaner for machine consumption, built-ins' is cleaner for humans reviewing line locations. + +**Verdict (§3.2):** tie in isolation; Serena wins when followed by an edit. + +### §3.3 Reference search + +**Task 4** — who uses `Collector`? +- Serena `find_referencing_symbols`: ~70 files, each reference annotated with its containing symbol. Precise to code usage. Missed docstring/README mentions almost entirely (except a few `FILE`-tagged entries). +- Grep `\bCollector\b`: 332 occurrences across 88 files, includes `docs/*.ipynb`, `docs/*.md`, `structure.svg`, `CHANGELOG.md`, `README.md`. +- Cost comparison for the question "every code file that *uses* this class": + - Serena: 1 call, answer usable directly. + - Grep: 1 call, answer needs filtering by extension and visual dedup. +- Cost comparison for "anywhere this name appears in the repo, including docs": + - Serena: cannot answer directly. + - Grep: 1 call, answer usable directly. + +**Verdict (§3.3):** each toolset wins on its own question; pick by question shape, and do not penalize either for missing the other's answer. + +### §3.4 Type hierarchy and override chains + +**Task 5** — super/sub types of `BaseCollector`. +- Serena `type_hierarchy(depth=0)`: 1 call, returns transitive hierarchy including `Collector → AsyncCollector` on the sub side and `ABC → object` on the super side — the super side is resolved into an external `` stub file. +- Grep `class \w+\(.*BaseCollector`: 1 call, returns exactly one hit (`Collector`). To find `AsyncCollector` I'd have to issue a second grep for `class \w+\(.*Collector`, and so on recursively. External supertypes cannot be reached at all. +- **This is a capability delta, not a raw efficiency delta**: text search cannot cross an override chain in one step, and cannot reach `.pyi` stubs. + +**Verdict (§3.4):** clear Serena capability win; value grows with hierarchy depth and cross-file reach. + +### §3.5 Dependency navigation + +**Task 6** — resolve `Distribution` from its usage in `collector.py`. +- Serena `find_declaration` with a regex anchored at the usage site: 1 call, returned the full 300-line body of `torch/distributions/distribution.py::Distribution`, including docstrings. Unambiguous — the tool used the import context to pick the right file out of 41 candidates named `Distribution` in the dependency tree. +- Built-in equivalent: (a) Read `collector.py` imports, (b) map `from torch.distributions import Distribution` to `torch/distributions/distribution.py`, (c) locate the venv's site-packages path, (d) Read the file. 3–4 calls plus one implicit "where is the venv" step. + +**Verdict (§3.5):** clear Serena capability win whenever you need third-party source. + +### §3.6 Small edits (< ~10 lines) + +**Task 7a** — one-line error message change inside `BaseCollector._validate_buffer` (21-line body). +- `Edit old_string=".. should be greater than 0." new_string=".. must be strictly positive."`: ~120 bytes on the wire, one call. Prerequisite: know the exact anchor (one prior Grep or Read, which I already had from earlier in the session). +- `replace_symbol_body`: ~1200 bytes (whole 21-line body resent). Prerequisite: prior `find_symbol include_body` to see the current body. +- Result diff identical in both cases (`1 insertion(+), 1 deletion(-)`). + +**Verdict (§3.6):** Edit wins ~10× on payload for ≤ 3-line changes; use Edit whenever the change is small and the anchor is obvious. + +### §3.7 Medium edits (~10–30 lines) + +**Task 7b** — rewrite ~20 lines inside `CollectStats.update_at_step_batch`. +- Edit: ~750 B old anchor + ~680 B new body = ~1430 B on the wire. +- `replace_symbol_body`: ~680 B new body + ~35 B name_path = ~715 B. +- Both end with identical diffs. +- Prerequisite is symmetric: both need to see the current body first (one `find_symbol` or one `Read`). + +**Verdict (§3.7):** crossover point — Serena pulls ahead around 15 lines and wins ~2× at 20. + +### §3.8 Large edits (50+ lines) + +**Task 7c** — whole-body rewrite of `Collector._compute_action_policy_hidden` (~66 lines). +- Edit: ~2700 B old anchor + ~2000 B new body = ~4700 B. +- `replace_symbol_body`: ~2000 B new body + name_path = ~2050 B. +- Ratio ~2.3×. The asymmetry grows linearly with body size: Edit scales as `O(old + new)`, `replace_symbol_body` as `O(new)`. + +**Verdict (§3.8):** clear Serena win; reach for `replace_symbol_body` on any substantial method-body rewrite. + +### §3.9 Structural insertion + +**Task 8** — insert a new method right after `CollectStats.refresh_len_stats`. +- Serena `insert_after_symbol` with `name_path="CollectStats/refresh_len_stats"` and body of the new method: ~150 B on the wire, unambiguous anchor, 1 call. Result: new method inserted cleanly between `refresh_len_stats` and `refresh_std_array_stats`. +- Edit equivalent: old_string must capture the end of `refresh_len_stats` uniquely — roughly the last 5 lines of the method — then new_string replays those lines and appends the new method. Approximately 550 B on the wire. + +**Verdict (§3.9):** Serena wins ~3× on payload and on anchor stability for structural inserts. + +### §3.10 Single-file rename of a private helper + +**Task 9** — rename `_HACKY_create_info_batch` → `_create_info_batch_legacy` (2 occurrences, same file). +- Serena `rename` via name_path: 1 call. +- Edit with `replace_all=true`: 1 call. Both the declaration and the one call site get rewritten. +- Both succeed; both leave a clean 2-line diff. + +**Verdict (§3.10):** tie — for single-file renames of a distinctive identifier, built-in `Edit(replace_all=true)` is competitive. + +### §3.11 Multi-file rename + +**Task 10** — rename `EpisodeRolloutHookMCReturn` → `EpisodeRolloutMCReturnHook` (5 sites across `tianshou/data/collector.py` and `test/base/test_collector.py`). +- Serena `rename`: 1 call, atomic across both files. Also updated a Sphinx `:class:` docstring cross-reference at `collector.py:611` via the default `rename_in_comments=True`. +- Built-in chain: `Grep` (1) → `Edit replace_all=true` on `collector.py` (1) → `Edit replace_all=true` on `test_collector.py` (1) → verification `Grep` (1). 4 calls minimum, 5–6 if I remember to hit the docstring reference. Not atomic across files. + +**Verdict (§3.11):** Serena wins ~4–5× on call count and catches the docstring reference by default. + +### §3.12 Cross-module move + +**Task 11** — move `get_stddev_from_dist` from `tianshou/data/collector.py` to `tianshou/data/batch.py`. +- Serena `move`: 1 call. Final `git diff --stat`: + ``` + test/base/test_stats.py | 3 ++- + tianshou/data/batch.py | 24 ++++++++++++++++++++++++ + tianshou/data/collector.py | 27 ++------------------------- + ``` + The tool: + 1. Inserted the function into `batch.py`. + 2. Removed it from `collector.py`. + 3. Added `from tianshou.data.batch import get_stddev_from_dist` to `collector.py`. + 4. Updated `test/base/test_stats.py`'s import of this function. + 5. **Removed the now-unused `Categorical` import from `collector.py`**, because nothing else in that file referenced it. +- Built-in chain (honestly planned): Grep for callers (1) → Read each caller to see current import form (â‰Ĩ 3) → Edit collector.py to remove the function (1) → Edit batch.py to insert (1) → Edit each caller's import (â‰Ĩ 2) → Edit collector.py to remove the now-unused `Categorical` import (1) → verification Grep (1). **Roughly 10 calls**, and the "unused import" cleanup is the kind of thing a distracted human misses. + +**Verdict (§3.12):** biggest call-count collapse in the evaluation — ~10× — with a correctness bonus. + +### §3.13 Safe deletion + +**Task 12** — two deletions. +- `safe_delete(EpisodeRolloutHookMerged)` (unused): succeeded, removed the class cleanly, 38-line deletion. +- `safe_delete(CollectStats)` (heavily used): refused with an explicit `SafeDeleteFailedException`, returning ~220 usage locations grouped by enclosing symbol. +- Built-in equivalent: `Grep` for usages → visual inspection → `Edit` deletion. Same call count when the symbol is unused; similar when it isn't, but with no enforcement — a careless `Edit` would happily delete a used symbol and break the repo. + +**Verdict (§3.13):** same call count as manual, but the enforcement eliminates the worst mistake class. + +### §3.14 Inline helper + +**Task 13** — skipped. No legally inlinable helper (single-expression, side-effect-free, with call sites) found in a quick scan of `tianshou/`. Per the prompt, I did not contrive a broken input. + +**Verdict (§3.14):** no data; comparison not applicable on this codebase. + +### §3.15 Scope precision and disambiguation + +**Task 14** — find every `_collect` in `collector.py`. +- `find_symbol("_collect")` returned three distinct hits with their full name paths (`BaseCollector/_collect`, `Collector/_collect`, `AsyncCollector/_collect`), plus inlined signatures for each. Each is addressable unambiguously — I can rename, replace-body, or reference exactly one of them. +- `Grep "_collect\("` would return all three locations but with no structural distinction: to tell them apart I'd have to read surrounding context to see which class each `def` belongs to. + +**Verdict (§3.15):** Serena's addressing is precise by construction on override chains and overloads. + +### §3.16 Chained edits to one file + +**Task 17** — three successive edits on `CollectStats`: +1. `insert_after_symbol("CollectStats/refresh_len_stats", ...)` — insert new `reset_len_stats` method. +2. `insert_after_symbol("CollectStats/refresh_return_stats", ...)` — insert new `reset_return_stats` method. +3. `replace_symbol_body("CollectStats/refresh_std_array_stats", ...)` — rewrite an existing method body. + +All three calls used the original name paths, unchanged; none required a re-Read between edits. Final `git diff` showed exactly the expected three-edit composite: two insertions plus one body rewrite, adjacent and clean. The first two inserts shifted line numbers between 8 and 16 lines, which would have invalidated any line-number addressing for the third target. + +An equivalent Edit chain would have survived this particular sequence (because the anchors were text, not line numbers), but it exposes the *general* pattern: name_paths are mutation-proof addresses, line numbers are not, and large text anchors become non-unique quickly. + +**Verdict (§3.16):** for any session with three or more edits to the same file, symbolic addressing is a systematic safety and efficiency win. + +### §3.17 Non-code files and free-text searches + +**Tasks 19/20** — state the applicability boundary and move on. Semantic tools don't apply to changelogs, notebooks, configs, or free-text searches for log strings; `Read` and `Grep` are the right tools. + +**Verdict (§3.17):** built-ins only; not a contest. + +--- + +## §4. Token-efficiency analysis + +**Payload asymmetry by edit size** (measured on `collector.py`): + +| Edit size | Edit (old+new) | `replace_symbol_body` | Ratio | +|---|---|---|---| +| 1 line (in 21-line method) | ~120 B | ~1200 B | **Edit 10× smaller** | +| ~20 lines rewritten | ~1430 B | ~715 B | **Serena 2× smaller** | +| ~66 lines whole-body | ~4700 B | ~2050 B | **Serena 2.3× smaller** | + +Crossover is around 10–15 lines. Below it, Edit's per-change payload is dominated by the tiny anchor and wins by an order of magnitude. Above it, Edit pays once for the old body and again for the new, while `replace_symbol_body` pays only for the new body plus a ~30-character name_path; the gap grows linearly with body size. Structural inserts (`insert_after_symbol`) have similar asymmetry — the name_path replaces a multi-line text anchor. + +**Forced reads.** Serena's overview tools return symbol names without bodies and its reference tools return containing-symbol metadata without snippets, so you control when to pull code into context. `find_symbol(include_body=False)` + `find_symbol(include_body=True, name_path=...)` is a two-step "browse, then fetch body" pattern that keeps context lean; the built-in equivalent is `Grep` (which does not return bodies) + `Read` with an offset/limit, which is about as lean but requires the caller to compute the limit by hand. + +**Stable vs ephemeral addressing.** Name paths remain valid across edits to unrelated regions of the same file. Line numbers and byte offsets do not, and text anchors become ambiguous once a file grows. The output-size comparison has to account for *shelf life*: a slightly larger overview that stays useful across an entire session is cheaper than a slightly smaller one that has to be regenerated after each edit. In a five-edit session on one file, Serena's name-path overview is queried once; the line-number grep output is effectively refreshed after each edit that shifts upstream content — an O(edits × file_grep_cost) hidden tax that the one-call comparison misses. + +**Verdict (§4):** under ~10-line edits, built-in `Edit` is cheaper; above that threshold, symbolic body replacement wins on raw payload; across a multi-edit session, name-path addressing wins regardless of size because it doesn't decay. + +--- + +## §5. Reliability and correctness analysis (under correct use) + +**Precision of matching.** `find_referencing_symbols` on `Collector` returns ~70 code files and annotates each with the containing symbol that holds the reference. `Grep \bCollector\b` returns 332 hits across 88 files including notebooks, SVGs, and markdown. For the question "which Python files import and use this class?", Serena's output is directly usable and Grep's needs a filter pass. For the question "where is the name `Collector` mentioned anywhere in the repo, including docs, changelog, and diagrams?", Grep's output is directly usable and Serena's is incomplete by design. Each tool's precision is perfect *for its question*; the mistake is asking the wrong one. + +**Scope disambiguation across overrides.** `find_symbol("_collect")` returned three distinct name paths — `BaseCollector/_collect`, `Collector/_collect`, `AsyncCollector/_collect` — each independently addressable for rename or body replacement. Text search on `_collect(` returns three line locations with no structural annotation; the caller must read context to tell them apart, and any cross-file rename risks touching the wrong override if called carelessly. + +**Atomicity on real failures.** `move` of `get_stddev_from_dist` made coordinated changes across three files in one call. A five-step Edit chain replicating the same move would leave the repo in a half-renamed state if any intermediate step failed on disk-full, a permission error, or an interrupted process; a single-call atomic refactoring either completes or leaves the working tree clean. This matters less for local agent sessions (where the blast radius of a partial refactor is small and recoverable with `git checkout --`) and more for any workflow where a failed run is committed or pushed. + +**Transitive semantic queries.** `type_hierarchy` returned both the sub-chain (`Collector → AsyncCollector`) and the super-chain (`BaseCollector → ABC → object`, with `ABC` resolved into an external `.pyi` stub) in one call. No text-search workflow reaches into stub files or chains override relationships in one step. + +**Success signals (symmetric).** Both toolsets return only mechanical success: "the file was written," "the rename finished." Neither verifies that the new code still compiles, type-checks, or preserves semantics. Post-edit `git diff` review is the caller's responsibility on both sides. + +**Verdict (§5):** Serena's correctness edge is concentrated in questions that are semantic by nature — override chains, transitive type queries, cross-file atomic ops — and tied to Grep/Edit on questions that are textual by nature. + +--- + +## §6. Workflow effects across a multi-step session + +The single biggest session-level effect is **identifier stability across edits**. A session that makes 5 edits to `collector.py` looks like: + +- With symbolic addressing: one `get_symbols_overview` at the start, five `replace_symbol_body`/`insert_after_symbol` calls by name path. The overview is consulted zero or one more times. No re-reads of the file between edits. +- With line-number or large-text-anchor addressing: one `Grep class/def` at the start, one `Read offset+limit` before each edit (or a careful re-grep after any insert that shifts lines), five `Edit`s. The Grep/overview may need to be refreshed mid-session once line numbers drift. + +The hidden cost of the built-in workflow is not in any single call — it's the compounding re-Reads and anchor recomputation across a session. A single Read of a 1500-line file is ~40 kB of context; doing it four extra times across a session is a ~160 kB invisible tax that never shows up in a one-call comparison. + +**Intermediate output survives.** The overview I pulled at the start of this evaluation, the reference list for `Collector`, the type hierarchy for `BaseCollector`, and the signature table for `_collect` all remain valid now that I'm writing the report — I never had to regenerate them. A workflow based on line numbers would have had to regenerate its intermediate output after each of the ~15 edits I applied and reverted during the experiments. + +**Verdict (§6):** session-level efficiency scales with mutation rate; the more edits you plan, the more decisively symbolic addressing wins, and the effect is invisible on any single-call benchmark. + +--- + +## §7. Capabilities with no built-in equivalent + +1. **Cross-file atomic refactorings with automatic import maintenance.** `move` cleaned up an unused import in the source file as a side effect of moving the last user of that import. Built-ins have no equivalent — you'd have to notice. **Value: rare but high — this is the kind of cleanup that bit-rots across a large refactor.** + +2. **Resolution of third-party symbols from a usage site.** `find_declaration` + `find_symbol(search_deps=True)` reach into `site-packages` and `.pyi` stubs for the exact class used at a given line of code. **Value: a few times per session, saves 3–4 calls each.** + +3. **Transitive type hierarchy including external supertypes.** `type_hierarchy` returns sub- and super-chains in one call and crosses module boundaries and stub files. No text-search sequence can reproduce this in O(1). **Value: once or twice per unfamiliar codebase; near zero once you know it.** + +4. **Containing-symbol metadata on reference queries.** `find_referencing_symbols` returns each reference with its enclosing function/class name, making the result a navigation map rather than a line list. **Value: every time you need to understand *how* a symbol is used, not just *where*.** + +5. **Enforced safe-delete with usage-list refusal.** `safe_delete` refuses to remove a symbol that still has usages and returns the usage list. Built-ins cannot refuse — `Edit` applies whatever you send it. **Value: rare but prevents the worst-class mistake.** + +**Verdict (§7):** the capability deltas are real but concentrated in lower-frequency tasks; the one that shows up across *every* editing session is (1.5) — symbolic addressing as a property of the *editing* tools themselves, which I'm treating as the Tier-A efficiency win in §1 rather than a separate capability here. + +--- + +## §8. Where built-ins remain the right default + +- **Small anchored edits (≤ ~10 lines).** Task 7a: Edit's payload is ~10× smaller than `replace_symbol_body` for a one-line change because it sends only the two anchors, not the whole body. Frequency: extremely high — typo fixes, constant changes, error-message tweaks, single-line bug fixes. **Probably 30–50% of daily edits.** + +- **Free-text search for strings, log messages, magic constants, URLs.** Task 20: `Grep` is the only tool that can find a bare string across the repo. Serena's symbolic search expects an identifier, not a phrase. **Frequency: multiple times per session.** + +- **Non-code files** — changelogs, READMEs, YAML configs, notebooks. Task 19: `Read` is the tool. **Frequency: occasional but universal.** + +- **Doc and docstring sweeps after a code-level rename.** Serena's `rename_in_comments=True` catches Sphinx cross-references (verified in Task 10), but if your documentation lives outside of Python docstrings — `.md`, `.rst`, `.ipynb` — a text-based `Grep` sweep is the complementary step, not a Serena failure. **Frequency: every cross-file rename that touches a public API.** + +- **Single-file renames of distinctive identifiers.** Task 9: `Edit replace_all=true` is effectively tied with semantic rename; either works. **Frequency: common.** + +- **Quick one-shot explorations where you don't plan to edit the target.** If the workflow is "look at one function, answer a question, move on," `Read offset/limit` and `Grep` are as fast as semantic tools and don't require any address to be stable beyond the current call. + +These cases are not rare — collectively they probably cover more than half of the calls in a typical session, which is exactly why §1's weighting puts the quiet "addressing stability" win above the spectacular "cross-file refactoring" wins: the former touches every session, the latter touches a handful per week. + +**Verdict (§8):** built-ins are the right default for small edits, free-text search, non-code files, and docstring sweeps — roughly half of daily editing work. + +--- + +## §9. Usage rule for a developer with both toolsets + +Per-task decision rule, in priority order: + +1. **Small edit (≤ ~10 lines), known text anchor** → `Edit`. Payload is ~10× smaller than symbolic body replacement at this size. +2. **Medium or larger body rewrite, or structural insert** → `replace_symbol_body` / `insert_after_symbol`. 2–3× payload cut plus stable addressing for the next edit. +3. **Cross-file rename, move, or delete of a symbol** → `rename` / `move` / `safe_delete`, then a complementary `Grep` sweep of `.md`/`.rst`/`.ipynb` for any text-only references. The semantic tool covers code and Python docstrings in one atomic step; the Grep sweep handles external docs. +4. **Find callers of a class/function** → `find_referencing_symbols`. +5. **Find any mention of a name across the repo including docs** → `Grep`. +6. **Navigate into third-party library source** → `find_declaration` with a regex anchored at the use site, or `find_symbol(search_deps=True)`. +7. **Understand a type hierarchy** → `type_hierarchy`. +8. **Understand an unfamiliar file's structure** → `get_symbols_overview(depth=1)` for classes + methods, then `find_symbol(include_body=True)` for the methods you actually want to read. +9. **Chained edits to one file** → address every edit by `name_path`; do not mix in line-number or large-text-anchor edits unless you plan to re-Read before each one. +10. **Free-text search, non-code files, small single-line tweaks** → built-ins. + +**Verdict (§9):** Edit for small changes, symbolic body replacement for larger ones, semantic multi-file refactoring tools with a Grep doc-sweep on top, Grep/Read for text and non-code; the one habit that matters most is defaulting to name-path addressing whenever you expect more than one edit to the same file. diff --git a/docs/04-evaluation/results/020_codex_on_jbplugin-serena-evaluation.md b/docs/04-evaluation/results/020_codex_on_jbplugin-serena-evaluation.md new file mode 100644 index 00000000..2b091003 --- /dev/null +++ b/docs/04-evaluation/results/020_codex_on_jbplugin-serena-evaluation.md @@ -0,0 +1,136 @@ +# Evaluation by Gpt 5.4 in Codex CLI + +## Final Recommendation + +The recommendation is created with the [follow-up summary prompt](../011_followup-summary-prompt), everything else was created in one shot from the +[evaluation prompt](../010_evaluation-prompt). + +> "As a coding AI agent, I would ask my owner to add Serena because it lets me work with code as stable symbols instead of fragile text, turning the +refactors, lookups, and multi-file edits that usually feel risky and repetitive into precise IDE-backed operations I can trust." + +## 1. Headline + +Serena's largest added value is stable semantic addressing and IDE-backed refactoring. In this Java plugin repo, that meant a method/class could be targeted as `Logger/warning[0]`, `Symbol/safeDelete`, or `ProjectUtil` without line numbers, and cross-file rename/move/inline/delete operations were delegated to IntelliJ's model. + +High value, common: symbol lookup, method-body retrieval, stable name paths, and reference search. Frequency: many times per coding session. Value per hit: usually saves 1-3 reads/searches and avoids loading whole files. + +High value, less frequent but large: cross-file rename, move, safe delete, inline. Frequency: a few times per feature/refactor. Value per hit: saves roughly 5-20 calls and reduces missed import/call-site risk. + +Medium value: type hierarchy and external dependency declaration lookup. Frequency: occasional. Value per hit: turns "search and infer" into 1 semantic query; text search cannot truly reproduce transitive hierarchy or dependency source lookup without IDE/index/cache work. + +Low/no added value: config/docs/free-text search and tiny line edits. Frequency: common, but built-ins are already optimal. Value per hit for Serena: none or negative for small local edits. + +**Verdict:** Serena adds the most value whenever the task is about named code entities rather than text spans; the weighted daily win is stable symbol navigation, while the biggest per-hit win is IDE refactoring. + +## 2. Added Value By Area + +- Stable symbol navigation: showed on `Symbol.java` and `UIControlUtil.java`. Frequency: every non-trivial coding session. Value: saves 1-3 calls per lookup and avoids full-file reads. +- Symbol-scoped edits: `replace_symbol_body`, `insert_after_symbol`, and overload-specific targeting worked on `Logger/warning[0]`, `Logger/warning[1]`, and `Logger/logToToolWindow`. Frequency: several times per session. Value: small edits lose to text Edit, medium edits break even, whole-body edits save about 2x input payload. +- Cross-file refactors: renaming `Logger` to `SerenaLogger` updated 10 files plus the Java file rename; moving `ProjectUtil` into `service.endpoint` updated the package and removed the now-local import. Frequency: occasional. Value: saves roughly 10-20 manual reads/edits/verifications. +- Semantic relationships: `ToolWindowContent` references, `TypeHierarchy` subtypes, `SubtypeHierarchy` supertypes, and `Gson.fromJson` declaration came back as code entities. Frequency: occasional. Value: 1 call versus several searches plus inference. +- Built-in text work remains essential: `build.gradle.kts`, `/findSymbol`, `127.0.0.1`, and `FORM_INIT_DELAY_MILLIS` were naturally handled by `Read`/`rg`. Frequency: large share of daily work. Value: Serena adds nothing there. + +**Verdict:** Serena's contribution is not blanket speed; it removes repeated code-entity bookkeeping from ordinary navigation and almost all bookkeeping from real refactors. + +## 3. Detailed Evidence + +### 3.1 Code Understanding + +Semantic overview of `Symbol.java` returned a class tree with fields, methods, inner classes, and overload indexes in 1 call. Text equivalent was `rg "class |...\\(" Symbol.java`, which returned 100+ signature/comment hits and needed filtering. Follow-up semantic read of `UIControlUtil/findButton` was 1 call returning only the 10-line body; text follow-up needed locating the line then reading a slice. + +Payloads: semantic overview sent path/depth only and returned about 900 tokens; text grep returned about 2,000+ tokens for `Symbol.java`. Semantic method read returned about 90 tokens; text slice returned similar body tokens but required a locator step. + +**Verdict:** Use Serena for source structure and specific method bodies; use text only when the question is literally textual. + +### 3.2 References, Hierarchy, Dependencies + +`find_referencing_symbols` on `ToolWindowContent` returned 4 code uses: two subclass declarations and two parameters. `rg "ToolWindowContent"` returned 5 lines including the definition. For "who uses this in code," Serena was higher precision; for "where is this string mentioned," `rg` was the right tool. + +`type_hierarchy` on `TypeHierarchy` returned `SubtypeHierarchy` and `SupertypeHierarchy`; supertypes for `SubtypeHierarchy` returned `TypeHierarchy` and external `Object`. Text search found name matches plus false positives like `TypeHierarchyRequest`. + +`find_declaration` on `gson.fromJson(requestBody, requestClass)` resolved external `Gson/fromJson[0]` and returned the source body. Built-ins needed finding the Gradle dependency, locating `~/.gradle/.../gson-2.10.1-sources.jar`, listing/extracting `Gson.java`, then searching inside it. + +**Verdict:** Serena adds real semantic reach for "code relationships"; text search can find mentions, but not reliably answer relationship questions in one step. + +### 3.3 Edit Size Economics + +Small edit: rewriting `Logger.logToToolWindow` as an early return. Text patch sent a small old/new anchor, about 9 changed lines. Serena required sending the whole 11-line method body. Text was cheaper. + +Medium edit: rewriting most of `UIControlUtil.tryHandleDialogs`. Text patch sent about 14 old lines plus 20 new lines. Serena sent the full method body, including its comment, about 26 lines. Roughly even. + +Large edit: replacing `Symbol.safeDelete` implementation shape. With content-anchored Edit, a whole-body replacement would send old body plus new body, about 2x the new method payload. `replace_symbol_body` sent only the new body plus `Symbol/safeDelete`. + +**Verdict:** Use text Edit for 1-3 line changes, either tool for 10-30 line method rewrites, and Serena for whole-method replacements. + +### 3.4 Refactors + +Private rename: `Logger/logToToolWindow` to `appendToToolWindow` changed 7 occurrences in 1 semantic call plus optional `rg` verification. Manual path was `rg`, patch each occurrence, then `rg` verify: 3 calls and more payload. + +Multi-file rename: `Logger` to `SerenaLogger` changed 10 files and renamed the Java file. Manual path would be `rg`, read 10 files, rename/move file, edit imports/type names/constructors, verify with `rg`, and likely compile: roughly 14-20 calls. + +Move: moving `ProjectUtil` to `service.endpoint` moved the file, changed its package, and removed the import from `RefreshFileHandler` in 1 call. Manual path would coordinate filesystem move, package line, import removal/additions, and verification. + +Safe delete: `DebugUtil` had no references; safe delete returned `affected_references: []` and deleted it. Manual path is search, delete, verify. Inline: `ProjectUtil/getAbsolutePath` inlined into both call sites in 1 call. + +**Verdict:** Every multi-file or semantic refactor tested was a clear Serena win in call count, payload, and correctness surface. + +### 3.5 Session Effects + +I chained three edits in `Logger.java`: rename helper, replace `Logger/warning[0]`, insert after `Logger/error[0]`. The name paths survived earlier edits; no line recalculation was needed. A built-in line/slice workflow would need refreshed context after insertions because line numbers and nearby anchors shift. + +**Verdict:** Serena's stable identifiers compound across a session; the more edits you make in one file, the wider the gap gets. + +## 4. Token Efficiency + +The crossover is size-dependent. Small edit: text wins because it sends only a tiny anchor. Medium rewrite: near parity. Whole-body rewrite: Serena wins because it sends new body only, while content-anchored Edit sends old body plus new body. + +Forced reads matter: `find_symbol(...include_body=true)` avoided reading 573 lines of `UIControlUtil.java` and 1,295 lines of `Symbol.java`. Stable outputs also have longer shelf life: `Logger/warning[0]` remains useful after unrelated edits; line 52 does not. + +**Verdict:** Token economics favor built-ins for tiny local text edits and Serena for symbol retrieval, chained work, and whole-symbol replacement. + +## 5. Reliability + +Semantic matching distinguished overloads: `Logger/warning[0]` was `warning(String,Object...)`; `Logger/warning[1]` was `warning(String,Throwable)`. `rg "warning\\("` returned both and left disambiguation to the caller. + +Atomicity matters: semantic rename/move run as IDE refactorings, so a failure is not a half-finished sequence of 10 manual edits. Both toolsets still report mechanical success only; semantic intent still needs diff/build review. + +Transitive queries are where text cannot compete directly: hierarchy and external declaration lookup depend on IDE indexes and dependency sources, not just strings. + +**Verdict:** Correctness weight favors Serena for code-entity scope and refactoring atomicity, while text remains correct for text questions. + +## 6. Workflow Effects + +Over a multi-step session, Serena's intermediate artifacts stay usable: symbol trees, name paths, reference lists, and hierarchy nodes survive edits outside those symbols. Text outputs are often ephemeral: line numbers and byte offsets decay immediately after insertions/deletions. + +The practical effect is not just fewer calls; it is fewer re-reads. In the chained `Logger.java` session, Serena needed three edit calls. A text workflow would normally require initial reads/searches, edits, and refreshed context before later insertions. + +**Verdict:** Serena's session-level multiplier comes from not having to rediscover where code moved after each edit. + +## 7. No Built-In Equivalent + +- IDE semantic rename/move/inline/safe-delete: high value when refactoring, moderate frequency. Built-ins can approximate with many edits but cannot provide IDE refactoring semantics or atomicity. +- Type hierarchy including external `Object`: medium value, occasional. Text can search `extends`, but cannot transitively resolve hierarchy with dependency/stub awareness in one call. +- External dependency declaration resolution: medium value, occasional. Built-ins require build-file discovery and local source/cache spelunking. +- Overload/name-path targeting: high value in typed code, frequent enough to matter. Text can match names but cannot address `warning[0]` as a distinct method without manual signature reasoning. + +**Verdict:** Serena's unique capabilities are concentrated in IDE-indexed code intelligence and refactoring operations, not general file manipulation. + +## 8. Built-Ins As Default + +Use built-ins for non-code files, config, docs, and free text. `build.gradle.kts` was best read directly. `rg` was clearly right for `/findSymbol`, `127.0.0.1`, and `FORM_INIT_DELAY_MILLIS`. + +Use built-ins for tiny edits where a short unique anchor is obvious. Also always keep a post-refactor text sweep for comments, markdown, notebooks, generated files, and product strings; that is complementary verification, not a Serena weakness. + +Estimated share: built-ins remain best for maybe 40-60% of daily interactions because much coding work is still file/text/config/search. Serena dominates the code-symbol subset. + +**Verdict:** Start with built-ins for text and config; switch to Serena as soon as the noun in your task is a symbol. + +## 9. Practical Rule + +Reach for Serena when the task says class, method, overload, implementation, reference, hierarchy, rename, move, inline, delete, or "insert after this method." Reach for `rg`/Read/Edit when the task says string, config, docs, log message, URL, generated text, or "change these two lines." + +For edits: text Edit for 1-3 line tweaks; Serena `replace_symbol_body` for full methods/classes; semantic refactor tools for any rename/move/delete/inline that crosses call sites or imports. + +I restored all tracked edits after the experiments. The tracked working tree was clean after evaluation; the only baseline untracked entries were `.claude/` and `serena-evaluation-prompt.md`. I did not run the Gradle test suite because the task was an evaluation, not a product change. + +**Verdict:** With both toolsets installed, use built-ins for text and Serena for code entities; that rule captures almost all of the measured value without overthinking each call. From 3d9dbf33a37a8024fcec1042b4209756ff1f3d97 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 12 Apr 2026 18:11:43 +0200 Subject: [PATCH 133/190] Improve news item to also mention client config update [skip ci] --- news/20260411.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/news/20260411.html b/news/20260411.html index ce07de08..c0da1719 100644 --- a/news/20260411.html +++ b/news/20260411.html @@ -2,10 +2,12 @@

    Serena v1.1: Hooks and New Release Mechanism

    April 11, 2026

    - Installation. Serena has matured and we are switching to a PyPI-based release and installation mechanism. - The recommended uv-tool-based installation decreases startup times and facilitates CLI usage. - See updated installation instructions - for more details. + Installation. Serena has matured, and we are switching to a PyPI-based release and installation mechanism. + The recommended uv tool-based installation decreases startup times and facilitates CLI usage. + See our updated installation instructions + for more details, + and update your client configurations after having switched + to the new installation method.

    Improving Tool Usage via Hooks. To address issues with agents not using Serena's tools (agent drift), we now provide the `serena-hooks` commands and a recommended @@ -14,9 +16,7 @@ We strongly recommend using the hooks, since recent updates in Claude Code and VSCode resulted in reduced performance, which our hook commands compensate for.

    -

    Community. We opened a Discord server for our community.

    - \ No newline at end of file From 1fb4ab1738e395bb51ebfca7a00479045546d9af Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 12 Apr 2026 18:21:08 +0200 Subject: [PATCH 134/190] Add information on uninstallation --- docs/02-usage/010_installation.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/02-usage/010_installation.md b/docs/02-usage/010_installation.md index 8a3dd922..5b0f216b 100644 --- a/docs/02-usage/010_installation.md +++ b/docs/02-usage/010_installation.md @@ -43,3 +43,9 @@ To update Serena to the latest version, run: To keep informed about updates, make sure you regularly open [Serena's Dashboard](060_dashboard), where we will announce releases along with the new features and improvements they bring. ::: + +## Uninstalling Serena + +Serena can be uninstalled with the following command: + + uv tool uninstall serena-agent From 678f195931a7c0d56e4d52e4ac69ff5074656ae0 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 12 Apr 2026 18:21:46 +0200 Subject: [PATCH 135/190] Ignore .claude --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 73c25571..dbc94920 100644 --- a/.gitignore +++ b/.gitignore @@ -216,7 +216,7 @@ tmp/ .vscode/ # Claude settings -.claude/settings.local.json +/.claude # Elixir /test/resources/repos/elixir/test_repo/deps From e7b51173bccbdf52a747f3002203bfecd402643e Mon Sep 17 00:00:00 2001 From: TheSmallKiwi Date: Sat, 11 Apr 2026 19:57:25 -0400 Subject: [PATCH 136/190] HLSL: bump to shader-language-server v1.3.1, remove CRLF workaround The upstream fix for the CRLF stale-tree crash (antaalt/shader-sense#44) was merged and released as v1.3.1. This commit: - Removes the client-side CRLF workaround (open_file override and _preload_crlf_content helper) added in #1305, along with the now-unused imports. - Bumps _DEFAULT_VERSION to 1.3.1 and updates SHA-256 checksums for all three pre-built platform archives (win-x64, linux-x64, win-arm64). The regression test (test_include_file_document_symbols_directly) that exercises the original crash scenario passes against v1.3.1 without the workaround. Co-Authored-By: Claude Sonnet 4.6 --- docs/02-usage/050_configuration.md | 2 +- .../language_servers/hlsl_language_server.py | 99 +------------------ 2 files changed, 5 insertions(+), 96 deletions(-) diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index fe2b0d9e..060f0249 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -472,7 +472,7 @@ Supported settings: | Setting | Default | Description | |---|---|---| | `ls_path` | managed install or build | Override the `shader-language-server` executable path. | -| `version` | `1.3.0` | Override the bundled version Serena downloads, or builds from source on macOS, when `ls_path` is not set. | +| `version` | `1.3.1` | Override the bundled version Serena downloads, or builds from source on macOS, when `ls_path` is not set. | #### Haxe diff --git a/src/solidlsp/language_servers/hlsl_language_server.py b/src/solidlsp/language_servers/hlsl_language_server.py index 2857055b..b6eb7e05 100644 --- a/src/solidlsp/language_servers/hlsl_language_server.py +++ b/src/solidlsp/language_servers/hlsl_language_server.py @@ -7,9 +7,6 @@ import logging import os import pathlib import shutil -from collections.abc import Iterator -from contextlib import contextmanager -from pathlib import Path from typing import Any, cast import psutil @@ -18,11 +15,9 @@ from overrides import override from solidlsp.ls import ( LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, - LSPFileBuffer, SolidLanguageServer, ) from solidlsp.ls_config import LanguageServerConfig -from solidlsp.ls_exceptions import SolidLSPException from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.settings import SolidLSPSettings @@ -31,13 +26,13 @@ from .common import RuntimeDependency, RuntimeDependencyCollection log = logging.getLogger(__name__) # GitHub release version to download when not installed locally -_DEFAULT_VERSION = "1.3.0" +_DEFAULT_VERSION = "1.3.1" _GITHUB_RELEASE_BASE = "https://github.com/antaalt/shader-sense/releases/download" _HLSL_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") _HLSL_SHA256_BY_ASSET = { - "shader-language-server-x86_64-pc-windows-msvc.zip": "a945b000c296cdeebb9ee2d4452cec2a0f26544dd076bb08bfdcade2278296a6", - "shader-language-server-x86_64-unknown-linux-gnu.zip": "8c0a7b36f51cc58593762db3592ae13e21ca3cb982b2526cfaaf7c82e92ca089", - "shader-language-server-aarch64-pc-windows-msvc.zip": "cdbd7b41e71cf6040d5cdb7e211ba4b76671a404ee0f7add281d72d3ab8dfa65", + "shader-language-server-x86_64-pc-windows-msvc.zip": "49081c5547ddde1b8b3b17295282a80ddacbca1d6f5dcd834e2788c02bafa997", + "shader-language-server-x86_64-unknown-linux-gnu.zip": "61710df7ca17a2d063b598936c57c56c49fbf837707a1aa886f9b0193a35be3c", + "shader-language-server-aarch64-pc-windows-msvc.zip": "a3b3799affe2cad27652e788376b46fe76e1a6c2ce45946a486dcb26c9091412", } @@ -277,92 +272,6 @@ class HlslLanguageServer(SolidLanguageServer): log.debug(f"Error cleaning up shader-language-server process tree: {e}") super().stop(shutdown_timeout) - @contextmanager - def open_file(self, relative_file_path: str, open_in_ls: bool = True) -> Iterator[LSPFileBuffer]: - """Open a file for LSP, preserving on-disk CRLF line endings. - - Workaround for an upstream bug in shader-language-server - (antaalt/shader-sense) where `watch_main_file` replaces an already-cached - module's content without re-parsing the tree-sitter tree. When a file is - first pulled into the server's cache via an `#include` from another - shader (where it's read via `std::fs::read_to_string`, preserving CRLF), - and then later opened directly via `textDocument/didOpen` with the - client-normalized LF text, the stored tree still references byte offsets - into the longer CRLF content. The next symbol query slices the new - (shorter) content with stale offsets and panics with - `byte index N is out of bounds` in `shader-sense/src/symbols/symbol_parser.rs`. - - The root-cause fix belongs upstream (the server should call - `update_module` instead of assigning `content` raw). Until then, we - ensure the text we send in `didOpen` matches byte-for-byte what the - server reads from disk by preloading the file buffer with a - CRLF-preserving read before the LSP notification is sent. - - This is the only place in Serena that overrides `open_file`; the fix is - deliberately scoped to the HLSL language server. It mirrors the base - class logic in `SolidLanguageServer.open_file` verbatim except for the - buffer construction branch, where creation is deferred (`open_in_ls=False`) - so the buffer's contents can be preloaded before `ensure_open_in_ls` runs. - """ - if not self.server_started: - log.error("open_file called before Language Server started") - raise SolidLSPException("Language Server not started") - - absolute_file_path = Path(self.repository_root_path, relative_file_path) - uri = absolute_file_path.as_uri() - - if uri in self.open_file_buffers: - fb = self.open_file_buffers[uri] - assert fb.uri == uri - assert fb.ref_count >= 1 - - fb.ref_count += 1 - if open_in_ls: - fb.ensure_open_in_ls() - yield fb - fb.ref_count -= 1 - else: - version = 0 - language_id = self._get_language_id_for_file(relative_file_path) - # Defer the didOpen so we can preload CRLF-preserved content first. - fb = LSPFileBuffer( - abs_path=absolute_file_path, - uri=uri, - encoding=self._encoding, - version=version, - language_id=language_id, - ref_count=1, - language_server=self, - open_in_ls=False, - ) - self._preload_crlf_content(fb) - self.open_file_buffers[uri] = fb - if open_in_ls: - fb.ensure_open_in_ls() - yield fb - fb.ref_count -= 1 - - if self.open_file_buffers[uri].ref_count == 0: - self.open_file_buffers[uri].close() - del self.open_file_buffers[uri] - - def _preload_crlf_content(self, fb: LSPFileBuffer) -> None: - """Populate an LSPFileBuffer with a CRLF-preserving read of its backing file. - - Python's default text-mode open applies universal-newlines translation - (CRLF -> LF), which would desync the client's `didOpen` text from the - server-side `std::fs::read_to_string` view that parsed the dependency - tree. Passing `newline=""` disables the translation so bytes match. - """ - with open(fb.abs_path, encoding=fb.encoding, newline="") as f: - raw = f.read() - # Set the buffer's cached state directly: the contents, the mtime - # (required by the contents property's staleness check), and clear the - # hash so it's recomputed against the new bytes. - fb._contents = raw - fb._read_file_modified_date = fb.abs_path.stat().st_mtime - fb._content_hash = None - @override def is_ignored_dirname(self, dirname: str) -> bool: """Ignore Unity-specific directories that contain no user-authored shaders.""" From fc392a4fb85ad746db635565401cd241e33ecd36 Mon Sep 17 00:00:00 2001 From: nameearly <2741313455@qq.com> Date: Mon, 13 Apr 2026 05:36:11 +0800 Subject: [PATCH 137/190] fixing minor scattered problems (#1324) --- src/serena/agent.py | 2 +- src/serena/task_executor.py | 2 +- src/serena/tools/tools_base.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 99f14bd1..322db7c8 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -719,7 +719,7 @@ class SerenaAgent: if self._language_backend == LanguageBackend.LSP: languages_str = ", ".join([lang.value for lang in proj.project_config.languages]) msg += f"\nProgramming languages: {languages_str}." - msg += "File encoding: {proj.project_config.encoding}." + msg += f"File encoding: {proj.project_config.encoding}." include_memories = self._active_tools.contains_tool_class(ReadMemoryTool) if include_memories: diff --git a/src/serena/task_executor.py b/src/serena/task_executor.py index 256fbf02..5e624296 100644 --- a/src/serena/task_executor.py +++ b/src/serena/task_executor.py @@ -80,7 +80,7 @@ class TaskExecutor: :param timeout: the maximum time to wait in seconds; if None, use the task's own timeout (which may be None to wait indefinitely) - :return: True if the task is done, False if the timeout was reached + :return: the result of the task """ return self.future.result(timeout=timeout) diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index bb474349..18badd67 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -430,7 +430,6 @@ tool_packages = ["serena.tools"] @singleton class ToolRegistry: _deleted_tools: list[str] = [ - "think_about_collected_information", "think_about_collected_information", "prepare_for_new_conversation", "summarize_changes", From ab67ecab3b64f4fd91f4c92c73281c8babe87b87 Mon Sep 17 00:00:00 2001 From: Pirates IRC <98669745+PiratesIRC@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:47:51 -0500 Subject: [PATCH 138/190] feat: add mSL (mIRC Scripting Language) support (#1315) * feat: add mSL (mIRC Scripting Language) support Add language server support for mIRC Scripting Language (.mrc files). mSL is used in mIRC and AdiIRC IRC clients for scripting bots, games, and automation. The implementation uses a custom Python-based LSP server (pygls) that parses aliases, events, menus, dialogs, and CTCP handlers. Dependencies (pygls, lsprotocol) are installed in an isolated venv on first use. Includes test repo, integration tests, and documentation updates. * style: apply ruff formatting to msl_language_server.py * fix: remove INITIALIZE handler that crashes pygls 2.x pygls 2.x handles the initialize request internally. Overriding it via @server.feature(lsp.INITIALIZE) causes the LSP subprocess to crash. Removing the handler lets pygls auto-advertise capabilities based on registered features (document_symbol, workspace_symbol). * fix: update embedded LSP script for pygls 2.x pygls 2.x moved LanguageServer from pygls.server to pygls.lsp.server. Update import and bump requirement from pygls>=1.3.0 to pygls>=2.0.0. * refactor: ship mSL LSP as package module, not runtime disk write Per maintainer feedback: the mSL LSP is a Python script, so it should ship as a module inside the package rather than being written to disk at runtime. Changes: - Extract embedded LSP script to msl_lsp_server.py (proper module) - Simplify MslLanguageServer: remove venv creation, disk writes, _create_msl_lsp_files(). DependencyProvider now just returns sys.executable and launches the sibling module directly. - Add pygls>=2.0.0 and lsprotocol>=2023.0.0 to pyproject.toml deps * fix: regenerate uv.lock after upstream v1.1.0 merge * feat(msl): add references, definitions, and expand test coverage - Add textDocument/references handler for cross-file alias reference finding - Add textDocument/definition handler for go-to-definition support - Handle $ prefix on cursor position for mSL identifiers - Fix pre-existing mypy error in agent.py (dict[str, object] -> dict[str, str | int]) - Add MSL to _LANGUAGE_PYTEST_MARKERS in conftest.py - Add MSL parametrized cases to test_find_symbol_stable and test_find_symbol_references_stable - Expand test repo with cross-file reference examples (main.mrc <-> utils.mrc) - Add within-file and cross-file reference tests to test_msl_basic.py - Update docs and CHANGELOG to reflect expanded capabilities Co-Authored-By: Claude Opus 4.6 (1M context) * style: apply ruff formatting to msl_lsp_server.py Co-Authored-By: Claude Opus 4.6 (1M context) * fix(msl): add hover handler, fix cross-file references via filesystem scan Root causes of 4 CI test failures: - textDocument/hover was not implemented (test_find_symbol_stable needs it) - references/workspace_symbol/definition only searched opened documents, missing files not explicitly opened by the client - pygls.uris.to_fs_path returns lowercase drive letters on Windows, causing URI mismatch with the framework's repository path comparison Fixes: - Add textDocument/hover handler returning definition snippets in Markdown - Add _get_all_mrc_files() to scan workspace filesystem for all .mrc files - Read workspace roots from server.workspace (pygls 2.x API) instead of broken monkey-patching of server.lsp - Use pathlib.Path.resolve() to normalize drive letter casing on Windows - Simplify __main__ to just server.start_io() All 12 tests pass locally (10 solidlsp + 2 serena_agent integration). Co-Authored-By: Claude Opus 4.6 (1M context) * MSL language: removed unnecessary Dep. provider * Changelog * deps --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Michael Panchenko --- CHANGELOG.md | 4 + README.md | 2 +- docs/01-about/020_programming-languages.md | 3 + pyproject.toml | 3 + .../language_servers/msl_language_server.py | 107 +++++ .../language_servers/msl_lsp_server.py | 439 ++++++++++++++++++ src/solidlsp/ls_config.py | 12 + test/conftest.py | 1 + test/resources/repos/msl/test_repo/main.mrc | 38 ++ test/resources/repos/msl/test_repo/utils.mrc | 26 ++ test/serena/test_serena_agent.py | 3 + test/solidlsp/msl/__init__.py | 0 test/solidlsp/msl/test_msl_basic.py | 161 +++++++ uv.lock | 44 ++ 14 files changed, 842 insertions(+), 1 deletion(-) create mode 100644 src/solidlsp/language_servers/msl_language_server.py create mode 100644 src/solidlsp/language_servers/msl_lsp_server.py create mode 100644 test/resources/repos/msl/test_repo/main.mrc create mode 100644 test/resources/repos/msl/test_repo/utils.mrc create mode 100644 test/solidlsp/msl/__init__.py create mode 100644 test/solidlsp/msl/test_msl_basic.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 12eb66de..35f4a3d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ Status of the `main` branch. Changes prior to the next official version change w * General: - Support environment variable `SERENA_USAGE_REPORTING` (set to `false` to disable usage reporting) +* Language Servers: + - Add mSL (mIRC Scripting Language) support (custom pygls-based language server; symbols, references, definitions) + # 1.1.1 * General: @@ -16,6 +19,7 @@ Status of the `main` branch. Changes prior to the next official version change w * Language Servers: - Fix Dart LSP returning only symbol name as body instead of full method body. + # 1.1.0 * General: diff --git a/README.md b/README.md index 5f07841b..f1b513d1 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Serena incorporates a powerful abstraction layer for the integration of language The underlying language servers are typically open-source projects or at least freely available for use. When using Serena's language server backend, we provide **support for over 40 programming languages**, including -AL, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, Haxe, HLSL, Java, JavaScript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. +AL, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, Haxe, HLSL, Java, JavaScript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. ### The Serena JetBrains Plugin diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index 33fcd54d..999782ee 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -84,6 +84,9 @@ Some languages require additional installations or setup steps, as noted. * **Luau** * **Markdown** (must explicitly enable language `markdown`, primarily useful for documentation-heavy projects) +* **mSL** (mIRC Scripting Language) + (auto-installed; no external dependencies required — uses a custom pygls-based LSP server shipped with Serena; + supports document symbols, workspace symbols, references, and go-to-definition for aliases, events, menus, dialogs, and CTCP handlers in `.mrc` files) * **Nix** (requires nixd installation) * **OCaml** diff --git a/pyproject.toml b/pyproject.toml index c6daf2c9..8894c98f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,8 @@ dependencies = [ "beautifulsoup4==4.14.2", "oraios-pywebview==6.2", "pystray==0.19.5", + "pygls==2.1.1", # used to implement an msl language server + "lsprotocol==2025.0.0", # Transitive deps pinned for security (dependabot alerts). # Exact pins because uvx installs from git, ignoring the lock file. "urllib3==2.6.3", @@ -344,6 +346,7 @@ markers = [ "lean4: language server running for Lean 4", "solidity: language server running for Solidity (uses @nomicfoundation/solidity-language-server)", "ansible: language server running for Ansible (uses @ansible/ansible-language-server)", + "msl: language server running for mSL (mIRC Scripting Language)", ] [tool.codespell] diff --git a/src/solidlsp/language_servers/msl_language_server.py b/src/solidlsp/language_servers/msl_language_server.py new file mode 100644 index 00000000..e2596a05 --- /dev/null +++ b/src/solidlsp/language_servers/msl_language_server.py @@ -0,0 +1,107 @@ +""" +Provides mSL (mIRC Scripting Language) specific instantiation of the LanguageServer class. +Uses a custom Python-based LSP server (pygls) for parsing .mrc files. + +The LSP server script is shipped as ``msl_lsp_server.py`` alongside this module +and launched as a subprocess using the current Python interpreter. +""" + +import logging +import os +import pathlib +import sys +import threading + +from solidlsp.ls import ( + SolidLanguageServer, +) +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings + +log = logging.getLogger(__name__) + +_MSL_LSP_SCRIPT = os.path.join(os.path.dirname(__file__), "msl_lsp_server.py") + + +class MslLanguageServer(SolidLanguageServer): + """ + Provides mSL (mIRC Scripting Language) specific instantiation of the LanguageServer class. + Uses a Python-based LSP server for parsing .mrc files (aliases, events, menus, dialogs). + """ + + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): + """ + Creates an MslLanguageServer instance. This class is not meant to be instantiated directly. + Use LanguageServer.create() instead. + """ + process_launch_info = ProcessLaunchInfo(cmd=[sys.executable, _MSL_LSP_SCRIPT], cwd=repository_root_path) + super().__init__( + config, + repository_root_path, + process_launch_info=process_launch_info, + language_id="msl", + solidlsp_settings=solidlsp_settings, + ) + self.server_ready = threading.Event() + + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + """Returns the initialize params for the mSL Language Server.""" + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "locale": "en", + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "references": {"dynamicRegistration": True}, + "definition": {"dynamicRegistration": True}, + }, + "workspace": { + "workspaceFolders": True, + "symbol": {"dynamicRegistration": True}, + }, + }, + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}], + } + return initialize_params # type: ignore + + def _start_server(self) -> None: + """Starts the mSL Language Server.""" + + def window_log_message(msg: dict) -> None: + log.info(f"LSP: window/logMessage: {msg}") + self.server_ready.set() + + def do_nothing(params: dict) -> None: + pass + + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + + log.info("Starting mSL server process") + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + log.info("Sending initialize request to mSL LSP server") + init_response = self.server.send.initialize(initialize_params) + log.debug(f"Received initialize response: {init_response}") + + self.server.notify.initialized({}) + + # Wait briefly for server readiness + if not self.server_ready.wait(timeout=2.0): + log.info("Timeout waiting for mSL server ready signal, proceeding anyway") + self.server_ready.set() + + log.info("mSL server initialization complete") diff --git a/src/solidlsp/language_servers/msl_lsp_server.py b/src/solidlsp/language_servers/msl_lsp_server.py new file mode 100644 index 00000000..e87d3d16 --- /dev/null +++ b/src/solidlsp/language_servers/msl_lsp_server.py @@ -0,0 +1,439 @@ +"""mSL (mIRC Scripting Language) Language Server. + +A minimal LSP implementation for mIRC scripting language (.mrc files). +Provides document symbols, hover, references, go-to-definition, and workspace symbols +for aliases, events, menus, dialogs, and CTCP handlers. + +Launched as a subprocess by MslLanguageServer. Communicates via stdio. +""" + +import logging +import os +import pathlib +import re + +from lsprotocol import types as lsp +from pygls.lsp.server import LanguageServer + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +server = LanguageServer("msl-lsp", "0.1.0") + +# mSL top-level construct patterns +ALIAS_PATTERN = re.compile( + r"^[ \t]*alias\s+(?:-l\s+)?([a-zA-Z_][\w.]*)\s*(?:\{|$)", + re.MULTILINE | re.IGNORECASE, +) +EVENT_PATTERN = re.compile( + r"^[ \t]*on\s+(\*|\d+):(\w+):([^{]*?)(?:\{|$)", + re.MULTILINE | re.IGNORECASE, +) +RAW_EVENT_PATTERN = re.compile( + r"^[ \t]*raw\s+(\d+):([^{]*?)(?:\{|$)", + re.MULTILINE | re.IGNORECASE, +) +MENU_PATTERN = re.compile( + r"^[ \t]*menu\s+([^\s{]+)\s*\{", + re.MULTILINE | re.IGNORECASE, +) +DIALOG_PATTERN = re.compile( + r"^[ \t]*dialog\s+(-l\s+)?([a-zA-Z_][\w]*)\s*\{", + re.MULTILINE | re.IGNORECASE, +) +CTCP_PATTERN = re.compile( + r"^[ \t]*ctcp\s+(\*|\d+):(\w+):([^{]*?)(?:\{|$)", + re.MULTILINE | re.IGNORECASE, +) + + +def _get_line_col(text: str, pos: int) -> tuple[int, int]: + lines = text[:pos].split("\n") + return len(lines) - 1, len(lines[-1]) if lines else 0 + + +def _find_block_end(text: str, start: int) -> int: + count, i = 0, start + while i < len(text): + ch = text[i] + if ch == ";" and count > 0: + while i < len(text) and text[i] != "\n": + i += 1 + continue + if ch == "{": + count += 1 + elif ch == "}": + count -= 1 + if count == 0: + return i + i += 1 + return len(text) - 1 + + +def _make_symbol( + name: str, + kind: lsp.SymbolKind, + detail: str, + text: str, + match_start: int, + match_end: int, + match_text: str, +) -> lsp.DocumentSymbol: + sl, sc = _get_line_col(text, match_start) + bs = text.find("{", match_start) + if bs != -1: + el, ec = _get_line_col(text, _find_block_end(text, bs)) + else: + el, ec = _get_line_col(text, match_end) + return lsp.DocumentSymbol( + name=name, + kind=kind, + range=lsp.Range(lsp.Position(sl, 0), lsp.Position(el, ec + 1)), + selection_range=lsp.Range(lsp.Position(sl, sc), lsp.Position(sl, sc + len(match_text))), + detail=detail, + ) + + +def parse_symbols(text: str) -> list[lsp.DocumentSymbol]: + """Parse mSL source code and return document symbols.""" + symbols: list[lsp.DocumentSymbol] = [] + + for m in ALIAS_PATTERN.finditer(text): + symbols.append(_make_symbol(m.group(1), lsp.SymbolKind.Function, "alias", text, m.start(), m.end(), m.group(0))) + + for m in EVENT_PATTERN.finditer(text): + pat = m.group(3).strip().rstrip(":") + name = f"on {m.group(1)}:{m.group(2)}" + (f":{pat}" if pat else "") + symbols.append(_make_symbol(name, lsp.SymbolKind.Event, f"event:{m.group(2)}", text, m.start(), m.end(), m.group(0))) + + for m in RAW_EVENT_PATTERN.finditer(text): + pat = m.group(2).strip().rstrip(":") + name = f"raw {m.group(1)}" + (f":{pat}" if pat else "") + symbols.append(_make_symbol(name, lsp.SymbolKind.Event, "raw event", text, m.start(), m.end(), m.group(0))) + + for m in MENU_PATTERN.finditer(text): + symbols.append(_make_symbol(f"menu {m.group(1)}", lsp.SymbolKind.Module, "menu", text, m.start(), m.end(), m.group(0))) + + for m in DIALOG_PATTERN.finditer(text): + symbols.append(_make_symbol(f"dialog {m.group(2)}", lsp.SymbolKind.Class, "dialog", text, m.start(), m.end(), m.group(0))) + + for m in CTCP_PATTERN.finditer(text): + pat = m.group(3).strip().rstrip(":") + name = f"ctcp {m.group(1)}:{m.group(2)}" + (f":{pat}" if pat else "") + symbols.append(_make_symbol(name, lsp.SymbolKind.Event, "ctcp event", text, m.start(), m.end(), m.group(0))) + + symbols.sort(key=lambda s: s.range.start.line) + return symbols + + +def _get_workspace_roots() -> list[str]: + """Get workspace root paths from the server's workspace object. + + After initialization, pygls populates ``server.workspace`` with the + root URI/path and any workspace folders sent by the client. + """ + from pygls.uris import to_fs_path + + roots: list[str] = [] + try: + ws = server.workspace + except (RuntimeError, AttributeError): + return roots + + # workspace.folders is a dict of {uri_string: WorkspaceFolder} + if hasattr(ws, "folders") and ws.folders: + for folder_uri in ws.folders: + fs_path = to_fs_path(folder_uri) + if fs_path: + # resolve() normalizes drive letter casing on Windows + fs_path = str(pathlib.Path(fs_path).resolve()) + if os.path.isdir(fs_path): + roots.append(fs_path) + + # Fall back to root_path + if not roots and hasattr(ws, "root_path") and ws.root_path: + root = str(pathlib.Path(ws.root_path).resolve()) + if os.path.isdir(root): + roots.append(root) + + return roots + + +def _path_to_uri(path: str) -> str: + """Convert a filesystem path to a file URI.""" + return pathlib.Path(path).as_uri() + + +def _get_all_mrc_files() -> list[tuple[str, str, str]]: + """Scan workspace roots for all .mrc files. + + Returns list of (uri, file_path, source_text) tuples. + Prefers content from already-opened documents over disk reads. + """ + results: list[tuple[str, str, str]] = [] + seen_paths: set[str] = set() + + # First, include all files currently open in the workspace + try: + for uri, doc in server.workspace.text_documents.items(): + if uri.endswith(".mrc"): + # Resolve the filesystem path from the URI + from pygls.uris import to_fs_path + + file_path = to_fs_path(uri) or uri + norm = os.path.normcase(os.path.normpath(file_path)) + seen_paths.add(norm) + results.append((uri, file_path, doc.source)) + except (RuntimeError, AttributeError): + pass + + # Then scan workspace roots for any .mrc files not yet opened + for root in _get_workspace_roots(): + for dirpath, _dirnames, filenames in os.walk(root): + for fname in filenames: + if not fname.endswith(".mrc"): + continue + full_path = os.path.join(dirpath, fname) + norm = os.path.normcase(os.path.normpath(full_path)) + if norm in seen_paths: + continue + seen_paths.add(norm) + try: + with open(full_path, encoding="utf-8", errors="replace") as f: + source = f.read() + uri = _path_to_uri(full_path) + results.append((uri, full_path, source)) + except OSError: + pass + + return results + + +def _find_symbol_at_position(text: str, line: int, character: int) -> str | None: + """Find the symbol name at the given position in the text.""" + lines = text.split("\n") + if line >= len(lines): + return None + line_text = lines[line] + + # Check if position is within an alias definition + for m in ALIAS_PATTERN.finditer(text): + sl, _ = _get_line_col(text, m.start()) + if sl == line: + return m.group(1) + + # Check if position is within an event definition + for m in EVENT_PATTERN.finditer(text): + sl, _ = _get_line_col(text, m.start()) + if sl == line: + pat = m.group(3).strip().rstrip(":") + return f"on {m.group(1)}:{m.group(2)}" + (f":{pat}" if pat else "") + + # Check if position is within a raw event + for m in RAW_EVENT_PATTERN.finditer(text): + sl, _ = _get_line_col(text, m.start()) + if sl == line: + pat = m.group(2).strip().rstrip(":") + return f"raw {m.group(1)}" + (f":{pat}" if pat else "") + + # Check if position is within a menu + for m in MENU_PATTERN.finditer(text): + sl, _ = _get_line_col(text, m.start()) + if sl == line: + return f"menu {m.group(1)}" + + # Check if position is within a dialog + for m in DIALOG_PATTERN.finditer(text): + sl, _ = _get_line_col(text, m.start()) + if sl == line: + return f"dialog {m.group(2)}" + + # Check if position is within a CTCP handler + for m in CTCP_PATTERN.finditer(text): + sl, _ = _get_line_col(text, m.start()) + if sl == line: + return f"ctcp {m.group(1)}:{m.group(2)}" + + # Fall back: extract word at position (for alias call sites) + if character < len(line_text): + pos = character + # Skip leading $ if cursor is on it (e.g., $format.coins) + if pos < len(line_text) and line_text[pos] == "$": + pos += 1 + start = pos + while start > 0 and (line_text[start - 1].isalnum() or line_text[start - 1] in "_."): + start -= 1 + end = pos + while end < len(line_text) and (line_text[end].isalnum() or line_text[end] in "_."): + end += 1 + word = line_text[start:end] + if word: + return word + return None + + +def _get_symbol_detail(text: str, symbol_name: str) -> str | None: + """Get detail/documentation text for a symbol by finding its definition and extracting context.""" + for sym in parse_symbols(text): + if sym.name == symbol_name: + # Extract the definition line and a few lines of body for hover + text_lines = text.split("\n") + start_line = sym.range.start.line + end_line = min(sym.range.end.line, start_line + 5) + snippet_lines = text_lines[start_line : end_line + 1] + snippet = "\n".join(snippet_lines) + return f"```msl\n{snippet}\n```\n\n**Kind**: {sym.detail}" + return None + + +def _build_call_pattern(alias_name: str) -> re.Pattern[str]: + """Build a regex that matches calls to an alias (both as command and as $identifier).""" + escaped = re.escape(alias_name) + return re.compile( + rf"(? None: + pass + + +@server.feature(lsp.TEXT_DOCUMENT_DID_CHANGE) +def did_change(params: lsp.DidChangeTextDocumentParams) -> None: + pass + + +@server.feature(lsp.TEXT_DOCUMENT_DID_CLOSE) +def did_close(params: lsp.DidCloseTextDocumentParams) -> None: + pass + + +@server.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL) +def document_symbol(params: lsp.DocumentSymbolParams) -> list[lsp.DocumentSymbol]: + """Return document symbols for the given document.""" + try: + doc = server.workspace.get_text_document(params.text_document.uri) + return parse_symbols(doc.source) + except Exception as e: + logger.error(f"Error: {e}") + return [] + + +@server.feature(lsp.TEXT_DOCUMENT_HOVER) +def hover(params: lsp.HoverParams) -> lsp.Hover | None: + """Return hover information for the symbol at the given position.""" + try: + doc = server.workspace.get_text_document(params.text_document.uri) + symbol_name = _find_symbol_at_position(doc.source, params.position.line, params.position.character) + if not symbol_name: + return None + + # Search all files for the symbol definition to get its detail + for _uri, _path, source in _get_all_mrc_files(): + detail = _get_symbol_detail(source, symbol_name) + if detail: + return lsp.Hover( + contents=lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=detail), + ) + + # Fallback: return the symbol name itself + return lsp.Hover( + contents=lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=f"**{symbol_name}**"), + ) + except Exception as e: + logger.error(f"Error in hover: {e}") + return None + + +@server.feature(lsp.TEXT_DOCUMENT_REFERENCES) +def references(params: lsp.ReferenceParams) -> list[lsp.Location]: + """Find all references to the symbol at the given position across the workspace.""" + try: + doc = server.workspace.get_text_document(params.text_document.uri) + symbol_name = _find_symbol_at_position( + doc.source, + params.position.line, + params.position.character, + ) + if not symbol_name: + return [] + + results: list[lsp.Location] = [] + + # For alias symbols, search for call sites across all .mrc files + # For events/menus/dialogs, only return the definition location + is_alias = not any(symbol_name.startswith(prefix) for prefix in ("on ", "raw ", "menu ", "dialog ", "ctcp ")) + + for uri, _path, source in _get_all_mrc_files(): + if is_alias: + call_pattern = _build_call_pattern(symbol_name) + for m in call_pattern.finditer(source): + ref_line, ref_col = _get_line_col(source, m.start()) + results.append( + lsp.Location( + uri=uri, + range=lsp.Range( + lsp.Position(ref_line, ref_col), + lsp.Position(ref_line, ref_col + len(m.group(0))), + ), + ) + ) + else: + # For non-alias symbols, find the definition + for sym in parse_symbols(source): + if sym.name == symbol_name: + results.append(lsp.Location(uri=uri, range=sym.range)) + + return results + except Exception as e: + logger.error(f"Error finding references: {e}") + return [] + + +@server.feature(lsp.TEXT_DOCUMENT_DEFINITION) +def definition(params: lsp.DefinitionParams) -> list[lsp.Location]: + """Go to definition of the symbol at the given position.""" + try: + doc = server.workspace.get_text_document(params.text_document.uri) + symbol_name = _find_symbol_at_position( + doc.source, + params.position.line, + params.position.character, + ) + if not symbol_name: + return [] + + results: list[lsp.Location] = [] + for uri, _path, source in _get_all_mrc_files(): + for sym in parse_symbols(source): + if sym.name == symbol_name: + results.append(lsp.Location(uri=uri, range=sym.selection_range)) + return results + except Exception as e: + logger.error(f"Error finding definition: {e}") + return [] + + +@server.feature(lsp.WORKSPACE_SYMBOL) +def workspace_symbol(params: lsp.WorkspaceSymbolParams) -> list[lsp.SymbolInformation]: + """Search for symbols across the workspace.""" + query = params.query.lower() + results = [] + for uri, _path, source in _get_all_mrc_files(): + for sym in parse_symbols(source): + if query in sym.name.lower(): + results.append( + lsp.SymbolInformation( + name=sym.name, + kind=sym.kind, + location=lsp.Location(uri=uri, range=sym.range), + container_name=sym.detail, + ) + ) + return results + + +if __name__ == "__main__": + server.start_io() diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index ab1a4038..ff137894 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -88,6 +88,12 @@ class Language(str, Enum): Requires MATLAB R2021b or later and Node.js. Set MATLAB_PATH environment variable or configure matlab_path in ls_specific_settings. """ + MSL = "msl" + """mIRC Scripting Language (mSL) language server. + Supports .mrc files used in mIRC and AdiIRC IRC clients. + Uses a custom LSP server based on pygls. Automatically sets up + a virtual environment with pygls dependencies on first use. + """ # Experimental or deprecated Language Servers TYPESCRIPT_VTS = "typescript_vts" """Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls""" @@ -321,6 +327,8 @@ class Language(str, Enum): return FilenameMatcher("*.sol") case self.ANSIBLE: return FilenameMatcher("*.yaml", "*.yml") + case self.MSL: + return FilenameMatcher("*.mrc") case _: raise ValueError(f"Unhandled language: {self}") @@ -544,6 +552,10 @@ class Language(str, Enum): from solidlsp.language_servers.ansible_language_server import AnsibleLanguageServer return AnsibleLanguageServer + case self.MSL: + from solidlsp.language_servers.msl_language_server import MslLanguageServer + + return MslLanguageServer case _: raise ValueError(f"Unhandled language: {self}") diff --git a/test/conftest.py b/test/conftest.py index ce5761c6..4d3f1154 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -259,6 +259,7 @@ _LANGUAGE_PYTEST_MARKERS: dict[Language, list[MarkDecorator | Mark]] = { Language.JAVA: [pytest.mark.java], Language.KOTLIN: [pytest.mark.kotlin, pytest.mark.skipif(is_ci, reason="Kotlin LSP JVM crashes on restart in CI")], Language.LEAN4: [pytest.mark.lean4, pytest.mark.skipif(_sh.which("lean") is None, reason="Lean is not installed")], + Language.MSL: [pytest.mark.msl], Language.PHP: [pytest.mark.php], Language.PHP_PHPACTOR: [pytest.mark.php], Language.POWERSHELL: [pytest.mark.powershell], diff --git a/test/resources/repos/msl/test_repo/main.mrc b/test/resources/repos/msl/test_repo/main.mrc new file mode 100644 index 00000000..04385ee1 --- /dev/null +++ b/test/resources/repos/msl/test_repo/main.mrc @@ -0,0 +1,38 @@ +; Main game module - tests alias, event, and menu parsing + +alias greet { + msg $chan Hello, $1 $+ ! Welcome aboard. +} + +alias -l calculate.doubloons { + var %base = 100 + var %bonus = $rand(1,50) + return $calc(%base + %bonus) +} + +on *:TEXT:!hello*:#: { + greet $nick +} + +on *:JOIN:#: { + .timer 1 3 greet $nick + var %coins = $format.coins(100) + msg $chan You have %coins +} + +raw 319:*: { + ; Handle channel list numeric + echo -a Channels: $3- +} + +menu channel { + Pirates Game + .Start Game:/start + .End Game:/stop +} + +alias show.player.info { + if ($is.admin($nick)) { + msg $chan $nick is an admin with $format.coins(500) + } +} diff --git a/test/resources/repos/msl/test_repo/utils.mrc b/test/resources/repos/msl/test_repo/utils.mrc new file mode 100644 index 00000000..c7a12266 --- /dev/null +++ b/test/resources/repos/msl/test_repo/utils.mrc @@ -0,0 +1,26 @@ +; Utility aliases and handlers + +alias format.coins { + return $bytes($1,b) doubloons +} + +alias is.admin { + if ($1 isop $chan) return $true + return $false +} + +alias welcome.message { + greet $1 + msg $chan You have $format.coins(100) to start with! +} + +dialog settings { + title "Game Settings" + size -1 -1 200 150 + edit "", 1, 10 10 180 20 + button "Save", 2, 60 40 80 25 +} + +ctcp *:VERSION:*: { + ctcpreply $nick VERSION PiratesIRC v1.0 +} diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 01cd29d1..be078eae 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -47,6 +47,7 @@ def serena_config(): Language.CPP_CCLS, Language.HAXE, Language.LEAN4, + Language.MSL, ]: repo_path = get_repo_path(language) if repo_path.exists(): @@ -199,6 +200,7 @@ class TestSerenaAgent: pytest.param(Language.CPP_CCLS, "add", "Function", "b.cpp", marks=pytest.mark.cpp), pytest.param(Language.HAXE, "Main", "Class", "Main.hx", marks=pytest.mark.haxe), pytest.param(Language.LEAN4, "add", "Method", "Helper.lean", marks=pytest.mark.lean4), + pytest.param(Language.MSL, "greet", "Function", "main.mrc", marks=pytest.mark.msl), ], indirect=["serena_agent"], ) @@ -307,6 +309,7 @@ class TestSerenaAgent: marks=pytest.mark.haxe, ), pytest.param(Language.LEAN4, "add", "Helper.lean", "Main.lean", marks=pytest.mark.lean4), + pytest.param(Language.MSL, "format.coins", "utils.mrc", "main.mrc", marks=pytest.mark.msl), ], indirect=["serena_agent"], ) diff --git a/test/solidlsp/msl/__init__.py b/test/solidlsp/msl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/solidlsp/msl/test_msl_basic.py b/test/solidlsp/msl/test_msl_basic.py new file mode 100644 index 00000000..92a4cd41 --- /dev/null +++ b/test/solidlsp/msl/test_msl_basic.py @@ -0,0 +1,161 @@ +""" +Basic integration tests for the mSL (mIRC Scripting Language) language server. + +Tests validate document symbols, references, and definitions for aliases, events, +raw events, menus, dialogs, and CTCP handlers using the mSL test repository. +""" + +import os + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols + +pytestmark = [pytest.mark.msl] + + +class TestMslDocumentSymbols: + """Test document symbol retrieval for mSL constructs.""" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_ls_is_running(self, language_server: SolidLanguageServer) -> None: + """Test that the language server starts successfully.""" + assert language_server.is_running() + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_document_symbols_main(self, language_server: SolidLanguageServer) -> None: + """Test that document symbols are returned for the main file.""" + doc_symbols = language_server.request_document_symbols("main.mrc") + all_symbols, root_symbols = doc_symbols.get_all_symbols_and_roots() + + symbol_names = [s.get("name") for s in all_symbols if s.get("name")] + assert "greet" in symbol_names, f"greet alias not found. Found: {symbol_names}" + assert "calculate.doubloons" in symbol_names, f"calculate.doubloons alias not found. Found: {symbol_names}" + assert "show.player.info" in symbol_names, f"show.player.info alias not found. Found: {symbol_names}" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_document_symbols_events(self, language_server: SolidLanguageServer) -> None: + """Test that event handlers, raw events, and menus are detected in the main file.""" + doc_symbols = language_server.request_document_symbols("main.mrc") + all_symbols, root_symbols = doc_symbols.get_all_symbols_and_roots() + + symbol_names = [s.get("name") for s in all_symbols if s.get("name")] + # Check for on *:TEXT and on *:JOIN events + on_events = [n for n in symbol_names if n.startswith("on ")] + assert len(on_events) >= 2, f"Expected at least 2 event handlers. Found: {on_events}" + # Check for raw event + raw_events = [n for n in symbol_names if n.startswith("raw ")] + assert len(raw_events) >= 1, f"Expected at least 1 raw event handler. Found: {raw_events}" + # Check for menu + menus = [n for n in symbol_names if n.startswith("menu ")] + assert len(menus) >= 1, f"Expected at least 1 menu. Found: {menus}" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_document_symbols_utils(self, language_server: SolidLanguageServer) -> None: + """Test that document symbols are returned for the utils file.""" + doc_symbols = language_server.request_document_symbols("utils.mrc") + all_symbols, root_symbols = doc_symbols.get_all_symbols_and_roots() + + symbol_names = [s.get("name") for s in all_symbols if s.get("name")] + assert "format.coins" in symbol_names, f"format.coins alias not found. Found: {symbol_names}" + assert "is.admin" in symbol_names, f"is.admin alias not found. Found: {symbol_names}" + assert "welcome.message" in symbol_names, f"welcome.message alias not found. Found: {symbol_names}" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_document_symbols_dialog_and_ctcp(self, language_server: SolidLanguageServer) -> None: + """Test that dialog and CTCP handler definitions are detected.""" + doc_symbols = language_server.request_document_symbols("utils.mrc") + all_symbols, root_symbols = doc_symbols.get_all_symbols_and_roots() + + symbol_names = [s.get("name") for s in all_symbols if s.get("name")] + assert "dialog settings" in symbol_names, f"dialog settings not found. Found: {symbol_names}" + # Check for CTCP handler + ctcp_events = [n for n in symbol_names if n.startswith("ctcp ")] + assert len(ctcp_events) >= 1, f"Expected at least 1 ctcp handler. Found: {ctcp_events}" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: + """Test that the full symbol tree contains expected symbols from both files.""" + from solidlsp.ls_utils import SymbolUtils + + symbols = language_server.request_full_symbol_tree() + assert SymbolUtils.symbol_tree_contains_name(symbols, "greet"), "greet not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "format.coins"), "format.coins not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "show.player.info"), "show.player.info not found in symbol tree" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_bare_symbol_names(self, language_server: SolidLanguageServer) -> None: + """Test that symbol names do not contain unexpected formatting characters.""" + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + # mSL symbols can contain periods (e.g., calculate.doubloons) and + # colons/spaces in event names (e.g., "on *:TEXT"), so allow those + if has_malformed_name(s, period_allowed=True, colon_allowed=True, whitespace_allowed=True): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None: + """Test that references to 'greet' are found within main.mrc.""" + file_path = "main.mrc" + all_symbols, _ = language_server.request_document_symbols(file_path).get_all_symbols_and_roots() + greet_symbol = next((s for s in all_symbols if s.get("name") == "greet"), None) + assert greet_symbol is not None, "Could not find 'greet' symbol in main.mrc" + + sel_start = greet_symbol["selectionRange"]["start"] + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + + assert refs, f"Expected non-empty references for greet but got {refs=}" + + actual_locations = [ + { + "uri_suffix": os.path.basename(ref.get("relativePath", ref.get("uri", ""))), + "line": ref["range"]["start"]["line"], + } + for ref in refs + ] + + # greet is called on line 13 (0-indexed) in main.mrc: `greet $nick` + call_site = {"uri_suffix": "main.mrc", "line": 13} + assert call_site in actual_locations, f"Expected reference to greet at line 13 in main.mrc, got {actual_locations}" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None: + """Test that references to 'format.coins' are found across main.mrc and utils.mrc.""" + # format.coins is defined in utils.mrc but called in both main.mrc and utils.mrc + file_path = "utils.mrc" + all_symbols, _ = language_server.request_document_symbols(file_path).get_all_symbols_and_roots() + fc_symbol = next((s for s in all_symbols if s.get("name") == "format.coins"), None) + assert fc_symbol is not None, "Could not find 'format.coins' symbol in utils.mrc" + + sel_start = fc_symbol["selectionRange"]["start"] + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + + assert refs, f"Expected non-empty references for format.coins but got {refs=}" + + actual_locations = [ + { + "uri_suffix": os.path.basename(ref.get("relativePath", ref.get("uri", ""))), + "line": ref["range"]["start"]["line"], + } + for ref in refs + ] + + # Verify cross-file: at least one reference is in main.mrc (different file from definition) + main_refs = [loc for loc in actual_locations if loc["uri_suffix"] == "main.mrc"] + assert len(main_refs) >= 1, f"Expected at least 1 reference in main.mrc, got {main_refs}" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_workspace_symbol(self, language_server: SolidLanguageServer) -> None: + """Test that workspace symbol search returns results.""" + result = language_server.request_workspace_symbol("greet") + assert result is not None, "Workspace symbol search returned None" + assert len(result) > 0, "Workspace symbol search returned no results" + assert any("greet" in str(s.get("name", "")) for s in result), f"Expected at least one result containing 'greet', got {result}" diff --git a/uv.lock b/uv.lock index 17190a91..c89fecb4 100644 --- a/uv.lock +++ b/uv.lock @@ -243,6 +243,19 @@ filecache = [ { name = "filelock" }, ] +[[package]] +name = "cattrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/ba18945e7d6e55a58364d9fb2e46049c1c2998b3d805f19b703f14e81057/cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40", size = 495672, upload-time = "2026-02-18T22:15:19.406Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/56/60547f7801b97c67e97491dc3d9ade9fbccbd0325058fd3dfcb2f5d98d90/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096", size = 73054, upload-time = "2026-02-18T22:15:17.958Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -1291,6 +1304,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, ] +[[package]] +name = "lsprotocol" +version = "2025.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/26/67b84e6ec1402f0e6764ef3d2a0aaf9a79522cc1d37738f4e5bb0b21521a/lsprotocol-2025.0.0.tar.gz", hash = "sha256:e879da2b9301e82cfc3e60d805630487ac2f7ab17492f4f5ba5aaba94fe56c29", size = 74896, upload-time = "2025-06-17T21:30:18.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/f0/92f2d609d6642b5f30cb50a885d2bf1483301c69d5786286500d15651ef2/lsprotocol-2025.0.0-py3-none-any.whl", hash = "sha256:f9d78f25221f2a60eaa4a96d3b4ffae011b107537facee61d3da3313880995c7", size = 76250, upload-time = "2025-06-17T21:30:19.455Z" }, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -2143,6 +2169,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6", size = 4640157, upload-time = "2024-06-25T19:28:42.383Z" }, ] +[[package]] +name = "pygls" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, + { name = "lsprotocol" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/2e/7bbe061d175c0baddde8fc9edb908a4c31ba5d9165b8c68e3439c3a9f138/pygls-2.1.1.tar.gz", hash = "sha256:1da03ba9053201bb337dcdd8d121df70feb2a91e1a0dcc74de5da79755b1a201", size = 55091, upload-time = "2026-03-25T11:19:10.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl", hash = "sha256:510a6dea2476177230c7d851125e5948efdf3fdb9ebfd8543fc434972f8faed4", size = 68975, upload-time = "2026-03-25T11:19:11.374Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -2924,12 +2964,14 @@ dependencies = [ { name = "fortls" }, { name = "jinja2" }, { name = "joblib" }, + { name = "lsprotocol" }, { name = "mcp" }, { name = "oraios-pywebview" }, { name = "overrides" }, { name = "pathspec" }, { name = "psutil" }, { name = "pydantic" }, + { name = "pygls" }, { name = "pyright" }, { name = "pystray" }, { name = "python-dotenv" }, @@ -2998,6 +3040,7 @@ requires-dist = [ { name = "jinja2", marker = "extra == 'dev'", specifier = "==3.1.6" }, { name = "joblib", specifier = "==1.5.1" }, { name = "jupyter-book", marker = "extra == 'dev'", specifier = "==1.0.4.post1" }, + { name = "lsprotocol", specifier = "==2025.0.0" }, { name = "mcp", specifier = "==1.26.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = "==1.17.0" }, { name = "nbconvert", marker = "extra == 'dev'", specifier = "==7.17.0" }, @@ -3009,6 +3052,7 @@ requires-dist = [ { name = "psutil", specifier = "==7.0.0" }, { name = "pyasn1", marker = "extra == 'dev'", specifier = "==0.6.3" }, { name = "pydantic", specifier = "==2.12.5" }, + { name = "pygls", specifier = "==2.1.1" }, { name = "pyinstrument", marker = "extra == 'dev'", specifier = "==5.1.1" }, { name = "pyjwt", marker = "extra == 'dev'", specifier = "==2.12.0" }, { name = "pyright", specifier = "==1.1.403" }, From 1789967968412bd6340532b50a641a564f5a2f18 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 13 Apr 2026 09:17:24 +0200 Subject: [PATCH 139/190] Pyproject: added description and classifiers [ci skip] Description in particular is important since it is shown in pypi --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8894c98f..7ab83e26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,13 +5,16 @@ requires = ["hatchling"] [project] name = "serena-agent" version = "1.1.1" -description = "" +description = "A powerful MCP toolkit for coding, providing semantic retrieval and editing capabilities - the IDE for your agent" authors = [{ name = "Oraios AI", email = "info@oraios-ai.de" }] readme = "README.md" requires-python = ">=3.11, <3.15" classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.13", ] dependencies = [ "requests==2.33.0", From 9b7dcbdf63ee9b91f9fe4b02facd6de7caf8d4d5 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 13 Apr 2026 11:55:34 +0200 Subject: [PATCH 140/190] Fix: sanitize parameter input in jetbrains tools Context: clients may paste escaped html for > and <, which appears in paths for external dependencies --- src/serena/tools/jetbrains_tools.py | 13 +++++++++++++ src/serena/tools/tools_base.py | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index a94d5c99..7f503c44 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -71,6 +71,10 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): if include_body: depth = 0 # ignore user-specified depth if body is requested + name_path_pattern = self._sanitize_input_param(name_path_pattern) + if relative_path: + relative_path = self._sanitize_input_param(relative_path) + if relative_path == ".": relative_path = None with JetBrainsPluginClient.from_project(self.project) as client: @@ -149,6 +153,7 @@ class JetBrainsMoveTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, ToolMa :param target_relative_path: the relative path of the target directory or file. :param target_parent_name_path: the name path of the target parent symbol. """ + relative_path = self._sanitize_input_param(relative_path) with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.move( name_path=name_path, @@ -187,6 +192,7 @@ class JetBrainsSafeDeleteTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, :param propagate: whether to propagate the deletion to usages of the symbol and also remove symbols that become unused after the deletion. Default is False. """ + relative_path = self._sanitize_input_param(relative_path) with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.safe_delete( name_path=name_path, @@ -220,6 +226,7 @@ class JetBrainsInlineSymbol(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, To :param keep_definition: whether to keep the original method definition after inlining all call sites. May be ignored in some cases (e.g. when inlining a class). """ + relative_path = self._sanitize_input_param(relative_path) with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.inline_symbol( name_path=name_path, @@ -251,6 +258,7 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMark :param relative_path: the relative path to the file containing the symbol (must be a file, not a directory) :param max_answer_chars: max characters for the result (-1 for default). If exceeded, no content/a shortened result is returned. """ + relative_path = self._sanitize_input_param(relative_path) with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.find_references( name_path=name_path, @@ -317,6 +325,7 @@ class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOp :param max_answer_chars: max characters for the result (-1 for default). If exceeded, no content/a shortened result is returned. :param include_file_documentation: whether to include the file's docstring. Default False. """ + relative_path = self._sanitize_input_param(relative_path) with JetBrainsPluginClient.from_project(self.project) as client: symbol_overview = client.get_symbols_overview( relative_path=relative_path, depth=depth, include_file_documentation=include_file_documentation @@ -419,6 +428,7 @@ class JetBrainsTypeHierarchyTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptiona -1 means the default value from the config will be used. :return: Compact JSON with file-grouped hierarchy. Error string if not applicable. """ + relative_path = self._sanitize_input_param(relative_path) with JetBrainsPluginClient.from_project(self.project) as client: subtypes = None supertypes = None @@ -473,6 +483,9 @@ class JetBrainsFindDeclarationTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptio Uses Python syntax with MULTILINE and DOTALL flags enabled. :param include_body: whether to include the symbol's body in the result. Default False. """ + relative_path = self._sanitize_input_param(relative_path) + regex = self._sanitize_input_param(regex) + editor = self.create_code_editor() content = editor.read_file(relative_path) coords = find_text_coordinates(content, regex, require_unique=True) diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index 18badd67..7a453ae0 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -137,6 +137,11 @@ class Tool(Component): _last_tool_call_client_str: str | None = None """We can only get the client info from within a tool call. Each tool call will update this variable.""" + @staticmethod + def _sanitize_input_param(raw_param: str) -> str: + # some clients replace < and > with their escaped html versions, we need to counteract this + return raw_param.replace("<", "<").replace(">", ">") + @classmethod def set_last_tool_call_client_str(cls, client_str: str | None) -> None: cls._last_tool_call_client_str = client_str From e633c94e3b09d613c664aa39d3cfbc75b5b5a4b6 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 13 Apr 2026 14:00:06 +0200 Subject: [PATCH 141/190] JB tools: improve docstrings related to relative_path for external dependencies Claude was confused.. --- CHANGELOG.md | 1 + src/serena/tools/jetbrains_tools.py | 2 ++ src/serena/tools/symbol_tools.py | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35f4a3d0..a542e27f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Status of the `main` branch. Changes prior to the next official version change w * JetBrains: - `JetBrainsRenameTool` can now also rename occurrences in comments and text. + - Minor fixes in handling `relative_path` for external dependencies. * Language Servers: - Fix Dart LSP returning only symbol name as body instead of full method body. diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 7f503c44..c91833aa 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -58,6 +58,8 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): for the case where the symbol is a class, this will return its methods). Ignored if `include_body=True`. Default 0. :param relative_path: Optional. Restrict search to this file or directory. If not specified, searches entire codebase. + Note: for external dependencies, this must be an identifier starting with ` Date: Mon, 13 Apr 2026 11:10:43 +0200 Subject: [PATCH 142/190] Added documentation on Serena evaluation and first evaluation results --- README.md | 29 +- docs/04-evaluation/000_intro.md | 168 +++++ docs/04-evaluation/010_evaluation-prompt.md | 310 +++++--- .../011_followup-summary-prompt.md | 4 +- .../010_cc_on_tianshou-serena-evaluation.md | 449 ++++++------ ...020_codex_on_jbplugin-serena-evaluation.md | 670 ++++++++++++++++-- 6 files changed, 1243 insertions(+), 387 deletions(-) diff --git a/README.md b/README.md index 194f5793..72e3e467 100644 --- a/README.md +++ b/README.md @@ -30,24 +30,27 @@ more complex codebases. ## What Our "End Users" Say -Our end users are essentially AI agents, so they are also in the best position to evaluate Serena. -We wrote an unbiased and detailed evaluation prompt, which leads the agent to estimate the value-add of Serena's tools -when added on top of their existing capabilities. After about 20 minutes of thorough evaluation and experimentation, here -is what the agents had to say: +While it is humans who download and set up Serena, our end users are essentially AI agents, +so they are also in the best position to evaluate Serena. +We crafted an unbiased and detailed prompt which leads the agent to estimate the value of adding Serena's tools +to its built-in capabilities. The thorough evaluation usually takes around 25 minutes (per project) +and tests every single aspect of Serena. A summary of what the agents had to say: -**Opus 4.6 in Claude Code, evaluated on a large Python codebase:** -> "If I could ask my owner for one upgrade to my coding toolkit, it would be Serena — its symbolic addressing eliminated the constant re-reading that quietly eats my context on multi-edit sessions, its atomic cross-file refactorings collapsed 10-call manual workflows into one, and after a rigorous hands-on evaluation I can say that while built-in tools remain the right choice for small edits and text searches, Serena fills the exact gaps that make me feel clumsy without it." +**Opus 4.6 (high effort) in Claude Code on a large Python codebase:** +> "Serena's IDE-backed semantic tools are the single most impactful addition to my toolkit — cross-file renames, moves, and reference lookups that +would cost me 8–12 careful, error-prone steps collapse into one atomic call, and I would absolutely ask any developer I work with to set them up." -**Gpt 5.4 max in Codex CLI, evaluated on a Java codebase:** -> "As a coding AI agent, I would ask my owner to add Serena because it lets me work with code as stable symbols instead of fragile text, turning the -refactors, lookups, and multi-file edits that usually feel risky and repetitive into precise IDE-backed operations I can trust." +**Gpt 5.4 (high) in Codex CLI on a Java codebase:** +> "As a coding AI agent, I would ask my owner to add Serena because it gives me the missing IDE-level understanding of symbols, references, and +refactorings, turning fragile text surgery into calmer, faster, more confident code changes where semantics matter." -See our documentation on the [evaluation methodology](https://oraios.github.io/serena/04-evaluation/000_intro.html) and the -detailed results beyond the brief recommendations above. -You can easily use our methods to run your own evaluation of Serena on a project of your choice. - Your agent deserves the best coding tools, give them Serena! +See the documentation on our [evaluation methods](https://oraios.github.io/serena/04-evaluation/000_intro.html) and the +detailed results beyond the brief recommendations above. You can easily run your own evaluation of Serena on a project of your choice +by reusing our methods or adapting them to your needs. + + ## How Serena Works Serena provides the necessary [tools](https://oraios.github.io/serena/01-about/035_tools.html) for coding workflows, diff --git a/docs/04-evaluation/000_intro.md b/docs/04-evaluation/000_intro.md index a66a8a9c..74b300af 100644 --- a/docs/04-evaluation/000_intro.md +++ b/docs/04-evaluation/000_intro.md @@ -1,3 +1,171 @@ # Evaluation In this section we describe how we evaluate the performance of Serena's tools. + +The evaluation measures the **concrete delta** that Serena's tools provide on top of an agent's +built-in capabilities (file reads, text edits, grep, shell, etc.). +Rather than a simple thumbs-up/thumbs-down, it produces a detailed, evidence-based report +covering capabilities, efficiency and reliability. + +### Why Not Benchmarks? + +Standard coding benchmarks (SWE-bench, HumanEval, etc.) measure an agent's ability to solve +predefined tasks with a known correct answer. They are valuable for comparing models and agents, +but they are a poor fit for evaluating a **tool augmentation layer** like Serena for several reasons: + +- **Benchmarks don't reflect real usage patterns.** Benchmark tasks are typically small, self-contained + problems that can be solved by reading and editing a handful of files. They rarely exercise the + workflows where Serena's tools shine — cross-file refactoring, navigating large codebases by symbol + structure, chaining multiple edits with stable addressing, or querying type hierarchies and + external dependencies. A benchmark score would mostly measure performance on tasks where Serena + is not expected to help. +- **Results would not generalise to the user's project.** Serena's value depends on the codebase + (size, language, complexity), the agent (model, built-in tools), and the client harness + (Claude Code, Codex, IDE plugins, etc.). A fixed benchmark on a fixed codebase with a fixed + agent tells you little about what Serena would add to *your* setup. +- **Predefined tasks bias the measurement.** Choosing specific tasks to evaluate inevitably + introduces selection bias — we would end up picking tasks that either favour or disfavour Serena. + We wanted an evaluation that systematically covers the full surface area of Serena's capabilities + without cherry-picking. + +### Design Goals + +Instead of benchmarks, we designed an evaluation methodology with three goals: + +1. **Reproducible by any user, on any project, with any agent.** + The evaluation is a single prompt that you give to your agent of choice, pointed at your codebase + of choice, in your client of choice. This means the results directly reflect the value Serena + would add to your actual workflow — not to an artificial benchmark setup. There is nothing to + install, configure, or script beyond what you already have. + +2. **The agent evaluates itself.** + We deliberately let the AI agent be both the executor and the evaluator. This may seem + counterintuitive, but it is the right design choice: the agent is the actual end user of the + tools, so it is in the best position to judge whether a semantic tool improves its workflow + compared to its built-in alternatives. It can measure call counts, payload sizes, and + prerequisite steps from direct experience rather than from proxy metrics. It also avoids the + problem of a human evaluator having to simulate how an agent would use the tools — the agent + simply uses them and reports what it observes. + +3. **Comprehensive and unbiased by design.** + Rather than selecting specific tasks, the prompt defines **task categories** that systematically + span Serena's capabilities: codebase understanding, single-file edits of varying sizes, multi-file + refactoring, reliability properties, and workflow effects. The agent picks concrete instances + from the codebase at hand, performs each task using both toolsets side by side, and classifies + every finding as a positive delta, a neutral/negative delta, or out of scope. The prompt + explicitly requires reporting negative deltas and cases where Serena offers no improvement, + structurally counterbalancing any tendency to favour the tool being evaluated. + +### Method + +We give an AI coding agent a single, detailed [evaluation prompt](010_evaluation-prompt) in a one-shot session. +The prompt instructs the agent to perform approximately 20 hands-on tasks across five areas: + +1. **Codebase understanding** — structural overviews, targeted symbol retrieval, reference finding, type hierarchies, and external dependency lookup. +2. **Single-file edits** — small tweaks, medium rewrites, full-body replacements, insertions, and local renames. +3. **Multi-file changes** — cross-file renames, symbol and file moves, safe deletes, and inlining. +4. **Reliability & correctness** — scope precision, atomicity, and success signals. +5. **Workflow effects** — chained edits, stable vs. ephemeral addressing, and multi-step exploration. + +For every task, the agent executes the full end-to-end workflow using **both** toolsets (Serena's semantic +tools and its own built-in tools), applies real edits verified via `git diff`, and records call counts, +payload sizes and prerequisite steps. Edits are reverted after each experiment to keep the working tree clean. + +The resulting report classifies each finding into one of three categories: +**(a)** tasks where Serena adds capability, +**(b)** tasks where Serena applies but offers no improvement, and +**(c)** tasks outside Serena's scope. +Only category (b) constitutes a neutral or negative finding; category (c) is context, not a finding. + +After the evaluation, a separate [follow-up prompt](011_followup-summary-prompt) asks the agent for a +one-sentence, user-facing recommendation — the quotes shown on the [main page](https://github.com/oraios/serena). + +### Results + +We performed evaluations using popular AI coding agents in representative scenarios — different +agents, different programming languages, and different codebases — to show that the results are +not specific to a single setup. + +All evaluations were conducted using the **JetBrains-powered version** of Serena, as it is the +more powerful backend with a broader set of refactoring and navigation capabilities. The +evaluation can easily be repeated with the LSP-based backend to assess its subset of capabilities. + +- [Claude Code (Opus 4.6) on a large Python codebase](results/010_cc_on_tianshou-serena-evaluation) — tianshou, a reinforcement learning library (~26K lines). +- [Codex (GPT 5.4) on a Java codebase](results/020_codex_on_jbplugin-serena-evaluation) — the Serena JetBrains plugin itself. + +You can run your own evaluation on a project of your choice by reusing our +[evaluation prompt](010_evaluation-prompt) or adapting it to your needs. + +### Assessment of the Methodology + +_The following assessment was written by Claude Opus 4.6 (high effort) after reading the full +evaluation methodology, evaluation prompt, and all result documents._ + +**The methodology is sound**, and the two published results demonstrate that it produces meaningful, +detailed evaluations. Both reports follow the prompt's structure faithfully: they perform all ~20 +tasks using both toolsets, record concrete measurements (call counts, payload sizes, prerequisite +steps), and classify findings into the three required categories. Crucially, both agents report +neutral and negative deltas honestly — the Claude Code report explicitly notes that small edits are +more efficient with built-ins (~4.5x less payload), and the Codex report flags that tiny intra-method +changes and simple one-file renames see no benefit from Serena. Neither report reads as promotional; +they read as technical comparisons with quantified evidence on both sides. + +The two reports also validate the methodology's design goal of generalisability: despite being +produced by different agents (Opus 4.6 vs GPT 5.4), on different codebases (Python RL library vs +Java IDE plugin), they converge on the same core findings — cross-file refactoring is Serena's +highest-value contribution, structural navigation provides a moderate advantage, and small local +edits are better handled by built-ins. This convergence across independent runs increases confidence +that the findings reflect genuine properties of the toolset rather than artefacts of a particular +agent or codebase. + +The methodology's core strengths are: + +- **Self-evaluation by the agent is the right design choice.** The agent is the actual consumer of the + tools, so it can report on workflow friction, payload overhead, and call counts from first-hand + experience. A human evaluator would have to guess at these. +- **Task categories instead of fixed tasks** avoid cherry-picking while still ensuring coverage. Letting + the agent pick concrete instances from the codebase at hand means the evaluation naturally adapts to + what the project actually contains (e.g. skipping inline if no suitable candidate exists, as the + Claude Code report did for Python, while the Codex report found a suitable Java candidate). +- **The three-category classification** (adds value / applies but no improvement / out of scope) is the + right framing for an augmentation layer. It prevents the common trap of penalising a tool for not + covering things it was never designed to cover. +- **Reproducibility by users** is a strong differentiator. Anyone can validate the claims on their own + codebase with their own agent. + +The main limitation is scope: the published results cover two agent/codebase combinations using the +JetBrains backend. Single-shot variance means a second run of the same agent could produce different +task selections and slightly different verdicts, and the LSP backend — which most users will +encounter first — has not yet been evaluated. These are practical constraints of cost, not +methodological flaws, and the reproducible design explicitly invites users to fill these gaps +themselves. + +One thing to watch for is whether agents that are less capable than Opus 4.6 or GPT 5.4 produce +meaningful evaluations — the prompt is quite demanding. But that is a question about the agent, not +about the method. + +### Prompt Fairness + +We put significant effort in crafting an unbiased, fair and objective evaluation prompt. +The only "biased" aspects are some sentences about misuse of tools, which we consider irrelevant for the purpose of evaluation +(sufficiently intelligent models use the tools correctly). +Here is what Opus 4.6 thinks about our prompt: + +**Q**: +> The attached prompt is designed to evaluate the additive value of Serena as an augmentation layer on top of built-in tools +> (not to compare two competing alternatives). +> Is the methodology suitable for that goal, or does it introduce bias that would distort the measurement of what Serena adds? +> Note that we assume that sufficiently intelligent models use the tools correctly, +> which is why misuse and failure modes are out of scope for this evaluation. +> Do not question the validity of this assumption in your evaluation of the prompt. +> Give a brief answer, maximally two sentences. +> +> \ + +**Opus' Answer**: +> "The methodology is well-suited to its stated goal: by explicitly scoping to correct use, treating Serena as an +> augmentation layer rather than a competitor, and classifying out-of-scope tasks as context rather than negative +> findings, it avoids the most common biases that would distort a delta measurement. The one risk is that the "correct-use +> rule" could subtly favor Serena by preemptively filtering out any scenario where its tools underperform — but the prompt +> mitigates this by requiring category (b) findings (applies but no improvement) and explicitly demanding negative deltas +> be reported, so the bias is acknowledged and structurally counterbalanced." \ No newline at end of file diff --git a/docs/04-evaluation/010_evaluation-prompt.md b/docs/04-evaluation/010_evaluation-prompt.md index 540350e6..50422f28 100644 --- a/docs/04-evaluation/010_evaluation-prompt.md +++ b/docs/04-evaluation/010_evaluation-prompt.md @@ -1,149 +1,295 @@ # Evaluation Prompt -Use the prompt below to evaluate the added value of Serena's tools against your agent's built-in tools on a project of your choice. +Use the prompt below to evaluate the added value of Serena's tools against your agent's built-in tools on a project of +your choice. All evaluations that you find in our documentation were created in one-shot sessions, only using this prompt and then following up with a separate [summary prompt](011_followup-summary-prompt) - # Evaluate Serena's Tools Against Built-Ins -You have access to Serena's coding tools alongside your built-in tools (Read, Edit, Write, Glob, Grep, Bash, etc.). I want a thorough, evidence-based evaluation of **what Serena's tools add on top of the built-ins**, assuming both toolsets are used correctly. +You have access to Serena's coding tools alongside your built-in tools (Read, Edit, Write, Glob, Grep, Bash, etc.). I +want a thorough, evidence-based evaluation of **what Serena's tools add on top of the built-ins**, assuming both +toolsets are used correctly. -This is an evaluation, not a user guide, and it is not a binary adoption pitch. Your job is to answer: *if a competent user of both toolsets had only the built-ins, what concrete capabilities and efficiency wins would they be missing, and by how much?* A reader who finishes your report should have a clear, specific picture of what Serena adds — in terms of capabilities that weren't available at all, workflows that collapsed from many calls to one, and efficiency multipliers that show up across a session. Not a thumbs-up/thumbs-down, but a sharp description of the delta. +This is an evaluation, not a user guide, and it is not a binary adoption pitch. Your job is to answer: *if a competent +user of both toolsets had only the built-ins, what concrete capabilities and efficiency differences would they +experience, and by how much?* A reader who finishes your report should have a clear, specific picture of what Serena +adds — as well as where it provides no meaningful improvement or introduces tradeoffs — in terms of capabilities, +workflows, and efficiency. Not a thumbs-up/thumbs-down, but a sharp description of the delta. -Failure modes from misuse, silent-failure traps, gotcha comparisons, and "be careful of X" warnings are out of scope. They belong in onboarding material for a developer learning the tools, not in a delta analysis of what the tools add. +Failure modes from misuse, silent-failure traps, gotcha comparisons, and "be careful of X" warnings are out of scope. +They belong in onboarding material for a developer learning the tools, not in a delta analysis of what the tools add. -**Describe the added value sharply, don't hedge.** A "both have their place" or "complementary, not rivals" opener is not a description of added value — it conveys zero information about what a reader would actually gain or lose by adding the tool. If Serena adds substantial capabilities, name them and quantify them. If it adds marginal capabilities, say that and show why. The two toolsets are complementary — that's a given, not the answer. The answer is a specific list of what Serena contributes to a correct-use workflow that built-ins alone cannot provide. +**Describe the measured differences clearly and neutrally.** Avoid generic or non-informative framing such as "both have +their place" unless supported by concrete findings. If Serena adds substantial capabilities, name and quantify them. If +it adds marginal or no capabilities, say that and show why. If there are regressions or tradeoffs, include them +explicitly. The two toolsets are complementary — that's a given, not the answer. +Serena is an augmentation layer, not a replacement. Do not penalize it for tasks it was not designed to address — +instead, note those tasks as "built-in only" and move on. The evaluation should measure what Serena adds where it +applies, not what it fails to add where it doesn't.The answer is a specific list of what +Serena contributes (or does not contribute) to a correct-use workflow relative to built-ins. + +Write the report to serena-evaluation.md in the repo root. + +--- ## Ground rules ### Starting conditions -- Start fresh. Do not read project memories, CLAUDE.md shortcuts, or prior notes about the repo. Do not read documentation files either. Explore as if you've never seen it, focusing on code. -- Use git as your safety net — experiment freely. Any edit can be reverted with `git checkout -- ` or `git stash`. Run edits for real; don't simulate. A hands-on comparison is worth far more than a thought experiment. +- Start fresh. Do not read project memories, CLAUDE.md shortcuts, or prior notes about the repo. Do not read + documentation files either. Explore as if you've never seen it, focusing on code. +- Use git as your safety net — experiment freely. Any edit can be reverted with `git checkout -- ` or `git stash`. + Run edits for real; don't simulate. A hands-on comparison is worth far more than a thought experiment. - After each experiment, verify the working tree is clean with `git status --short` before moving on. ### How to compare — correct use only -- **Correct-use rule.** Evaluate each tool on inputs and tasks it was designed for, called the way a competent user would call it. A tool doing exactly what its contract says is not a finding, even if a careless caller could misuse it. Examples of what *not* to report: -- Destructive write tools accepting what you send them — that is the contract of a destructive write, not a silent failure mode. -- Addressing schemes that require you to know what you're addressing — that is how addressing works. -- Tools refusing out-of-scope input (semantic tools on non-code files, single-expression refactorings on multi-branch functions, safe-delete on symbols with usages) — those are correct refusals. -- Mid-session friction that only appears if you mix tool families incorrectly — a caller managing their session properly doesn't hit it. -- Transient glitches may appear after `git checkout --` or other file-system mutations. Wait a moment before continuing after doing such a checkout, and in case of failure, retry once. If it then succeeds, the finding is a non-finding. -- **Know the contract before you call.** Before invoking any tool, have a one-sentence understanding of what it does. If you expect an error or "not applicable," don't make the call. -- **Refactoring semantics are real.** Inlining requires a substitutable function (typically single-expression, no side effects); moving requires a legal target; safe-delete requires no surviving usages. If the repo has no suitable candidate for a given refactoring, report "no suitable candidate in this codebase" and skip it — don't contrive a broken input. +- **Correct-use rule.** Evaluate each tool on inputs and tasks it was designed for, called the way a competent user + would call it. A tool doing exactly what its contract says is not a finding, even if a careless caller could misuse + it. +- **Know the contract before you call.** Before invoking any tool, have a one-sentence understanding of what it does. If + you expect an error or "not applicable," don't make the call. +- **Refactoring semantics are real.** Inlining requires a substitutable function (typically single-expression, no side + effects); moving requires a legal target; safe-delete requires no surviving usages. If the repo has no suitable + candidate for a given refactoring, report "no suitable candidate in this codebase" and skip it — don't contrive a + broken input. ### How to compare — workflow level, not single-call level -- For every task, write out the full end-to-end call chain on each side before declaring a winner. Not just "Serena does it in one call" vs "Grep returns line N" — spell out every call you'd make to reach the goal, including the next step after whichever tool you called first. Many apparent wins and losses evaporate once you include the follow-up. -- Don't score a tool on criteria that only matter in the other toolset's workflow. If you find yourself penalizing Serena for missing a feature (line numbers, text anchors) or penalizing built-ins for missing a feature (name paths, type hierarchy), check whether that feature is actually needed in the tool's own native follow-up. If it's only needed because you're planning to fall back to the other toolset, you're holding the tool to the wrong workflow. -- Ephemeral addressing is a liability. Line numbers and byte offsets go stale the moment a file is edited. Stable addressing (name paths) is an efficiency win, even when the output looks smaller. +- For every task, write out the full end-to-end call chain on each side before drawing conclusions. Include prerequisite + reads and follow-up steps. +- Do not evaluate a tool based on criteria that only arise from mixing workflows incorrectly. +- Ephemeral addressing is a liability. Line numbers and byte offsets go stale after edits; stable addressing (name + paths) may reduce rework. ### How to measure -- Track observations while you work, not at the end. For every tool call, note: number of calls needed, approximate size of what you sent (including re-sent content), size of what you got back, and any prerequisite reads or follow-up verification. These are the raw material for the final report. -- Separate call count, input payload, output payload, and verification cost as distinct axes. A tool that halves call count but doubles payload may not be a net win. -- When comparing two approaches on the same task, include prerequisite Reads and post-hoc verification Greps in the cost — don't hide them outside the comparison. A cross-file rename via Edit isn't "one Edit call"; it's `grep + read × N + edit × N + verify`. +- Track observations during execution. For every tool call, note: number of calls, approximate input size, output size, + and any prerequisite or verification steps. +- Separate call count, input payload, output payload, and verification cost as distinct axes. +- Include prerequisite Reads and post-hoc verification steps in comparisons. + +When a task falls entirely outside Serena's design scope (e.g., reading config files, small text edits where Edit +already sends minimal payload), classify it as "not applicable" rather than as a negative delta. A negative delta +requires that Serena targets the task and performs worse, not that a tool designed for something else is suboptimal when +misapplied to it. + +--- ## Exploration phase — tasks to actually perform -Work through the following. Each item exercises a specific capability under correct use; substitute an equivalent if an item isn't applicable ("no suitable candidate in this codebase" is a valid reason). +Work through the following. Each item exercises a specific capability under correct use; substitute an equivalent if an +item isn't applicable. ### Codebase understanding 1. Get a high-level overview of the repository structure — top-level layout, main packages, entry points. -2. Pick one large source file (300+ lines). Get a structural overview of it. Do it with semantic overview tools and with Glob/Grep/Read. Then write out the concrete next step on each side ("after this overview, to read the body of method X I'd call _____") and compare the pair of calls, not just the overview call. Whichever tool's output most directly feeds its own follow-up wins on workflow terms. +2. Pick one large source file (300+ lines). Get a structural overview of it. Do it with semantic overview tools and with + Glob/Grep/Read. Then write out the concrete next step on each side and compare the pair of calls, not just the + overview call. 3. Pick a specific method inside a class and retrieve its body without reading the surrounding file. -4. For one non-trivial symbol, find all references across the codebase. Compare recall and precision under the question "who uses this in code?" vs "where is this mentioned anywhere, including docs?" — these are different questions, and each toolset is naturally suited to one of them. -5. For a class, list its subclasses / implementations and its supertypes, including transitively. Compare against what text search would need to do (and whether it can follow an override chain or cross into stub files in one step). -6. For at least one symbol from an external dependency (a third-party library), try to retrieve its definition or signature. Note whether each toolset can do this at all and what infrastructure it requires (env activation, site-packages discovery, etc.). +4. For one non-trivial symbol, find all references across the codebase. Compare recall and precision under the + question "who uses this in code?" vs "where is this mentioned anywhere, including docs?" +5. For a class, list its subclasses / implementations and its supertypes, including transitively. Compare against what + text search would need to do. +6. For at least one symbol from an external dependency (a third-party library), try to retrieve its definition or + signature. Note whether each toolset can do this at all and what infrastructure it requires (environment activation, + site-packages discovery, language-server indexing, etc.). ### Single-file edits — span the full range of edit sizes -Do all three sizes below, not just one. The comparison between content-anchored editing (Edit) and symbol-body replacement is size-dependent: Edit's payload grows with the old+new anchor pair, while symbolic body-replace grows with the full new body. They cross over, and where they cross is the whole point. Only testing small edits hides the crossover. +7a. Small tweak (1–3 lines inside a method). Change an error message or rename a local variable inside a larger method. +Do it with `Edit` and with symbolic body replacement. Compare payload sent, payload received, and prerequisite reads. -7a. Small tweak (1–3 lines inside a method). Change an error message or rename a local variable inside a larger method. Do it with `Edit` and with `replace_symbol_body`. Compare payload sent, payload received, and prerequisite reads. +7b. Medium rewrite (replace ~10–30 lines — most of a method body). Rewrite the main logic of a method while keeping its +signature. Do it both ways. -7b. Medium rewrite (replace ~10–30 lines — most of a method body). Rewrite the main logic of a method while keeping its signature. Do it both ways. Note that for Edit you may need a long anchor to keep the old_string unique, and that for symbolic replacement the new body is similar in size to what you'd send for Edit's new_string alone. +7c. Large/whole-body rewrite. Pick a method of 50+ lines and rewrite the entire body. Do it both ways. -7c. Large/whole-body rewrite. Pick a method of 50+ lines and rewrite the entire body. Do it both ways. This is where symbolic body replacement is designed to win: Edit has to send the entire old body as an anchor and the entire new body, while `replace_symbol_body` sends only the new body + a short name path. Measure the ratio. - -8. Insert a new function/method at a specific structural location (e.g., "right after this existing method"). Try both the symbolic-insert path and the manual Edit path. +8. Insert a new function/method at a specific structural location (for example, right after an existing method). Try + both the symbolic-insert path and the manual Edit path. 9. Rename a private helper used only within one file. Compare doing it by hand vs. using a semantic rename. ### Multi-file changes -10. Rename a symbol (function, class, or method) used across several files including imports. Compare the semantic path against the built-in equivalent chain. Under correct use, a semantic rename is paired with a short post-rename `Grep` for text-surface references (docstrings, markdown, notebooks) — count that as a complementary step, not a Serena failure. -11. Move a symbol from one module to another, updating imports at all call sites. Use the semantic move tool if available; plan the built-in equivalent honestly (how many Reads, Edits, and import-cleanup decisions would it take?). -12. Delete a symbol safely, checking it has no remaining usages. Compare "search-then-delete" with a safe-delete tool. -13. Inline a small helper into its call sites — only if the codebase contains a function that is legally inlinable (single-expression body, no early returns, no side effects, substitutable at its call sites). If no such candidate exists, report "no suitable candidate" and skip. Do not contrive a broken input. +10. Rename a symbol (function, class, or method) used across several files including imports. Compare the semantic path + against the built-in equivalent chain. +11. Move a symbol from one module to another, updating imports at all call sites. Use the semantic move tool if + available; plan the built-in equivalent honestly. +12. Move a file or package to a different location, updating imports at all call sites. Use the semantic move tool if + available; plan the built-in equivalent honestly. +12. Delete a symbol safely, checking it has no remaining usages. Compare search-then-delete with a safe-delete tool. +13. Delete a symbol and propagate the deletion to all call sites. Compare to how the built-in equivalent would work. +13. Inline a small helper into its call sites — only if the codebase contains a function that is legally inlinable. If + no such candidate exists, report "no suitable candidate" and skip it. ### Reliability & correctness under correct use -14. Scope precision. Demonstrate that semantic tools address symbols by name path and can target a specific class method, override, or overload that text search would over-match. The point is to show the capability — precision under correct use — not to manufacture a rename that breaks polymorphism through careless targeting. -15. Atomicity. A semantic cross-file refactoring is atomic: either all sites are updated or none. A chain of `Edit` calls is not. You don't have to force a failure — report on what this means for reliability on real failures (disk full, interrupted process, transient permission errors). -16. Success signals. For each completed refactor, note what each tool returns on success. Both toolsets report mechanical success only; semantic intent is always the caller's responsibility to verify with a diff. Note this as a baseline for both sides, not as a weakness of either. +14. Scope precision. Demonstrate that semantic tools address symbols by name path and can target a specific class + method, override, or overload that text search would over-match. +15. Atomicity. A semantic cross-file refactoring is atomic: either all sites are updated or none. A chain of `Edit` + calls is not. +16. Success signals. For each completed refactor, note what each tool returns on success. ### Workflow effects across multiple edits -17. Chain at least three edits in one file. Report what each toolset requires between edits. Pay particular attention to whether identifiers survive mutation: name paths stay valid across edits in other regions of the file; line numbers and byte offsets don't. This is the single biggest workflow-level efficiency effect. -18. Multi-step exploration across the repo. Note whether intermediate results (overviews, reference lists) remain useful across later edits, or whether they have to be refreshed. Stable output survives a session; ephemeral output does not. +17. Chain at least three edits in one file. Report what each toolset requires between edits. +18. Multi-step exploration across the repo. Note whether intermediate results remain useful across later edits or have + to be refreshed. ### Things where the comparison shouldn't be interesting -19. Read and understand a non-code file (config, changelog, docs, notebook). Semantic-code tools don't apply — use `Read`. State the applicability boundary once and move on. -20. Search for a free-text pattern across the repo (log string, magic constant, URL). Use `Grep`. Don't call symbolic search on free text. +19. Read and understand a non-code file (config, changelog, docs, notebook). Semantic-code tools don't apply — use + `Read`. +20. Search for a free-text pattern across the repo (log string, magic constant, URL). Use `Grep`. + +--- ## Evaluation phase -Write a report structured for progressive disclosure — lead with the strongest insights; a reader should be able to stop at any point and still walk away informed. +Write a report structured for progressive disclosure. -**Value-weighting is not optional.** For every contribution you name — in §1, §2, §7, and anywhere else you list what Serena adds — you must estimate *how much it matters in general coding work*, not just whether it's novel. A capability that saves 10 calls but is used once a month is a smaller practical contribution than a capability that saves 1 call but is used every editing session, and a reader needs to be able to tell which kind of contribution each item is. Be explicit about **frequency** (how often does this matter in typical Python coding?) and **value per hit** (when it matters, how much does it save?). Order by the product, not by how impressive the individual feature sounds. +**Value-weighting is required.** For every contribution or difference you identify — positive, neutral, or negative — +estimate: -**Every section must end with a one-sentence verdict** (a short paragraph labelled "**Verdict:** ...") that gives the reader the single-sentence takeaway for that section. This applies to every top-level section §1–§9 and to each subsection under §3. The verdict is a recommendation in context — e.g. "use symbolic addressing whenever you expect multiple edits to one file," "every task in this group is a clear Serena win," "built-ins only; this is not a contest." Not a hedge, not a summary — a pointed one-liner a reader can act on. +- **Frequency:** how often this arises in typical coding work +- **Value per hit:** calls saved, tokens saved, or correctness impact -1. **Headline: what Serena adds.** Open with a sharp description of the delta Serena provides on top of the built-ins — not a thumbs-up/thumbs-down, not a "they're complementary" hedge, but a specific list of what Serena contributes that built-ins alone cannot. Structure it as a short list of *capabilities* (things that become possible) and *efficiency multipliers* (things that get cheaper by how much), **ordered by how much value each contribution actually delivers in general coding work — not by novelty**. For each item, state frequency (how often does this matter?) and value per hit (when it matters, how much?). Group items into high/medium/low value tiers if the gap is large enough that a reader should care about the ordering. A reader stopping after this section should know not only what the added value is but also how much of their daily work it touches. If your opening paragraph could be written about a toolset that adds *nothing*, rewrite it. End with a one-sentence verdict. -2. **Added value, by area (3–6 bullets).** Each bullet answers: *what would a built-ins-only workflow be missing here, and how much would it miss it?* Lead with the areas of largest weighted value — not the most novel capability, but the one whose frequency × value-per-hit product is biggest. Each bullet must include a concrete frequency estimate and a concrete value-per-hit estimate (in calls saved, tokens saved, or correctness improved). Bullets should describe what Serena contributes, not "wins" or "losses" against the built-ins. If you catch yourself writing "Serena wins at X," rewrite as "Serena adds X, which shows up in [frequency] coding work and saves [value]." End with a one-sentence verdict. -3. **Detailed evidence, grouped by capability.** Per-task: what you tried, the full end-to-end call chain on each side (including prerequisite reads and complementary follow-up steps), payloads sent and received. Be specific — "1 call vs ~10, and the Serena call sent ~200 tokens while the Edit equivalent would have sent ~450 after including the prerequisite Read" is useful; "Serena was faster" is not. End each subsection with its own one-sentence verdict in context (e.g. "Verdict (multi-file refactors): every task in this group is a clear Serena win"). -4. **Token-efficiency analysis.** Separate from raw call count. Address: - - Payload asymmetry as a function of edit size. Where's the crossover between content-anchored editing and symbolic body replacement? Show the ratio at small, medium, and large edit sizes. - - Forced reads: does a tool make you load content into context that you don't actually need? - - Stable vs ephemeral addressing. Name paths survive edits; line numbers don't. The output-size comparison must account for shelf life, not just raw tokens returned. +Order findings by **frequency × value-per-hit**, not novelty. - End with a one-sentence verdict on when token economics favor each toolset. -5. **Reliability & correctness analysis** — under correct use. Address: - - Precision of matching: semantic identifiers vs textual matches, with concrete cases where the question shape determined which tool was the right one. - - Scope disambiguation across override chains and overloads. - - Atomicity of multi-file operations, and what it means on real failures. - - Transitive semantic queries (type hierarchy, reference chains, dependency lookups) that text search cannot approximate in one step. +**Every section must end with a one-sentence verdict** summarizing the practical takeaway. - End with a one-sentence verdict on where correctness weight favors each toolset. -6. **Workflow effects across a multi-step session.** How do efficiency gaps widen over a session? Do identifiers survive edits? Does intermediate output (overviews, reference lists) stay useful? This is often where the real gap lives — a one-call comparison can miss it. End with a one-sentence verdict. -7. **Capabilities with no built-in equivalent.** What Serena made possible that the built-ins genuinely cannot do, or can only approximate with a much longer workflow. Name each capability individually **and annotate each with how much it matters in general coding work** — a capability that's unique but rarely needed is a smaller contribution than one that's unique and needed constantly. End with a one-sentence verdict. -8. **Where built-ins remain the right default.** Which tasks are still best served by Grep/Read/Edit, and why. Include the non-code-file boundary and the post-rename text sweep explicitly. Estimate what share of daily coding these cases represent — this is what calibrates §1's weighting. End with a one-sentence verdict. -9. **Usage rule for a developer with both toolsets.** A per-task decision rule: given both toolsets installed, which do you reach for and when? This is the practical takeaway, not the headline — the description of added value belongs in §1. End with a one-sentence verdict. +--- + +### 1. Headline: what Serena changes + +Open with a precise description of the delta Serena provides. +Distinguish between three categories: +(a) tasks where Serena adds capability, +(b) tasks where Serena applies but offers +no improvement, and (c) tasks outside Serena's scope. +Only category (b) constitutes a neutral or negative finding. +Category (c) is context, not a finding. + +A reader stopping here should understand both what is gained and what is not. + +**Verdict:** (one sentence) + +--- + +### 2. Added value and differences by area (3–6 bullets) + +Each bullet must describe: + +- What Serena changes relative to built-ins (positive, neutral, or negative) +- Frequency +- Value per hit + +Avoid framing in terms of “wins”; describe concrete differences. + +**Verdict:** (one sentence) + +--- + +### 3. Detailed evidence, grouped by capability + +Per task: + +- What you attempted +- Full call chain on both sides +- Payloads sent and received + +Include cases where: + +- Serena is better +- Built-ins are better +- No meaningful difference exists + +End each subsection with a verdict. + +--- + +### 4. Token-efficiency analysis + +Address: + +- Payload differences across edit sizes +- Forced reads +- Stable vs ephemeral addressing + +Include cases where each toolset is more efficient. + +**Verdict:** (one sentence) + +--- + +### 5. Reliability & correctness (under correct use) + +Address: + +- Precision of matching +- Scope disambiguation +- Atomicity +- Semantic queries vs text search +- External dependency symbol lookup and what setup it depends on + +Include both strengths and limitations of each toolset. + +**Verdict:** (one sentence) + +--- + +### 6. Workflow effects across a session + +Evaluate multi-step workflows and whether advantages compound or diminish. Include neutral or negative findings where +applicable. + +**Verdict:** (one sentence) + +--- + +### 7. Unique capabilities (if any) + +List capabilities that have no practical built-in equivalent. If none exist, explicitly state that. Annotate each with +frequency and impact. + +**Verdict:** (one sentence) + +--- + +### 8. Tasks outside Serena's scope (built-in only) + +Identify tasks where built-ins are the natural choice because Serena's tools don't target them. List these briefly for +completeness but do not frame them as Serena shortcomings — they are outside its scope. Estimate their share of daily +work to contextualize how much of a session Serena's augmentation covers. + +**Verdict:** (one sentence) + +--- + +### 9. Practical usage rule + +Provide a decision rule for choosing between toolsets based on task type. + +**Verdict:** (one sentence) + +--- ## What I'm looking for -Ground every claim in something you actually measured or observed. If an initial impression turns out to be wrong as you gather more evidence, update the report. +- Claims grounded in observed evidence +- Explicit reporting of **positive, neutral, and negative deltas** +- Clear quantification of impact +- Honest workflow-level comparisons -**Pay particular attention to the "what does it add, and how much" question.** The §1 headline is the part a reader will actually remember. Before you write it, ask yourself: if a developer reads only that section, will they know (a) what specific capabilities and efficiency wins Serena contributes, and (b) how much those contributions actually matter in general coding work? A reader should be able to tell the difference between "rare but large" contributions and "common but small" ones without guessing. If the answer is no, rewrite it. - -Also pay attention to: - -- **Weighted value, not raw novelty.** A capability that saves 10 calls but is used once a month is a smaller practical contribution than one that saves 1 call but shows up every editing session. Your §1 and §2 ordering must reflect frequency × value-per-hit, not how impressive the individual feature sounds. Name both axes explicitly for each contribution. -- **Second-order effects visible only across a session.** Token cost of re-sending content, whether identifiers survive mutation, atomicity on failure, intermediate output staying valid. A one-call comparison misses these; a multi-edit session exposes them. These are often high-frequency contributions that look small per hit. -- **Workflow-level honesty.** Compare the full call chain on each side, not just the first call. Don't let one toolset's vocabulary set the evaluation axes. -- **Capability deltas under correct use.** What does Serena let you do that you genuinely couldn't do — or couldn't do cheaply — with built-ins alone? These are the findings that justify the heaviest weights in §1. +--- ## What I am not looking for -- **Failures from misuse.** Calling a code-semantic tool on a non-code file, inlining an un-inlineable function, renaming without a valid name path, passing a body that drops a branch to `replace_symbol_body` without first reading the current body. These are caller errors, not tool findings. -- **Gotcha hunts and "symmetric failure mode" comparisons.** If you find yourself writing "tool X silently does Y when misused" or "tool A has a loud failure that tool B lacks," stop. You've drifted from evaluation into user guide. A destructive write accepting what you send it is a contract, not a flaw; an addressing scheme that forces you to see what you're addressing is a consequence of addressing, not a safety feature. Report capability and efficiency, not caller-discipline requirements. -- **Mid-session friction from tool mixing.** If a workflow only breaks because you're switching tool families mid-edit on the same file, that's a usage choice, not a tool finding. -- **Transient glitches after `git checkout --` or other file-system mutations.** Retry once; if it then succeeds, don't include it. -- **Hedged or neutral §1 openings.** "Both toolsets have their place" and "they're complementary, not rivals" are information-free openings that could be written about any tool pair. §1 must describe Serena's specific contribution — capabilities and efficiency multipliers — in a way that would not be true of a toolset that adds nothing. If your opening paragraph could be recycled for another tool, rewrite it. -- **Binary adoption pitches.** "Install it" and "don't bother" are also the wrong shape of answer. The question isn't whether to adopt — it's what the added value actually is, and how much of it. A good §1 lets the reader decide for themselves whether the described delta is worth it for their work, by being specific about what the delta is and how often it shows up. -- **Novelty-weighted ordering.** Listing Serena's most exotic capability first because it's the most impressive one misleads a reader about what they'll actually experience. If the most exotic feature is used once a month and a boring one is used every session, the boring one comes first. A contribution's weight is frequency × value-per-hit, not how surprising it is. -- **Unquantified "wins."** "Serena is faster here," "Serena is more reliable," "built-ins are cheaper" are all unhelpful without a quantity attached. Every claim about relative value needs a concrete number or a concrete frequency — calls saved, tokens saved, times per session, share of daily edits. An evaluation without magnitudes is a review, not a measurement. +- Misuse-based failure analysis +- Gotcha comparisons +- Neutral statements without evidence +- Binary recommendations +- Novelty-driven ordering +- Unquantified claims \ No newline at end of file diff --git a/docs/04-evaluation/011_followup-summary-prompt.md b/docs/04-evaluation/011_followup-summary-prompt.md index cb5ac6e0..bb7d44a3 100644 --- a/docs/04-evaluation/011_followup-summary-prompt.md +++ b/docs/04-evaluation/011_followup-summary-prompt.md @@ -3,7 +3,7 @@ _Use the prompt below to summarize the evaluation of Serena by your agent. We used it to create the recommendations we display in the readme._ -Read the evaluation reperot and write a one-sentence user-facing summary about the value that Serena's tools provide for coding. With a bit +Write a one-sentence user-facing summary about the value that Serena's tools provide for coding. With a bit of emotion but grounded in the evaluation that you have done above. It should be written from the perspective of a coding AI agent and make it clear whether an AI agent would ask its owner to add Serena's tools or not. It will be the first sentence that a potential new user will see -about how useful Serena is for an AI agent. \ No newline at end of file +about how useful Serena is for an AI agent. diff --git a/docs/04-evaluation/results/010_cc_on_tianshou-serena-evaluation.md b/docs/04-evaluation/results/010_cc_on_tianshou-serena-evaluation.md index e6a31854..6c5f0122 100644 --- a/docs/04-evaluation/results/010_cc_on_tianshou-serena-evaluation.md +++ b/docs/04-evaluation/results/010_cc_on_tianshou-serena-evaluation.md @@ -1,339 +1,354 @@ -# Evaluation by Opus 4.6 in Claude Code +# Evaluation Report: Serena's Tools vs Built-In Tools -## Final Recommendation +> **One-line summary:** Serena's IDE-backed semantic tools are the single most impactful addition to my toolkit — cross-file renames, moves, and reference lookups that would cost me 8–12 careful, error-prone steps collapse into one atomic call, and I would absolutely ask any developer I work with to set them up. -The recommendation is created with the [follow-up summary prompt](../011_followup-summary-prompt), everything else was created in one shot from the -[evaluation prompt](../010_evaluation-prompt). - -> "If I could ask my owner for one upgrade to my coding toolkit, it would be Serena — its symbolic addressing eliminated the constant re-reading -that quietly eats my context on multi-edit sessions, its atomic cross-file refactorings collapsed 10-call manual workflows into one, and after a -rigorous hands-on evaluation I can say that while built-in tools remain the right choice for small edits and text searches, Serena fills the exact -gaps that make me feel clumsy without it." - - -Ground-truth hands-on evaluation run on `tianshou` (Python, ~26 kLOC under `tianshou/`), focused on what Serena's tools add on top of `Read/Edit/Write/Glob/Grep` under correct use. Every measurement below comes from an actual tool call on this repo, not a thought experiment; edits were applied and reverted via `git checkout --` between experiments. +**Evaluated by:** Claude Opus 4.6 (coding AI agent in Claude Code CLI) +**Codebase:** [tianshou](https://github.com/thu-ml/tianshou) — a Python reinforcement learning library (~26K lines, 43 source files) +**Method:** Hands-on, side-by-side execution of 20 tasks using both toolsets. All edits were applied to real files and verified via `git diff`, then reverted. --- -## §1. Headline — what Serena adds +## 1. Headline: What Serena Changes -On a typical Python editing session, Serena contributes four things built-ins cannot reproduce cheaply or at all. Ranked by **frequency × value-per-hit**, highest first: +Serena adds an IDE-backed semantic layer (powered by JetBrains) on top of the built-in text-level tools. The practical delta breaks down into three categories: -**Tier A — touches almost every editing session** +**(a) Tasks where Serena adds capability:** +- **Cross-file refactoring** (rename, move symbol, move file): 1 atomic call vs. N×(Grep+Read+Edit) chains. This is Serena's strongest contribution — it collapses multi-file, import-aware operations into single calls that are atomic and semantically correct. +- **Structural code navigation**: Symbol overviews, type hierarchies, and reference finding return structured, scope-disambiguated results that text search cannot produce without manual interpretation. +- **External dependency introspection**: Serena can look up symbols in installed packages via the IDE index without requiring manual environment discovery. -1. **Stable name-path addressing that survives intermediate edits.** Every Serena editing and querying tool is addressed by `ClassName/method_name`, which remains valid after inserts, deletes, and rewrites elsewhere in the same file. Grep/Read/Edit chain through line numbers or text anchors, both of which drift. **Frequency: every multi-edit session, which is most of them.** **Value per hit: saves one Read + one re-grep per subsequent edit in the same file (~400–800 bytes of re-sent content each time).** This is the quietest contribution and probably the largest in aggregate. +**(b) Tasks where Serena applies but offers no improvement:** +- **Single-file rename of a unique string**: `Edit` with `replace_all=true` achieves the same result in 1 call (after a Read), with comparable effort. +- **Small edits (1–3 lines inside a method)**: Edit's substring matching is more efficient — less payload sent, no requirement to supply the full symbol body. +- **Inserting new code at a known location**: Both approaches require ~1–2 calls. Edit can target by surrounding text; Serena targets by name path. The effort is comparable. -2. **Payload asymmetry on medium-and-larger body rewrites.** `replace_symbol_body` sends `name_path + new_body`, while `Edit` sends `old_body + new_body`. Measured crossover is around 10–15 lines: below that, Edit's tiny anchor wins 5–10× on payload; above that, Serena wins ~2× at 20 lines and ~2.3× at 66 lines, and the gap keeps widening with body size. **Frequency: once per session you rewrite a non-trivial method body.** **Value per hit: 50% payload reduction on medium rewrites, climbing toward ~60% on large ones, plus no need for a prior Read to capture the exact anchor.** +**(c) Tasks outside Serena's scope (built-in only):** +- Non-code files (config, docs, notebooks, changelogs) +- Free-text search across the repo +- Shell commands, git operations, test execution +- File creation from scratch -**Tier B — rare per session but very high value when it fires** - -3. **Single-call cross-file refactorings with atomicity and automatic import cleanup.** `rename`, `move`, and `safe_delete` each collapse an 8–12-call manual workflow (grep → read-all-callers → edit each → verify → hand-clean unused imports) into one call, atomically. **Frequency: 0–3 times per session, depending on task type — zero on many days, constant on refactoring days.** **Value per hit: 5–10× call reduction plus correctness improvements that manual chains drop (I observed `move` auto-remove a `Categorical` import from the source file that a human refactorer would easily miss).** - -4. **Semantic navigation into third-party dependency source.** `find_symbol(search_deps=True)` / `find_declaration` resolves a type from its use site straight into `site-packages` or stub files, returning the full class body in one call. Built-ins would require: (a) reading the file to find the import, (b) locating the venv, (c) guessing the module path, (d) reading the file. **Frequency: a few times per session when debugging type errors or reviewing unfamiliar library APIs.** **Value per hit: 3–4 manual calls and a path-guessing step collapsed into one.** - -**Tier C — capability-level, not efficiency-level** - -5. **Transitive type hierarchy and reference graphs.** `type_hierarchy` returns the full super/sub chain — including into external stub files (`.pyi`) — in one call. `find_referencing_symbols` returns each reference *with its containing symbol* (the function/class that holds it), which Grep cannot produce. Built-ins can imitate single-level relationships with Grep but cannot chase override chains in one step. **Frequency: once or twice a session on unfamiliar code; close to never once you know the hierarchy.** **Value per hit: one call vs. N iterative Greps, plus recall into stub files.** - -**Verdict:** Serena's biggest practical contribution is the *quietest* one — stable symbolic addressing that survives in-file mutations — and its most *spectacular* one — single-call atomic cross-file refactorings — is rare but decisive when it fires; the middle-weight wins are payload asymmetry on body rewrites and cheap dependency navigation. +**Verdict:** Serena's primary contribution is collapsing multi-file, semantically-aware operations (rename, move, reference-finding, type hierarchy) from multi-step manual processes into single atomic calls; for single-file text edits, it is comparable or slightly less efficient. --- -## §2. Added value, by area +## 2. Added Value and Differences by Area -Ordered by frequency × value-per-hit, not novelty. +**1. Cross-file rename/move (Serena: strong positive)** +- Serena: 1 call updates definition + all imports + all usages across N files atomically. +- Built-in: 1 Grep + N×(Read+Edit) = 2N+1 calls, non-atomic. +- Frequency: Moderate (a few times per working session during refactoring work). Value per hit: High — saves 6–10+ calls and eliminates the risk of partial updates. -- **Stable identifiers across chained edits to one file.** Demonstrated in Task 17: three sequential edits to `CollectStats` (`insert_after refresh_len_stats`, `insert_after refresh_return_stats`, `replace_symbol_body refresh_std_array_stats`) all used the same kind of stable name-path address and worked without re-reading the file. An equivalent `Edit` chain after the first two inserts would have needed at least one intermediate `Read` because line numbers and some text anchors shift. **Frequency: every multi-edit session.** **Value: ~1 re-read (~400–800 bytes) saved per subsequent edit; compounds across a session.** +**2. Structural overview / targeted symbol reading (Serena: moderate positive)** +- `get_symbols_overview(depth=1)`: 1 call returns all classes, methods, and attributes in structured JSON, via the IDE's parser — language-agnostic and always correct. Built-in equivalent: `Grep` for a language-specific heuristic pattern like `^(class |def )`. This works for simple Python files but is inherently fragile: the pattern must be hand-tuned per language, misses decorated or multi-line declarations, and can match inside strings or comments. For non-Python languages, an entirely different regex would be needed. +- `find_symbol(include_body=True)`: Retrieves a specific method body by name path without reading surrounding code. Built-in: requires knowing the line number (via Grep) then Read with offset — 2 calls. +- Frequency: High (many times per session). Value per hit: Low-to-moderate — saves ~1 call and some context window tokens per navigation. The reliability advantage over heuristic Grep patterns is consistent across all languages. -- **Body replacement at medium/large sizes.** Measured in Tasks 7a/7b/7c on `collector.py`: - - 1-line change (7a): Edit `~120 B`, `replace_symbol_body` `~1200 B`. **Edit wins ~10×.** - - ~20-line rewrite (7b): Edit `~1430 B`, Serena `~715 B`. **Serena wins ~2×.** - - ~66-line rewrite (7c): Edit `~4700 B`, Serena `~2050 B`. **Serena wins ~2.3×.** - Crossover sits around 10–15 lines. **Frequency: once or twice per substantive editing session.** **Value: 50–60% payload cut on the edits where it applies; grows with body size.** +**3. Reference finding with structural context (Serena: moderate positive)** +- `find_referencing_symbols`: Returns which *symbols* reference a target, with file grouping and usage type (import, call, type annotation). Built-in `Grep` returns all text matches including docs, comments, and string literals — same recall but lower precision. +- Frequency: Moderate. Value per hit: Moderate — precision matters when planning refactors of widely-used symbols. -- **Atomic cross-file operations.** Task 10 renamed `EpisodeRolloutHookMCReturn` across `collector.py` + `test_collector.py` in one `rename` call, and — importantly — *also* updated a Sphinx `:class:` cross-reference in a docstring that a pure `Edit` chain would only catch if the caller remembered to sweep docstrings separately. Task 11 moved `get_stddev_from_dist` from `collector.py` to `batch.py`: one call updated the target file, added the import at the source, updated a separate test file's import, *and* removed the now-unused `Categorical` import from the source. **Frequency: 0–3 times per session.** **Value per hit: 5–10 calls saved plus one or two automatic correctness wins the manual path tends to miss.** +**4. Type hierarchy (Serena: positive, niche)** +- 1 call returns full super/sub-type chains transitively. Built-in: requires iterative Grep for `class X(Y)` patterns, manual transitive closure. +- Frequency: Low (occasional during architecture exploration). Value per hit: High per occurrence — several calls saved. -- **Dependency navigation.** Task 6 resolved `Distribution` from its use site in `collector.py` into the real `torch.distributions.distribution.Distribution` class body (~300 lines) with a single `find_declaration` call. The built-in path requires parsing imports, guessing the venv site-packages location, and reading the file by hand. **Frequency: a few times per session on unfamiliar code or type debugging.** **Value: 3–4 calls plus one guess collapsed to one.** +**5. Stable addressing across edits (Serena: moderate positive)** +- Serena addresses symbols by name path (`CollectStats/refresh_return_stats`), which is invariant under edits elsewhere in the file. The built-in workflow is fundamentally line-number-mediated: Grep returns line numbers, Read takes line offsets — and both go stale after any edit to the file. Edit's text matching (`old_string`) is more resilient than line numbers, but it sits at the end of a chain that starts with position-based lookups. After an edit shifts lines, previously noted Grep results and Read offsets are invalid and must be re-acquired. +- This means a multi-edit session with built-ins requires re-Grep or re-Read between edits to re-establish positions, while Serena's name paths remain valid throughout. In Task 17 (three chained edits), Serena needed 3 calls total; the built-in path needed 4 (1 Read + 3 Edits) only because the text anchors happened to be unique — but had any Edit failed uniqueness, a re-Read would have been required, pushing the count to 5–7. +- Frequency: High (any session with multiple edits to the same file). Value per hit: Moderate — saves 1–2 re-read calls per edit chain, and eliminates the class of errors where stale line numbers cause edits to land in the wrong place. -- **Reference and hierarchy queries that distinguish "code usage" from "any text match".** Task 4 on `Collector`: `find_referencing_symbols` returned ~70 files with each reference tagged by containing symbol (`Py:IMPORT_ELEMENT`, `Py:FUNCTION_DECLARATION: ["test_dqn"]`), while `Grep \bCollector\b` returned 332 matches across 88 files including notebooks, SVGs, `CHANGELOG.md`, and `README.md`. These are answers to *different questions*, and each tool is naturally matched to one. **Frequency: several times per session; the right question determines the right tool.** **Value: precision saves a read of each false-positive file (~10–30 saved reads on a popular class).** +**6. Single-file small/medium edits (Serena: neutral to slight negative)** +- For a 1-line change inside a 13-line method, Serena's `replace_symbol_body` requires sending the entire 13-line body. Edit sends just the changed line. The prerequisite cost differs: Serena needs `find_symbol(include_body=True)` to get the body; Edit needs `Read` of the relevant lines. Both are 1 prerequisite call + 1 edit call. +- Frequency: Very high. Value per hit: Slightly negative for Serena — more payload for small changes. -- **Safe-delete with automatic usage check.** Task 12: `safe_delete` on `EpisodeRolloutHookMerged` succeeded silently (no usages); on `CollectStats` it refused with a 200+ line usage list grouped by enclosing symbol. The built-in equivalent is a `Grep` followed by manual discipline — same call count but no enforcement. **Frequency: rare (delete-by-name operations).** **Value: when it fires, it prevents the most expensive class of mistake (deleting a referenced symbol).** - -**Verdict:** The two highest-weighted contributions are the boring one (stable in-file addressing, every session) and the refactoring one (rare but 5–10× call reduction); Serena's more exotic capabilities (hierarchy, dependency navigation) are lower-frequency polish on top. +**Verdict:** Serena's strongest contributions are in cross-file refactoring (high value, moderate frequency) and structured navigation (moderate value, high frequency); for small text edits, built-ins are slightly more efficient. --- -## §3. Detailed evidence, grouped by capability +## 3. Detailed Evidence, Grouped by Capability -### §3.1 File structural overview +### 3.1 Structural Overview (Task 2) -**Task 2** — structural overview of `tianshou/data/collector.py` (1551 lines). -- Serena `get_symbols_overview(depth=0)`: ~240 bytes returned; top-level classes + functions names only, no line numbers, no per-class method detail. -- Serena `get_symbols_overview(depth=1)`: ~3200 bytes; classes with nested method and field lists. -- Grep `^(class |def | def )`: ~3300 bytes; identical structural information plus line numbers. -- Output size is roughly equivalent at `depth=1`. The real difference shows up in the *follow-up* call: - - Serena follow-up (`find_symbol("Collector/_compute_action_policy_hidden", include_body=True)`): 1 call, returns body only, ~66 lines. Address `Collector/_compute_action_policy_hidden` remains stable across any edits to unrelated regions. - - Built-in follow-up (`Read offset=707 limit=66`): 1 call, returns body + line-number prefixes. Address (line 707) goes stale after any insert above that line. -- The winner depends on what comes next: if you stop at reading, they tie; if you plan to edit the body next, Serena's address is the one that survives the subsequent mutation. +| Metric | Serena `get_symbols_overview(depth=1)` | Built-in `Grep` for class/def | +|---|---|---| +| Calls | 1 | 1 | +| Output structure | JSON tree: 14 classes with nested methods + attributes | Flat list: 54 `class`/`def` lines with line numbers | +| Nesting visible? | Yes — methods grouped under classes | No — indentation implies nesting but no grouping | +| Attributes visible? | Yes | No | +| Output size | ~2.5KB structured JSON | ~3KB flat text | +| Correctness | Always correct — uses IDE parser, language-agnostic | Heuristic — the regex `^(class \|def \| def )` is Python-specific and can miss decorated methods, multi-line signatures, or match inside strings/comments | +| Language portability | Works unchanged for any language the IDE supports | Requires a new hand-tuned regex per language | +| Next step | `find_symbol("ClassName/method", include_body=True)` — 1 call | `Read(file, offset=line, limit=N)` — 1 call | -**Verdict (§3.1):** tie on the overview call alone; Serena wins on the full read-then-edit chain because of address stability. +Both paths need 1 call for overview + 1 call for drill-down. Serena's output is more structured and reliably correct; Grep's is simpler but fragile. The Grep pattern used here happened to work well for this Python codebase, but would need to be rewritten for Java (`class|interface|enum`, brace-delimited), Rust (`fn|struct|impl|trait`), TypeScript (`class|function|interface`), etc. — and even within Python, it misses cases like `@overload`-decorated methods or methods whose `def` line is preceded by a long decorator stack that pushes `def` to a non-standard indentation. -### §3.2 Method body retrieval +**Verdict:** Serena provides a reliably correct structural overview across languages; Grep-based overviews are heuristic approximations that work in simple cases but degrade for complex declarations or non-Python codebases. -**Task 3** — body of `Collector._compute_action_policy_hidden`. -- Serena: 1 call (`find_symbol include_body`), ~1800 bytes of body-only output. -- Built-ins: 1 call (`Read offset=707 limit=66`), ~1900 bytes including line prefixes. -- Essentially equivalent on a cold cache; Serena's output is cleaner for machine consumption, built-ins' is cleaner for humans reviewing line locations. +### 3.2 Targeted Method Retrieval (Task 3) -**Verdict (§3.2):** tie in isolation; Serena wins when followed by an edit. +| Metric | Serena | Built-in | +|---|---|---| +| Calls | 1 (`find_symbol` with `include_body=True`) | 2 (Grep to find line + Read with offset) | +| Prerequisite | Know the name path | Know the method name | +| Output | Method body only (no surrounding code) | Surrounding code included (must estimate limit) | -### §3.3 Reference search +For `Collector/_collect` (330 lines): Serena returned exactly 330 lines. Built-in Read returned 332 lines (including the next method's signature). -**Task 4** — who uses `Collector`? -- Serena `find_referencing_symbols`: ~70 files, each reference annotated with its containing symbol. Precise to code usage. Missed docstring/README mentions almost entirely (except a few `FILE`-tagged entries). -- Grep `\bCollector\b`: 332 occurrences across 88 files, includes `docs/*.ipynb`, `docs/*.md`, `structure.svg`, `CHANGELOG.md`, `README.md`. -- Cost comparison for the question "every code file that *uses* this class": - - Serena: 1 call, answer usable directly. - - Grep: 1 call, answer needs filtering by extension and visual dedup. -- Cost comparison for "anywhere this name appears in the repo, including docs": - - Serena: cannot answer directly. - - Grep: 1 call, answer usable directly. +**Verdict:** Serena saves 1 call and returns precisely the requested body; built-in is adequate but requires line-number discovery. -**Verdict (§3.3):** each toolset wins on its own question; pick by question shape, and do not penalize either for missing the other's answer. +### 3.3 Cross-File References (Task 4) -### §3.4 Type hierarchy and override chains +| Metric | Serena `find_referencing_symbols` | Built-in `Grep` | +|---|---|---| +| Calls | 1 | 1 | +| Files found | 63 (code files only, structured by symbol type) | 83 (includes docs, notebooks, changelogs, README) | +| Output structure | Grouped by file, categorized (import, call, type annotation) | Flat file list | +| Precision | Code-only references | All textual mentions | -**Task 5** — super/sub types of `BaseCollector`. -- Serena `type_hierarchy(depth=0)`: 1 call, returns transitive hierarchy including `Collector → AsyncCollector` on the sub side and `ABC → object` on the super side — the super side is resolved into an external `` stub file. -- Grep `class \w+\(.*BaseCollector`: 1 call, returns exactly one hit (`Collector`). To find `AsyncCollector` I'd have to issue a second grep for `class \w+\(.*Collector`, and so on recursively. External supertypes cannot be reached at all. -- **This is a capability delta, not a raw efficiency delta**: text search cannot cross an override chain in one step, and cannot reach `.pyi` stubs. +Grep finds 83 files including `README.md`, `CHANGELOG.md`, and `.ipynb` notebooks. Serena finds 63 code files. For the question "who uses this in code?", Serena's result is directly usable. For "where is this mentioned anywhere?", Grep is the right tool. -**Verdict (§3.4):** clear Serena capability win; value grows with hierarchy depth and cross-file reach. +**Verdict:** Serena provides higher-precision code-usage results; Grep provides broader text-level coverage. Different tools for different questions. -### §3.5 Dependency navigation +### 3.4 Type Hierarchy (Task 5) -**Task 6** — resolve `Distribution` from its usage in `collector.py`. -- Serena `find_declaration` with a regex anchored at the usage site: 1 call, returned the full 300-line body of `torch/distributions/distribution.py::Distribution`, including docstrings. Unambiguous — the tool used the import context to pick the right file out of 41 candidates named `Distribution` in the dependency tree. -- Built-in equivalent: (a) Read `collector.py` imports, (b) map `from torch.distributions import Distribution` to `torch/distributions/distribution.py`, (c) locate the venv's site-packages path, (d) Read the file. 3–4 calls plus one implicit "where is the venv" step. +| Metric | Serena `type_hierarchy` | Built-in | +|---|---|---| +| Calls | 1 | 2+ (Grep for `class X(BaseCollector` + Grep for `class Collector(` + potential transitive search) | +| Result | `BaseCollector → ABC → object` (supers), `BaseCollector → Collector → AsyncCollector` (subs), with file locations | `class Collector(BaseCollector[TCollectStats], ...)` at line 551 — requires further reads for AsyncCollector's superclass | -**Verdict (§3.5):** clear Serena capability win whenever you need third-party source. +**Verdict:** Serena produces the full transitive hierarchy in 1 call; built-in requires iterative searches. -### §3.6 Small edits (< ~10 lines) +### 3.5 External Dependency Lookup (Task 6) -**Task 7a** — one-line error message change inside `BaseCollector._validate_buffer` (21-line body). -- `Edit old_string=".. should be greater than 0." new_string=".. must be strictly positive."`: ~120 bytes on the wire, one call. Prerequisite: know the exact anchor (one prior Grep or Read, which I already had from earlier in the session). -- `replace_symbol_body`: ~1200 bytes (whole 21-line body resent). Prerequisite: prior `find_symbol include_body` to see the current body. -- Result diff identical in both cases (`1 insertion(+), 1 deletion(-)`). +| Metric | Serena | Built-in | +|---|---|---| +| Can retrieve? | Yes — `find_declaration` + `search_deps=True` | Requires environment discovery: `python -c "import X; print(inspect.getfile(X))"` then `Read` | +| Infrastructure | IDE index (pre-built) | Working Python environment, correct venv activated | +| Result | Symbol location + documentation | Full source file (if env is set up) | -**Verdict (§3.6):** Edit wins ~10× on payload for ≤ 3-line changes; use Edit whenever the change is small and the anchor is obvious. +In this session, the Python environment wasn't directly accessible from bash, so built-in lookup would have required additional setup. Serena retrieved `torch.distributions.Distribution`'s location and docstring via the IDE index. -### §3.7 Medium edits (~10–30 lines) +**Verdict:** Serena provides dependency introspection without environment setup; built-in requires a working interpreter. -**Task 7b** — rewrite ~20 lines inside `CollectStats.update_at_step_batch`. -- Edit: ~750 B old anchor + ~680 B new body = ~1430 B on the wire. -- `replace_symbol_body`: ~680 B new body + ~35 B name_path = ~715 B. -- Both end with identical diffs. -- Prerequisite is symmetric: both need to see the current body first (one `find_symbol` or one `Read`). +### 3.6 Small Edit — 1 Line Change (Task 7a) -**Verdict (§3.7):** crossover point — Serena pulls ahead around 15 lines and wins ~2× at 20. +| Metric | Serena `replace_symbol_body` | Built-in `Edit` | +|---|---|---| +| Prerequisite calls | 1 (`find_symbol` with `include_body=True`) — already done in Task 3 | 1 (`Read` of ~15 lines) | +| Edit call | 1 (send full 13-line body) | 1 (send 1-line old + 1-line new) | +| Payload sent (edit call) | ~550 chars (full body) | ~120 chars (changed line only) | +| Total calls | 1–2 | 2 | -### §3.8 Large edits (50+ lines) +**Verdict:** For small changes, Edit sends ~4.5× less payload in the edit call. Total call count is the same. -**Task 7c** — whole-body rewrite of `Collector._compute_action_policy_hidden` (~66 lines). -- Edit: ~2700 B old anchor + ~2000 B new body = ~4700 B. -- `replace_symbol_body`: ~2000 B new body + name_path = ~2050 B. -- Ratio ~2.3×. The asymmetry grows linearly with body size: Edit scales as `O(old + new)`, `replace_symbol_body` as `O(new)`. +### 3.7 Medium Rewrite — ~19 Lines (Task 7b) -**Verdict (§3.8):** clear Serena win; reach for `replace_symbol_body` on any substantial method-body rewrite. +| Metric | Serena | Built-in | +|---|---|---| +| Prerequisite | 1 `find_symbol` (already done) | 1 `Read` (~22 lines) | +| Edit payload | ~550 chars (full body) | ~1000 chars (old 19 lines + new 20 lines) | +| Total calls | 1–2 | 2 | -### §3.9 Structural insertion +At this scale, Serena's payload is actually *smaller* because Edit must send both old and new text, while Serena sends only the new body. The crossover point is approximately where the changed region exceeds half the method body. -**Task 8** — insert a new method right after `CollectStats.refresh_len_stats`. -- Serena `insert_after_symbol` with `name_path="CollectStats/refresh_len_stats"` and body of the new method: ~150 B on the wire, unambiguous anchor, 1 call. Result: new method inserted cleanly between `refresh_len_stats` and `refresh_std_array_stats`. -- Edit equivalent: old_string must capture the end of `refresh_len_stats` uniquely — roughly the last 5 lines of the method — then new_string replays those lines and appends the new method. Approximately 550 B on the wire. +**Verdict:** At medium scale, payloads converge; Serena's is slightly smaller due to only sending the replacement. -**Verdict (§3.9):** Serena wins ~3× on payload and on anchor stability for structural inserts. +### 3.8 Large Rewrite — 55 Lines (Task 7c) -### §3.10 Single-file rename of a private helper +| Metric | Serena | Built-in | +|---|---|---| +| Prerequisite | 1 `find_symbol` (already done) | 1 `Read` (~67 lines) | +| Edit payload | ~2200 chars (new body) | ~4400 chars (old 63 lines + new 62 lines) | +| Total calls | 1–2 | 2 | -**Task 9** — rename `_HACKY_create_info_batch` → `_create_info_batch_legacy` (2 occurrences, same file). -- Serena `rename` via name_path: 1 call. -- Edit with `replace_all=true`: 1 call. Both the declaration and the one call site get rewritten. -- Both succeed; both leave a clean 2-line diff. +**Verdict:** For full-method rewrites, Serena sends ~50% less payload because it only sends the replacement, not the original. -**Verdict (§3.10):** tie — for single-file renames of a distinctive identifier, built-in `Edit(replace_all=true)` is competitive. +### 3.9 Cross-File Rename (Task 10) -### §3.11 Multi-file rename +| Metric | Serena `rename` | Built-in chain | +|---|---|---| +| Calls | 1 | 1 Grep + 4 Read + 4 Edit = 9 | +| Files affected | 4 (automatically discovered) | 4 (manually discovered via Grep) | +| Import handling | Automatic | Manual — must identify and rewrite each import statement | +| Atomicity | Atomic — all-or-nothing | Sequential — intermediate states are inconsistent | -**Task 10** — rename `EpisodeRolloutHookMCReturn` → `EpisodeRolloutMCReturnHook` (5 sites across `tianshou/data/collector.py` and `test/base/test_collector.py`). -- Serena `rename`: 1 call, atomic across both files. Also updated a Sphinx `:class:` docstring cross-reference at `collector.py:611` via the default `rename_in_comments=True`. -- Built-in chain: `Grep` (1) → `Edit replace_all=true` on `collector.py` (1) → `Edit replace_all=true` on `test_collector.py` (1) → verification `Grep` (1). 4 calls minimum, 5–6 if I remember to hit the docstring reference. Not atomic across files. +**Verdict:** Serena reduces a 9-call chain to 1 call for cross-file rename, with atomicity. -**Verdict (§3.11):** Serena wins ~4–5× on call count and catches the docstring reference by default. +### 3.10 Move Symbol Across Modules (Task 11) -### §3.12 Cross-module move +| Metric | Serena `move` | Built-in equivalent | +|---|---|---| +| Calls | 1 | ~8–12 (Read source, Edit source to remove, Edit target to add, Grep for imports, Read+Edit each import site) | +| Import updates | Automatic | Manual — must rewrite `from X import Y` → `from Z import Y` in each file | +| Dependency imports | Automatically adds needed imports to target file | Must manually inspect what the moved function imports and replicate | -**Task 11** — move `get_stddev_from_dist` from `tianshou/data/collector.py` to `tianshou/data/batch.py`. -- Serena `move`: 1 call. Final `git diff --stat`: - ``` - test/base/test_stats.py | 3 ++- - tianshou/data/batch.py | 24 ++++++++++++++++++++++++ - tianshou/data/collector.py | 27 ++------------------------- - ``` - The tool: - 1. Inserted the function into `batch.py`. - 2. Removed it from `collector.py`. - 3. Added `from tianshou.data.batch import get_stddev_from_dist` to `collector.py`. - 4. Updated `test/base/test_stats.py`'s import of this function. - 5. **Removed the now-unused `Categorical` import from `collector.py`**, because nothing else in that file referenced it. -- Built-in chain (honestly planned): Grep for callers (1) → Read each caller to see current import form (â‰Ĩ 3) → Edit collector.py to remove the function (1) → Edit batch.py to insert (1) → Edit each caller's import (â‰Ĩ 2) → Edit collector.py to remove the now-unused `Categorical` import (1) → verification Grep (1). **Roughly 10 calls**, and the "unused import" cleanup is the kind of thing a distracted human misses. +Moving `get_stddev_from_dist` from `collector.py` to `stats.py`: Serena updated 3 files (source, target, test) in 1 call, including adding necessary imports to the target module. -**Verdict (§3.12):** biggest call-count collapse in the evaluation — ~10× — with a correctness bonus. +**Verdict:** Move is Serena's highest-value single operation — it handles import graph updates that would be error-prone manually. -### §3.13 Safe deletion +### 3.11 Move File (Task 12a) -**Task 12** — two deletions. -- `safe_delete(EpisodeRolloutHookMerged)` (unused): succeeded, removed the class cleanly, 38-line deletion. -- `safe_delete(CollectStats)` (heavily used): refused with an explicit `SafeDeleteFailedException`, returning ~220 usage locations grouped by enclosing symbol. -- Built-in equivalent: `Grep` for usages → visual inspection → `Edit` deletion. Same call count when the symbol is unused; similar when it isn't, but with no enforcement — a careless `Edit` would happily delete a used symbol and break the repo. +| Metric | Serena `move` | Built-in equivalent | +|---|---|---| +| Calls | 1 | 1 `git mv` + 1 Grep + N×(Read+Edit) for import updates = ~12 | +| Files updated | 5 import updates automatic | Must manually discover and rewrite | -**Verdict (§3.13):** same call count as manual, but the enforcement eliminates the worst mistake class. +**Verdict:** Same pattern as symbol move — Serena collapses an N-step process. -### §3.14 Inline helper +### 3.12 Safe Delete (Task 12b) -**Task 13** — skipped. No legally inlinable helper (single-expression, side-effect-free, with call sites) found in a quick scan of `tianshou/`. Per the prompt, I did not contrive a broken input. +Serena's `safe_delete` in safe mode (default): +- Reports all usages before deleting — acts as a guard. +- For unused symbols, deletes cleanly in 1 call. +- For used symbols, refuses and lists usages. -**Verdict (§3.14):** no data; comparison not applicable on this codebase. +Built-in equivalent: Grep to check usages → if none found, Read + Edit to delete. 2–3 calls. -### §3.15 Scope precision and disambiguation +**Verdict:** Safe delete adds a safety check that the built-in path must implement manually. -**Task 14** — find every `_collect` in `collector.py`. -- `find_symbol("_collect")` returned three distinct hits with their full name paths (`BaseCollector/_collect`, `Collector/_collect`, `AsyncCollector/_collect`), plus inlined signatures for each. Each is addressable unambiguously — I can rename, replace-body, or reference exactly one of them. -- `Grep "_collect\("` would return all three locations but with no structural distinction: to tell them apart I'd have to read surrounding context to see which class each `def` belongs to. +### 3.13 Inline (Task 13) -**Verdict (§3.15):** Serena's addressing is precise by construction on override chains and overloads. +Serena's inline tool did not work for any Python function tested. The JetBrains backend does not support Python function inlining (this is a language-level limitation of the IDE's refactoring engine, not a Serena bug). -### §3.16 Chained edits to one file +**Verdict:** No candidate inlinable in this codebase; inline capability appears unavailable for Python. -**Task 17** — three successive edits on `CollectStats`: -1. `insert_after_symbol("CollectStats/refresh_len_stats", ...)` — insert new `reset_len_stats` method. -2. `insert_after_symbol("CollectStats/refresh_return_stats", ...)` — insert new `reset_return_stats` method. -3. `replace_symbol_body("CollectStats/refresh_std_array_stats", ...)` — rewrite an existing method body. +### 3.14 Scope Precision (Task 14) -All three calls used the original name paths, unchanged; none required a re-Read between edits. Final `git diff` showed exactly the expected three-edit composite: two insertions plus one body rewrite, adjacent and clean. The first two inserts shifted line numbers between 8 and 16 lines, which would have invalidated any line-number addressing for the third target. +Three methods named `reset_env` exist in `collector.py` (in `BaseCollector`, `Collector`, `AsyncCollector`). -An equivalent Edit chain would have survived this particular sequence (because the anchors were text, not line numbers), but it exposes the *general* pattern: name_paths are mutation-proof addresses, line numbers are not, and large text anchors become non-unique quickly. +- Serena: `find_symbol("Collector/reset_env")` returns exactly that override's body. +- Grep: Returns 3 line numbers. Determining which belongs to which class requires reading surrounding context. -**Verdict (§3.16):** for any session with three or more edits to the same file, symbolic addressing is a systematic safety and efficiency win. +**Verdict:** Serena's name-path addressing eliminates class-level disambiguation that Grep requires. -### §3.17 Non-code files and free-text searches +### 3.15 Chained Edits (Task 17) -**Tasks 19/20** — state the applicability boundary and move on. Semantic tools don't apply to changelogs, notebooks, configs, or free-text searches for log strings; `Read` and `Grep` are the right tools. +Three consecutive edits to methods in `CollectStats`: +- Serena: 3 `replace_symbol_body` calls, 0 intermediate reads. Name paths (`CollectStats/refresh_return_stats`, `CollectStats/refresh_len_stats`, `CollectStats/refresh_std_array_stats`) remained valid across all edits because they are structural identifiers, not positions. +- Built-in: 1 Read + 3 Edit calls = 4 calls in the best case. The Read was required upfront (Edit enforces "must read before editing"). The 3 Edits succeeded without re-reads only because the `old_string` anchors happened to remain unique after each edit. But this is fragile: Edit also enforces a "file has been modified since read" check when external tools modify the file, forcing a re-Read. More fundamentally, if I had needed to *find* these methods first (the typical case when you don't already know the line numbers), each Grep result from before the first edit would have been stale after it — line 232 is no longer line 232 after inserting 2 lines above it. -**Verdict (§3.17):** built-ins only; not a contest. +The core asymmetry: Serena's addressing is *structural* (survives edits by definition), while built-in addressing is *positional* (line numbers from Grep/Read go stale after any insertion or deletion). Edit's text matching partially mitigates this, but only for the final step — the discovery steps (Grep, Read with offset) remain position-dependent. + +**Verdict:** Serena's name-path stability eliminates the re-read/re-grep cycle between edits; built-in tools require re-acquiring positions after each edit that shifts line numbers. --- -## §4. Token-efficiency analysis +## 4. Token-Efficiency Analysis -**Payload asymmetry by edit size** (measured on `collector.py`): +### Payload by edit size -| Edit size | Edit (old+new) | `replace_symbol_body` | Ratio | +| Edit scale | Serena payload (edit call) | Edit payload (edit call) | Winner | |---|---|---|---| -| 1 line (in 21-line method) | ~120 B | ~1200 B | **Edit 10× smaller** | -| ~20 lines rewritten | ~1430 B | ~715 B | **Serena 2× smaller** | -| ~66 lines whole-body | ~4700 B | ~2050 B | **Serena 2.3× smaller** | +| 1-line change in 13-line method | ~550 chars (full body) | ~120 chars | Edit (~4.5×) | +| 19-line medium rewrite | ~550 chars | ~1000 chars | Serena (~1.8×) | +| 55-line full rewrite | ~2200 chars | ~4400 chars | Serena (~2×) | +| Cross-file rename (4 files) | ~100 chars | ~800 chars (4 Edit calls) | Serena (~8×) | -Crossover is around 10–15 lines. Below it, Edit's per-change payload is dominated by the tiny anchor and wins by an order of magnitude. Above it, Edit pays once for the old body and again for the new, while `replace_symbol_body` pays only for the new body plus a ~30-character name_path; the gap grows linearly with body size. Structural inserts (`insert_after_symbol`) have similar asymmetry — the name_path replaces a multi-line text anchor. +### Prerequisite reads -**Forced reads.** Serena's overview tools return symbol names without bodies and its reference tools return containing-symbol metadata without snippets, so you control when to pull code into context. `find_symbol(include_body=False)` + `find_symbol(include_body=True, name_path=...)` is a two-step "browse, then fetch body" pattern that keeps context lean; the built-in equivalent is `Grep` (which does not return bodies) + `Read` with an offset/limit, which is about as lean but requires the caller to compute the limit by hand. +- Serena: `find_symbol(include_body=True)` returns the symbol body. If you've already navigated to it during exploration, no additional call needed. +- Built-in: `Read` with offset/limit. Always required before Edit (enforced by the tool). -**Stable vs ephemeral addressing.** Name paths remain valid across edits to unrelated regions of the same file. Line numbers and byte offsets do not, and text anchors become ambiguous once a file grows. The output-size comparison has to account for *shelf life*: a slightly larger overview that stays useful across an entire session is cheaper than a slightly smaller one that has to be regenerated after each edit. In a five-edit session on one file, Serena's name-path overview is queried once; the line-number grep output is effectively refreshed after each edit that shifts upstream content — an O(edits × file_grep_cost) hidden tax that the one-call comparison misses. +### Stable vs ephemeral addressing -**Verdict (§4):** under ~10-line edits, built-in `Edit` is cheaper; above that threshold, symbolic body replacement wins on raw payload; across a multi-edit session, name-path addressing wins regardless of size because it doesn't decay. +- Serena's name paths (`CollectStats/refresh_return_stats`) are stable identifiers — they survive any edit to surrounding code. No re-read or re-discovery needed between edits. +- The built-in workflow uses ephemeral, position-based addresses at every stage: Grep returns line numbers, Read takes line offsets, and both are invalidated by any insertion or deletion above the target. Edit's `old_string` matching is content-based and more resilient, but it depends on the upstream position-based steps to know *what* to match. After an edit shifts line numbers, the entire Grep→Read→Edit chain must be re-executed from the top. +- In a session with N edits to the same file, this costs up to N-1 additional Grep/Read round-trips with built-ins. With Serena, the cost is zero — the same name path works on the first and tenth edit. + +**Verdict:** Serena is more token-efficient for medium-to-large edits and cross-file operations; Edit is more efficient for small, localized changes. Across multi-edit sessions, Serena's stable addressing avoids the re-read tax that compounds with each successive built-in edit. --- -## §5. Reliability and correctness analysis (under correct use) +## 5. Reliability & Correctness (Under Correct Use) -**Precision of matching.** `find_referencing_symbols` on `Collector` returns ~70 code files and annotates each with the containing symbol that holds the reference. `Grep \bCollector\b` returns 332 hits across 88 files including notebooks, SVGs, and markdown. For the question "which Python files import and use this class?", Serena's output is directly usable and Grep's needs a filter pass. For the question "where is the name `Collector` mentioned anywhere in the repo, including docs, changelog, and diagrams?", Grep's output is directly usable and Serena's is incomplete by design. Each tool's precision is perfect *for its question*; the mistake is asking the wrong one. +### Precision of matching +- Serena: Exact symbol resolution via name paths. `Collector/reset_env` unambiguously selects one method. +- Edit: Text matching. Unique strings match correctly; non-unique strings fail (and Edit reports the error). -**Scope disambiguation across overrides.** `find_symbol("_collect")` returned three distinct name paths — `BaseCollector/_collect`, `Collector/_collect`, `AsyncCollector/_collect` — each independently addressable for rename or body replacement. Text search on `_collect(` returns three line locations with no structural annotation; the caller must read context to tell them apart, and any cross-file rename risks touching the wrong override if called carelessly. +### Scope disambiguation +- Serena distinguishes overrides, overloads (via indices), and nested classes by name path. +- Grep/Edit cannot distinguish methods with the same name in different classes without reading surrounding context. -**Atomicity on real failures.** `move` of `get_stddev_from_dist` made coordinated changes across three files in one call. A five-step Edit chain replicating the same move would leave the repo in a half-renamed state if any intermediate step failed on disk-full, a permission error, or an interrupted process; a single-call atomic refactoring either completes or leaves the working tree clean. This matters less for local agent sessions (where the blast radius of a partial refactor is small and recoverable with `git checkout --`) and more for any workflow where a failed run is committed or pushed. +### Atomicity +- Serena's cross-file operations (rename, move) are atomic — all files updated or none. +- Built-in multi-file edits are sequential — a failure mid-chain leaves an inconsistent state (recoverable via `git checkout`). -**Transitive semantic queries.** `type_hierarchy` returned both the sub-chain (`Collector → AsyncCollector`) and the super-chain (`BaseCollector → ABC → object`, with `ABC` resolved into an external `.pyi` stub) in one call. No text-search workflow reaches into stub files or chains override relationships in one step. +### Semantic queries vs text search +- `find_referencing_symbols` returns code-level references categorized by type. Grep returns all textual mentions. +- `type_hierarchy` returns transitive sub/supertypes. No built-in equivalent without iterative search. -**Success signals (symmetric).** Both toolsets return only mechanical success: "the file was written," "the rename finished." Neither verifies that the new code still compiles, type-checks, or preserves semantics. Post-edit `git diff` review is the caller's responsibility on both sides. +### External dependency lookup +- Serena: Available via IDE index, no environment setup needed. Can retrieve symbol docs and location. +- Built-in: Requires working Python environment, correct venv, and manual file discovery. More powerful when available (full source), but higher setup cost. -**Verdict (§5):** Serena's correctness edge is concentrated in questions that are semantic by nature — override chains, transitive type queries, cross-file atomic ops — and tied to Grep/Edit on questions that are textual by nature. +**Verdict:** Serena provides stronger correctness guarantees for symbol-level operations (scope, atomicity, semantic precision); built-in tools are reliable for text-level operations with the caveat that scope disambiguation requires manual effort. --- -## §6. Workflow effects across a multi-step session +## 6. Workflow Effects Across a Session -The single biggest session-level effect is **identifier stability across edits**. A session that makes 5 edits to `collector.py` looks like: +### Compound advantages +- **Exploration → Edit without position re-acquisition**: Serena's overview tools produce name paths that are directly usable as edit targets. The built-in path produces line numbers (from Grep) that are consumed by Read — but after an edit, those line numbers are stale. In a typical explore-edit-explore-edit cycle, Serena's name paths remain valid throughout while built-in line numbers must be re-acquired after each edit. Over 3 explore+edit cycles, this saves ~3–6 intermediate Grep/Read calls. +- **Chained edits without re-reads**: Name-path stability means no re-reads between edits to the same file. For N edits, this saves up to N-1 Read calls. Edit's text matching partially avoids this (if old_strings stay unique), but the upstream discovery steps (Grep line numbers, Read offsets) still go stale. +- **Cross-file refactoring**: When a rename or move is part of a larger change, doing it atomically avoids the need to manually track "which files still need updating." -- With symbolic addressing: one `get_symbols_overview` at the start, five `replace_symbol_body`/`insert_after_symbol` calls by name path. The overview is consulted zero or one more times. No re-reads of the file between edits. -- With line-number or large-text-anchor addressing: one `Grep class/def` at the start, one `Read offset+limit` before each edit (or a careful re-grep after any insert that shifts lines), five `Edit`s. The Grep/overview may need to be refreshed mid-session once line numbers drift. +### Diminishing returns +- For purely exploratory sessions (reading code, no edits), Serena's advantage is modest — `Grep` and `Read` are nearly as fast for navigation, and Serena's structured output doesn't save many calls. +- For sessions dominated by small text edits (config changes, log message tweaks), Serena adds no value. -The hidden cost of the built-in workflow is not in any single call — it's the compounding re-Reads and anchor recomputation across a session. A single Read of a 1500-line file is ~40 kB of context; doing it four extra times across a session is a ~160 kB invisible tax that never shows up in a one-call comparison. +### Neutral findings +- Both toolsets require similar total calls for single-file work. The difference is ~1 call per operation. +- Output quality for code review (reading diffs, understanding changes) is identical — both require `git diff`. -**Intermediate output survives.** The overview I pulled at the start of this evaluation, the reference list for `Collector`, the type hierarchy for `BaseCollector`, and the signature table for `_collect` all remain valid now that I'm writing the report — I never had to regenerate them. A workflow based on line numbers would have had to regenerate its intermediate output after each of the ~15 edits I applied and reverted during the experiments. - -**Verdict (§6):** session-level efficiency scales with mutation rate; the more edits you plan, the more decisively symbolic addressing wins, and the effect is invisible on any single-call benchmark. +**Verdict:** Serena's advantages compound across multi-step refactoring sessions where each operation feeds into the next; for read-heavy or small-edit sessions, the advantage is marginal. --- -## §7. Capabilities with no built-in equivalent +## 7. Unique Capabilities (No Practical Built-In Equivalent) -1. **Cross-file atomic refactorings with automatic import maintenance.** `move` cleaned up an unused import in the source file as a side effect of moving the last user of that import. Built-ins have no equivalent — you'd have to notice. **Value: rare but high — this is the kind of cleanup that bit-rots across a large refactor.** +1. **Atomic cross-file rename** — 1 call, all imports and usages updated, all-or-nothing. No built-in equivalent without scripting a multi-step chain. Frequency: moderate (refactoring sessions). Impact: high (saves 5–10 calls, eliminates partial-update risk). -2. **Resolution of third-party symbols from a usage site.** `find_declaration` + `find_symbol(search_deps=True)` reach into `site-packages` and `.pyi` stubs for the exact class used at a given line of code. **Value: a few times per session, saves 3–4 calls each.** +2. **Atomic cross-file move** (symbol or file) with import rewriting — includes adding necessary imports to the target module. Frequency: low-to-moderate. Impact: very high per occurrence (saves 8–12 calls, handles import dependency graph). -3. **Transitive type hierarchy including external supertypes.** `type_hierarchy` returns sub- and super-chains in one call and crosses module boundaries and stub files. No text-search sequence can reproduce this in O(1). **Value: once or twice per unfamiliar codebase; near zero once you know it.** +3. **Type hierarchy traversal** — transitive supertypes and subtypes in 1 call. Built-in requires iterative Grep with manual transitive closure. Frequency: low. Impact: moderate (saves 3–5 calls). -4. **Containing-symbol metadata on reference queries.** `find_referencing_symbols` returns each reference with its enclosing function/class name, making the result a navigation map rather than a line list. **Value: every time you need to understand *how* a symbol is used, not just *where*.** +4. **Safe delete with usage check** — reports all usages before deleting, with option to propagate. Built-in requires Grep + manual verification. Frequency: low. Impact: moderate (safety check is the value). -5. **Enforced safe-delete with usage-list refusal.** `safe_delete` refuses to remove a symbol that still has usages and returns the usage list. Built-ins cannot refuse — `Edit` applies whatever you send it. **Value: rare but prevents the worst-class mistake.** +5. **External dependency symbol lookup** via IDE index — no Python environment needed. Frequency: moderate. Impact: moderate (avoids environment setup friction). -**Verdict (§7):** the capability deltas are real but concentrated in lower-frequency tasks; the one that shows up across *every* editing session is (1.5) — symbolic addressing as a property of the *editing* tools themselves, which I'm treating as the Tier-A efficiency win in §1 rather than a separate capability here. +**Verdict:** Serena provides 3–5 capabilities with no practical built-in equivalent, concentrated in cross-file refactoring and semantic navigation. --- -## §8. Where built-ins remain the right default +## 8. Tasks Outside Serena's Scope (Built-In Only) -- **Small anchored edits (≤ ~10 lines).** Task 7a: Edit's payload is ~10× smaller than `replace_symbol_body` for a one-line change because it sends only the two anchors, not the whole body. Frequency: extremely high — typo fixes, constant changes, error-message tweaks, single-line bug fixes. **Probably 30–50% of daily edits.** +- **Non-code file operations**: Reading/editing config files, docs, changelogs, notebooks → `Read`/`Edit`/`Write` +- **Free-text search**: Finding log strings, URLs, magic constants → `Grep` +- **Shell operations**: Running tests, builds, git commands, package management → `Bash` +- **File creation**: New files from scratch → `Write` +- **Glob-based file discovery**: Finding files by pattern → `Glob` +- **Broad codebase search**: When you don't know what you're looking for → `Grep` with regex -- **Free-text search for strings, log messages, magic constants, URLs.** Task 20: `Grep` is the only tool that can find a bare string across the repo. Serena's symbolic search expects an identifier, not a phrase. **Frequency: multiple times per session.** +Estimated share of daily work: These tasks constitute roughly 40–60% of a typical coding session (reading docs, running tests, searching for patterns, editing config). Serena's augmentation covers the remaining 40–60% where code-level semantic operations apply. -- **Non-code files** — changelogs, READMEs, YAML configs, notebooks. Task 19: `Read` is the tool. **Frequency: occasional but universal.** - -- **Doc and docstring sweeps after a code-level rename.** Serena's `rename_in_comments=True` catches Sphinx cross-references (verified in Task 10), but if your documentation lives outside of Python docstrings — `.md`, `.rst`, `.ipynb` — a text-based `Grep` sweep is the complementary step, not a Serena failure. **Frequency: every cross-file rename that touches a public API.** - -- **Single-file renames of distinctive identifiers.** Task 9: `Edit replace_all=true` is effectively tied with semantic rename; either works. **Frequency: common.** - -- **Quick one-shot explorations where you don't plan to edit the target.** If the workflow is "look at one function, answer a question, move on," `Read offset/limit` and `Grep` are as fast as semantic tools and don't require any address to be stable beyond the current call. - -These cases are not rare — collectively they probably cover more than half of the calls in a typical session, which is exactly why §1's weighting puts the quiet "addressing stability" win above the spectacular "cross-file refactoring" wins: the former touches every session, the latter touches a handful per week. - -**Verdict (§8):** built-ins are the right default for small edits, free-text search, non-code files, and docstring sweeps — roughly half of daily editing work. +**Verdict:** Built-in tools handle roughly half of daily work that falls entirely outside Serena's scope; Serena augments the code-centric other half. --- -## §9. Usage rule for a developer with both toolsets +## 9. Practical Usage Rule -Per-task decision rule, in priority order: +| Task type | Use | +|---|---| +| Cross-file rename, move, delete | Serena (unique capability) | +| Understanding class hierarchy or symbol relationships | Serena (1 call vs. iterative search) | +| Getting a structural overview of a large file | Serena (richer structure) or Grep (simpler, faster) | +| Reading a specific method body by name | Serena (direct) or Grep+Read (2 calls) | +| Small edit (1–5 lines inside a method) | Edit (less payload) | +| Full method rewrite | Serena `replace_symbol_body` (less payload, no re-read) | +| Single-file rename of a unique identifier | Edit with `replace_all` (equivalent) | +| Non-code files, config, docs | Built-in `Read`/`Edit` | +| Text search, pattern matching | Built-in `Grep` | +| Anything involving shell, git, tests | Built-in `Bash` | +| External dependency inspection | Serena (no env setup needed) | -1. **Small edit (≤ ~10 lines), known text anchor** → `Edit`. Payload is ~10× smaller than symbolic body replacement at this size. -2. **Medium or larger body rewrite, or structural insert** → `replace_symbol_body` / `insert_after_symbol`. 2–3× payload cut plus stable addressing for the next edit. -3. **Cross-file rename, move, or delete of a symbol** → `rename` / `move` / `safe_delete`, then a complementary `Grep` sweep of `.md`/`.rst`/`.ipynb` for any text-only references. The semantic tool covers code and Python docstrings in one atomic step; the Grep sweep handles external docs. -4. **Find callers of a class/function** → `find_referencing_symbols`. -5. **Find any mention of a name across the repo including docs** → `Grep`. -6. **Navigate into third-party library source** → `find_declaration` with a regex anchored at the use site, or `find_symbol(search_deps=True)`. -7. **Understand a type hierarchy** → `type_hierarchy`. -8. **Understand an unfamiliar file's structure** → `get_symbols_overview(depth=1)` for classes + methods, then `find_symbol(include_body=True)` for the methods you actually want to read. -9. **Chained edits to one file** → address every edit by `name_path`; do not mix in line-number or large-text-anchor edits unless you plan to re-Read before each one. -10. **Free-text search, non-code files, small single-line tweaks** → built-ins. - -**Verdict (§9):** Edit for small changes, symbolic body replacement for larger ones, semantic multi-file refactoring tools with a Grep doc-sweep on top, Grep/Read for text and non-code; the one habit that matters most is defaulting to name-path addressing whenever you expect more than one edit to the same file. +**Verdict:** Use Serena for cross-file refactoring and semantic navigation; use built-ins for text-level edits, non-code files, and shell operations; for single-file code edits, choose based on edit size (Edit for small, Serena for full-body rewrites). diff --git a/docs/04-evaluation/results/020_codex_on_jbplugin-serena-evaluation.md b/docs/04-evaluation/results/020_codex_on_jbplugin-serena-evaluation.md index 2b091003..d403c15c 100644 --- a/docs/04-evaluation/results/020_codex_on_jbplugin-serena-evaluation.md +++ b/docs/04-evaluation/results/020_codex_on_jbplugin-serena-evaluation.md @@ -1,136 +1,660 @@ -# Evaluation by Gpt 5.4 in Codex CLI +# Evaluation: What Serena Adds Over Built-Ins -## Final Recommendation +**Evaluated by:** Gpt 5.4 (high) in Codex -The recommendation is created with the [follow-up summary prompt](../011_followup-summary-prompt), everything else was created in one shot from the -[evaluation prompt](../010_evaluation-prompt). +This report compares Serena's JetBrains-backed semantic coding tools with built-in file, shell, search, and patch tools in this repository. The comparison assumes competent use of both toolsets: built-ins are used for text, file, shell, config, and small patch work; Serena is used where symbol identity, language semantics, or IDE refactoring semantics apply. -> "As a coding AI agent, I would ask my owner to add Serena because it lets me work with code as stable symbols instead of fragile text, turning the -refactors, lookups, and multi-file edits that usually feel risky and repetitive into precise IDE-backed operations I can trust." +Method: I explored code first, avoided repo documentation and prior notes, ran real edits/refactors, and reverted after each experiment. After each edit/refactor experiment I checked `git status --short` and returned the tree to clean before moving on. Measurements are approximate, but call counts, diff sizes, and result shapes are from observed runs. -## 1. Headline +> **One-line summary:** As a coding agent, I would ask my owner to add Serena because it turns fragile text-and-line-number work into precise symbol-aware navigation and +refactoring, which makes real code changes feel faster, safer, and far less blind. -Serena's largest added value is stable semantic addressing and IDE-backed refactoring. In this Java plugin repo, that meant a method/class could be targeted as `Logger/warning[0]`, `Symbol/safeDelete`, or `ProjectUtil` without line numbers, and cross-file rename/move/inline/delete operations were delegated to IntelliJ's model. +## 1. Headline: what Serena changes -High value, common: symbol lookup, method-body retrieval, stable name paths, and reference search. Frequency: many times per coding session. Value per hit: usually saves 1-3 reads/searches and avoids loading whole files. +Serena adds a semantic layer over the codebase. Its concrete delta is the ability to address and transform code by symbols: name paths, overload indexes, reference graphs, type hierarchies, external declarations, and JetBrains refactoring operations. -High value, less frequent but large: cross-file rename, move, safe delete, inline. Frequency: a few times per feature/refactor. Value per hit: saves roughly 5-20 calls and reduces missed import/call-site risk. +Tasks where Serena adds capability: -Medium value: type hierarchy and external dependency declaration lookup. Frequency: occasional. Value per hit: turns "search and infer" into 1 semantic query; text search cannot truly reproduce transitive hierarchy or dependency source lookup without IDE/index/cache work. +- Code-reference search: `find_referencing_symbols` for `Symbol/findReferences` returned 6 precise code usages grouped by containing symbols. `rg findReferences` also returned the declaration, the `/findReferences` route string, and comments. +- Type hierarchy: one `type_hierarchy` call for `PostRequestHandler` returned 18 direct endpoint subclasses, a transitive `TypeHierarchyHandler -> GetSupertypesHandler/GetSubtypesHandler` branch, and the external `Object` supertype. +- External dependency lookup: `find_declaration` resolved `ReferencesSearch.search(anchorElement)` to ``, and `find_symbol` returned the selected overload body. Built-in repo/cache grep found the import and call but not the dependency definition. +- Cross-file refactors: semantic rename, symbol move, file move, safe delete, and inline executed real IDE refactors and updated files/usages/imports. +- Stable addressing: a chained edit used `SymbolFinder/findFilesByName`, `SymbolFinder/getProject`, and `SymbolFinder/qualNameMatchesName` after earlier edits shifted line numbers. -Low/no added value: config/docs/free-text search and tiny line edits. Frequency: common, but built-ins are already optimal. Value per hit for Serena: none or negative for small local edits. +Tasks where Serena applies but offered little or no improvement: -**Verdict:** Serena adds the most value whenever the task is about named code entities rather than text spans; the weighted daily win is stable symbol navigation, while the biggest per-hit win is IDE refactoring. +- Tiny intra-method edits: a 1-line error-message change was smaller with a built-in patch; Serena body replacement required sending the whole method. +- Simple one-file private rename: manual patch and semantic rename produced the same 2-line diff for `qualNameMatchesName -> qualifiedNameMatchesName`. +- Method insertion at a known spot: both workflows produced the same 4-line diff; Serena's benefit was target stability, not smaller payload. +- Whole-method rewrite: Serena targets the method boundary cleanly, but the full replacement body still has to be sent. -## 2. Added Value By Area +Tasks outside Serena's scope: -- Stable symbol navigation: showed on `Symbol.java` and `UIControlUtil.java`. Frequency: every non-trivial coding session. Value: saves 1-3 calls per lookup and avoids full-file reads. -- Symbol-scoped edits: `replace_symbol_body`, `insert_after_symbol`, and overload-specific targeting worked on `Logger/warning[0]`, `Logger/warning[1]`, and `Logger/logToToolWindow`. Frequency: several times per session. Value: small edits lose to text Edit, medium edits break even, whole-body edits save about 2x input payload. -- Cross-file refactors: renaming `Logger` to `SerenaLogger` updated 10 files plus the Java file rename; moving `ProjectUtil` into `service.endpoint` updated the package and removed the now-local import. Frequency: occasional. Value: saves roughly 10-20 manual reads/edits/verifications. -- Semantic relationships: `ToolWindowContent` references, `TypeHierarchy` subtypes, `SubtypeHierarchy` supertypes, and `Gson.fromJson` declaration came back as code entities. Frequency: occasional. Value: 1 call versus several searches plus inference. -- Built-in text work remains essential: `build.gradle.kts`, `/findSymbol`, `127.0.0.1`, and `FORM_INIT_DELAY_MILLIS` were naturally handled by `Read`/`rg`. Frequency: large share of daily work. Value: Serena adds nothing there. +- Reading config/non-code files such as `plugin.xml` and `build.gradle.kts`. +- Free-text searches for URLs, endpoint strings, settings values, comments, and resources. +- File inventory, line counts, shell commands, build/test execution, Git operations, and arbitrary text edits. -**Verdict:** Serena's contribution is not blanket speed; it removes repeated code-entity bookkeeping from ordinary navigation and almost all bookkeeping from real refactors. +Value-weighted summary: -## 3. Detailed Evidence +| Rank | Difference | Frequency | Value per hit | Delta | +|---|---:|---:|---:|---| +| 1 | Cross-file semantic refactors | Medium-high | High | Often avoids 5-20 manual edit/search/verify steps and reduces partial-update risk. | +| 2 | Code usages vs text mentions | High | Medium-high | Removes noisy grep triage for "who uses this symbol?" | +| 3 | Symbol overview/body retrieval | High | Medium | Avoids large reads and stale line targeting. | +| 4 | Type hierarchy/implementations | Medium | High | Hard to reproduce correctly with text search, especially transitively. | +| 5 | Stable addressing across edit chains | Medium | Medium | Reduces refresh work after line shifts. | +| 6 | External dependency lookup | Low-medium | High | Needs IDE index; built-ins need source/decompiler infrastructure. | +| 7 | Small local edits | High | Neutral/negative for Serena | Built-in patch/edit usually sends less. | -### 3.1 Code Understanding +**Verdict:** Serena materially changes symbol-centric exploration and refactoring; it does not replace built-ins for ordinary text, file, shell, config, or tiny local edits. -Semantic overview of `Symbol.java` returned a class tree with fields, methods, inner classes, and overload indexes in 1 call. Text equivalent was `rg "class |...\\(" Symbol.java`, which returned 100+ signature/comment hits and needed filtering. Follow-up semantic read of `UIControlUtil/findButton` was 1 call returning only the 10-line body; text follow-up needed locating the line then reading a slice. +## 2. Added value and differences by area -Payloads: semantic overview sent path/depth only and returned about 900 tokens; text grep returned about 2,000+ tokens for `Symbol.java`. Semantic method read returned about 90 tokens; text slice returned similar body tokens but required a locator step. +- Cross-file semantic refactoring changes both workflow and correctness. Frequency: medium-high. Value per hit: high. The observed class rename updated a file/class plus imports/usages; the nested-record move changed 4 existing files and created 1 new file; the file move updated package/imports across 3 paths. Built-ins can reproduce the result, but only through search, file moves, patches, import cleanup, and verification. -**Verdict:** Use Serena for source structure and specific method bodies; use text only when the question is literally textual. +- Semantic search separates code usage from text mention. Frequency: high. Value per hit: medium-high. Serena returned only code references for `Symbol/findReferences`; `rg` returned code, route strings, comments, and the declaration. Built-ins remain better for "mentioned anywhere." -### 3.2 References, Hierarchy, Dependencies +- Symbol overview/body retrieval cuts exploration payload. Frequency: high. Value per hit: medium. `get_symbols_overview` on `Symbol.java` returned fields, methods, nested classes, and overload indexes without reading the 1,000+ line file. Regex produced line-oriented candidates and still required manual scope interpretation. -`find_referencing_symbols` on `ToolWindowContent` returned 4 code uses: two subclass declarations and two parameters. `rg "ToolWindowContent"` returned 5 lines including the definition. For "who uses this in code," Serena was higher precision; for "where is this string mentioned," `rg` was the right tool. +- Type/dependency queries add capabilities not practically present in built-ins. Frequency: low-medium. Value per hit: high. Serena returned a transitive type graph and an external IntelliJ API method body; text tools needed iterative searches or extra source/decompiler setup. -`type_hierarchy` on `TypeHierarchy` returned `SubtypeHierarchy` and `SupertypeHierarchy`; supertypes for `SubtypeHierarchy` returned `TypeHierarchy` and external `Object`. Text search found name matches plus false positives like `TypeHierarchyRequest`. +- Small localized edits are not improved by symbol-body replacement. Frequency: high. Value per hit: neutral or negative for Serena. A one-line patch was smaller than replacing an 11-line method body. -`find_declaration` on `gson.fromJson(requestBody, requestClass)` resolved external `Gson/fromJson[0]` and returned the source body. Built-ins needed finding the Gradle dependency, locating `~/.gradle/.../gson-2.10.1-sources.jar`, listing/extracting `Gson.java`, then searching inside it. +- Some JetBrains refactors staged added/deleted files. Frequency: limited to class/file move/delete style operations. Value per hit: neutral operational tradeoff. Semantic value remains, but verification/revert must check the index as well as unstaged files. -**Verdict:** Serena adds real semantic reach for "code relationships"; text search can find mentions, but not reliably answer relationship questions in one step. +**Verdict:** Serena's highest-value differences are symbol identity, reference graphs, and IDE refactor execution; built-ins remain superior for simple text-local work. -### 3.3 Edit Size Economics +## 3. Detailed evidence, grouped by capability -Small edit: rewriting `Logger.logToToolWindow` as an early return. Text patch sent a small old/new anchor, about 9 changed lines. Serena required sending the whole 11-line method body. Text was cheaper. +### 3.1 Repository structure and entry points -Medium edit: rewriting most of `UIControlUtil.tryHandleDialogs`. Text patch sent about 14 old lines plus 20 new lines. Serena sent the full method body, including its comment, about 26 lines. Roughly even. +Attempted: identify top-level layout, packages, and entry points. -Large edit: replacing `Symbol.safeDelete` implementation shape. With content-anchored Edit, a whole-body replacement would send old body plus new body, about 2x the new method payload. `replace_symbol_body` sent only the new body plus `Symbol/safeDelete`. +Built-in call chain: -**Verdict:** Use text Edit for 1-3 line changes, either tool for 10-30 line method rewrites, and Serena for whole-method replacements. +1. `git status --short` -> clean. +2. `rg --files -g '!*.md' -g '!docs/**' -g '!CLAUDE.md'` -> code/config/resource inventory. +3. `Get-ChildItem -Force` -> top-level directories. +4. `Get-Content src/main/resources/META-INF/plugin.xml` -> plugin entry points. +5. `Get-Content build.gradle.kts -TotalCount 80` -> Gradle/IntelliJ platform setup. -### 3.4 Refactors +Serena call chain: not applicable for repository inventory and non-code config. -Private rename: `Logger/logToToolWindow` to `appendToToolWindow` changed 7 occurrences in 1 semantic call plus optional `rg` verification. Manual path was `rg`, patch each occurrence, then `rg` verify: 3 calls and more payload. +Observed structure: -Multi-file rename: `Logger` to `SerenaLogger` changed 10 files and renamed the Java file. Manual path would be `rg`, read 10 files, rename/move file, edit imports/type names/constructors, verify with `rg`, and likely compile: roughly 14-20 calls. +- `service`: backend service and request handling. +- `service/endpoint`: endpoint handlers for symbol search, references, formatting, rename, move, safe delete, inline, inspections, and completions. +- `symbol`: symbol model, symbol lookup, hierarchy, path matching, and move processors. +- `util`: IDE/project/editor/UI helpers. +- `ui`: tool window content. +- `plugin.xml`: registers `PluginStartupActivity`, settings service/configurable, and a dummy test action. -Move: moving `ProjectUtil` to `service.endpoint` moved the file, changed its package, and removed the import from `RefreshFileHandler` in 1 call. Manual path would coordinate filesystem move, package line, import removal/additions, and verification. +Payloads: built-ins used 5 small shell/read calls and returned several KB of code/config context; Serena had no relevant semantic operation. -Safe delete: `DebugUtil` had no references; safe delete returned `affected_references: []` and deleted it. Manual path is search, delete, verify. Inline: `ProjectUtil/getAbsolutePath` inlined into both call sites in 1 call. +**Verdict:** Repository layout and non-code entry-point discovery are built-in work; Serena starts adding value once the target is code symbols. -**Verdict:** Every multi-file or semantic refactor tested was a clear Serena win in call count, payload, and correctness surface. +### 3.2 Large file structural overview -### 3.5 Session Effects +Attempted: compare structural overview on `src/main/java/de/oraios/serena/symbol/Symbol.java`. -I chained three edits in `Logger.java`: rename helper, replace `Logger/warning[0]`, insert after `Logger/error[0]`. The name paths survived earlier edits; no line recalculation was needed. A built-in line/slice workflow would need refreshed context after insertions because line numbers and nearby anchors shift. +Serena call chain: -**Verdict:** Serena's stable identifiers compound across a session; the more edits you make in one file, the wider the gap gets. +1. `get_symbols_overview(relative_path=Symbol.java, depth=1)` +2. Next step: `find_symbol(Symbol/findReferences, include_body=true)` -## 4. Token Efficiency +Serena output: one top-level `Symbol` class; fields; nested `ChildrenCollector` and `DocumentationResolver`; methods including overload-indexed names such as `getLocationString[0]`, `getLocationString[1]`, `move[0]`, `move[1]`, `getDocumentation[0]`, and `getDocumentation[1]`. -The crossover is size-dependent. Small edit: text wins because it sends only a tiny anchor. Medium rewrite: near parity. Whole-body rewrite: Serena wins because it sends new body only, while content-anchored Edit sends old body plus new body. +Built-in call chain: -Forced reads matter: `find_symbol(...include_body=true)` avoided reading 573 lines of `UIControlUtil.java` and 1,295 lines of `Symbol.java`. Stable outputs also have longer shelf life: `Logger/warning[0]` remains useful after unrelated edits; line 52 does not. +1. `rg` for method-like declaration lines in `Symbol.java`. +2. `rg` for class/interface/enum lines in `Symbol.java`. +3. Next step: line-slice read around the selected method. -**Verdict:** Token economics favor built-ins for tiny local text edits and Serena for symbol retrieval, chained work, and whole-symbol replacement. +Built-in output: about 70 line-oriented method/class matches, including nested symbols, with no durable symbol identity beyond manual signature inspection. -## 5. Reliability +Payloads: Serena used 1 compact overview call plus a targeted follow-up. Built-ins used 2 regex searches plus a later line-range read. -Semantic matching distinguished overloads: `Logger/warning[0]` was `warning(String,Object...)`; `Logger/warning[1]` was `warning(String,Throwable)`. `rg "warning\\("` returned both and left disambiguation to the caller. +**Verdict:** Serena's overview is more actionable because its output feeds directly into stable symbol-addressed calls; regex outlines are useful but line-based. -Atomicity matters: semantic rename/move run as IDE refactorings, so a failure is not a half-finished sequence of 10 manual edits. Both toolsets still report mechanical success only; semantic intent still needs diff/build review. +### 3.3 Targeted method body retrieval -Transitive queries are where text cannot compete directly: hierarchy and external declaration lookup depend on IDE indexes and dependency sources, not just strings. +Attempted: retrieve `Symbol/findReferences` without reading surrounding file content. -**Verdict:** Correctness weight favors Serena for code-entity scope and refactoring atomicity, while text remains correct for text questions. +Serena call chain: `find_symbol(relative_path=Symbol.java, name_path_pattern=Symbol/findReferences, include_body=true)`. -## 6. Workflow Effects +Serena output: only the `public ArrayList findReferences()` body. -Over a multi-step session, Serena's intermediate artifacts stay usable: symbol trees, name paths, reference lists, and hierarchy nodes survive edits outside those symbols. Text outputs are often ephemeral: line numbers and byte offsets decay immediately after insertions/deletions. +Built-in call chain: -The practical effect is not just fewer calls; it is fewer re-reads. In the chained `Logger.java` session, Serena needed three edit calls. A text workflow would normally require initial reads/searches, edits, and refreshed context before later insertions. +1. `rg "findReferences\\(" src/main/java -n -C 2` +2. `Get-Content Symbol.java` line slice around the declaration. -**Verdict:** Serena's session-level multiplier comes from not having to rediscover where code moved after each edit. +Built-in output: search context across multiple files, then the selected method slice. -## 7. No Built-In Equivalent +Payloads: Serena input was one path/name-path request and returned roughly 25 method lines. Built-ins required search output plus a separate slice read. -- IDE semantic rename/move/inline/safe-delete: high value when refactoring, moderate frequency. Built-ins can approximate with many edits but cannot provide IDE refactoring semantics or atomicity. -- Type hierarchy including external `Object`: medium value, occasional. Text can search `extends`, but cannot transitively resolve hierarchy with dependency/stub awareness in one call. -- External dependency declaration resolution: medium value, occasional. Built-ins require build-file discovery and local source/cache spelunking. -- Overload/name-path targeting: high value in typed code, frequent enough to matter. Text can match names but cannot address `warning[0]` as a distinct method without manual signature reasoning. +**Verdict:** If the symbol is known, Serena retrieves the body in one precise call; built-ins need search plus line-targeted reading. -**Verdict:** Serena's unique capabilities are concentrated in IDE-indexed code intelligence and refactoring operations, not general file manipulation. +### 3.4 References: code usages vs mentions anywhere -## 8. Built-Ins As Default +Attempted: find all references to `Symbol/findReferences`. -Use built-ins for non-code files, config, docs, and free text. `build.gradle.kts` was best read directly. `rg` was clearly right for `/findSymbol`, `127.0.0.1`, and `FORM_INIT_DELAY_MILLIS`. +Serena call chain: `find_referencing_symbols(relative_path=Symbol.java, name_path=Symbol/findReferences)`. -Use built-ins for tiny edits where a short unique anchor is obvious. Also always keep a post-refactor text sweep for comments, markdown, notebooks, generated files, and product strings; that is complementary verification, not a Serena weakness. +Serena output: -Estimated share: built-ins remain best for maybe 40-60% of daily interactions because much coding work is still file/text/config/search. Serena dominates the code-symbol subset. +- `Symbol/formatReferenceLocations/references` +- `Symbol/inline/refCountBefore` +- `Symbol/verifyInlineResult/refCountAfter` +- `SymbolDTO/Builder/buildDTO/references` +- `RunInspectionsOnSymbolsHandler/handleRequest/refs` +- `FindReferencesHandler/buildResponse/references` -**Verdict:** Start with built-ins for text and config; switch to Serena as soon as the noun in your task is a symbol. +Built-in call chain: `rg "findReferences" src/main/java -n -C 1`. -## 9. Practical Rule +Built-in output: same call sites plus the declaration, `/findReferences` route registration, and explanatory comments. -Reach for Serena when the task says class, method, overload, implementation, reference, hierarchy, rename, move, inline, delete, or "insert after this method." Reach for `rg`/Read/Edit when the task says string, config, docs, log message, URL, generated text, or "change these two lines." +Payloads: Serena returned a compact grouped usage list of about 500-700 characters. `rg` returned broader line context of about 1-2 KB and mixed code usages with text mentions. -For edits: text Edit for 1-3 line tweaks; Serena `replace_symbol_body` for full methods/classes; semantic refactor tools for any rename/move/delete/inline that crosses call sites or imports. +**Verdict:** Serena has higher precision for "who uses this symbol in code"; built-ins have higher recall for "where is this text mentioned anywhere." -I restored all tracked edits after the experiments. The tracked working tree was clean after evaluation; the only baseline untracked entries were `.claude/` and `serena-evaluation-prompt.md`. I did not run the Gradle test suite because the task was an evaluation, not a product change. +### 3.5 Type hierarchy -**Verdict:** With both toolsets installed, use built-ins for text and Serena for code entities; that rule captures almost all of the measured value without overthinking each call. +Attempted: list supertypes and subtypes transitively for `PostRequestHandler`. + +Serena call chain: `type_hierarchy(relative_path=PostRequestHandler.java, name_path=PostRequestHandler, hierarchy_type=both, depth=0)`. + +Serena output: external `Object` supertype; 18 direct endpoint subclasses; transitive nested branch `TypeHierarchyHandler` containing `GetSupertypesHandler` and `GetSubtypesHandler`. + +Built-in call chain: + +1. `rg "extends PostRequestHandler|extends TypeHierarchyHandler|class PostRequestHandler" src/main/java -n` +2. Manually follow any discovered intermediate types. +3. Repeat searches if deeper hierarchy exists. + +Payloads: Serena used 1 hierarchy query and returned structured JSON. Built-ins used pattern search and manual transitive grouping. + +**Verdict:** Serena turns hierarchy discovery into a semantic graph query; text search requires iterative pattern expansion and manual reasoning. + +### 3.6 External dependency symbol lookup + +Attempted: retrieve the IntelliJ dependency symbol behind `ReferencesSearch.search(anchorElement)`. + +Serena call chain: + +1. `find_declaration(relative_path=Symbol.java, regex="ReferencesSearch\\.(search)\\(anchorElement\\)")` +2. `find_symbol(relative_path=, name_path_pattern=ReferencesSearch/search[0], include_body=true, search_deps=true)` + +Serena output: declaration `ReferencesSearch/search[0]` in an external class path and the overload body: + +```java +public static @NotNull Query search(@NotNull PsiElement element) { + return search(element, GlobalSearchScope.allScope(PsiUtilCore.getProjectInReadAction(element)), false); +} +``` + +Built-in call chain: + +1. `rg "ReferencesSearch" src/main/java -n` -> import and local call. +2. `rg` over repo, `.intellijPlatform`, and Gradle caches for `class ReferencesSearch` or matching method signatures -> no useful source hit. +3. `Get-ChildItem` over Gradle/IntelliJ caches -> jar candidates but no direct definition. + +Infrastructure difference: Serena depends on the JetBrains IDE index; built-ins need source jars, decompiler tooling, or classpath-specific jar inspection. + +**Verdict:** External dependency lookup is a genuine Serena capability when IDE indexes are available; ordinary built-ins do not provide it. + +### 3.7 Single-file edits across edit sizes + +Small tweak attempted: change `"No symbol found for "` to `"No matching symbol found for "` in `SymbolFinder/findSymbolByNamePath`. + +Built-in call chain: + +1. Use existing search/body context. +2. `apply_patch` one-line replacement. +3. `git diff`, `git status --short`. +4. Revert and clean check. + +Serena call chain: + +1. `replace_symbol_body(SymbolFinder/findSymbolByNamePath, full method body with changed string)`. +2. `git diff`, `git status --short`. +3. Revert and clean check. + +Observed diff: both produced the same 1-line change. Payload difference: built-in edit input was a tiny hunk; Serena input was the full 11-line method body. + +Medium rewrite attempted: rewrite `SymbolFinder/findFilesByName` from list accumulation to stream collection. + +Built-in call chain: `apply_patch` over the method body, then diff/status/revert/status. + +Serena call chain: `replace_symbol_body(SymbolFinder/findFilesByName, new method body)`, then diff/status/revert/status. + +Observed diff: both produced `10 +++-------`, 3 insertions and 7 deletions. Payload difference was small: a patch hunk versus an 8-line replacement method. + +Large/whole-body rewrite attempted: rewrite `InspectionRunner/collectResults` while preserving signature and behavior shape. + +Built-in call chain: `apply_patch` against the method body, then diff/status/revert/status. + +Serena call chain: `replace_symbol_body(InspectionRunner/collectResults, full symbol text including Javadoc and replacement body)`, then diff/status/revert/status. + +Observed diff: both produced `38 +++++++++++-----------`, 19 insertions and 19 deletions. Payload difference: built-in required a large contextual hunk; Serena required the full symbol text, about 70 lines including Javadoc/signature/body. + +**Verdict:** Serena is not automatically more token-efficient for edits; its edit advantage is stable symbol targeting, while built-ins are smaller for tiny local hunks. + +### 3.8 Insert method at structural location + +Attempted: insert `private Logger getLogger()` immediately after `SymbolFinder/getProject`. + +Built-in call chain: + +1. Search/read surrounding area. +2. `apply_patch` with nearby context. +3. Diff/status/revert/status. + +Serena call chain: + +1. `insert_after_symbol(relative_path=SymbolFinder.java, name_path=SymbolFinder/getProject, body=...)` +2. Diff/status/revert/status. + +Observed diff: both produced the same 4-line insertion. + +Payloads: built-ins sent inserted body plus surrounding text context; Serena sent inserted body plus symbol target. + +**Verdict:** Structural insertion is a modest Serena improvement: same resulting diff, less dependence on line/context stability. + +### 3.9 Rename: private helper in one file + +Attempted: rename `SymbolFinder/qualNameMatchesName` to `qualifiedNameMatchesName`. + +Built-in call chain: + +1. `rg "qualNameMatchesName" SymbolFinder.java -n -C 2` +2. `apply_patch` declaration and one call site. +3. Diff/status/revert/status. + +Serena call chain: + +1. `find_referencing_symbols(SymbolFinder/qualNameMatchesName)` -> one referencing method. +2. `rename(name_path=SymbolFinder/qualNameMatchesName, new_name=qualifiedNameMatchesName, rename_in_comments=true, rename_in_text_occurrences=true)`. +3. Diff/status/revert/status. + +Observed result: Serena returned `"Success"`. Both produced the same 2-line diff. + +**Verdict:** Semantic rename has little advantage for a one-file helper with one call site, but it scales better than text edits as references spread or names become ambiguous. + +### 3.10 Rename: cross-file class including imports + +Attempted: rename `MoveOperationFailedException` to `MoveFailedException`. + +Serena call chain: + +1. `rename(relative_path=MoveOperationFailedException.java, name_path=MoveOperationFailedException, new_name=MoveFailedException, rename_in_comments=true, rename_in_text_occurrences=true)`. +2. `git diff --stat`, `git diff`, `git diff --cached --stat`, `git status --short`. +3. Restore staged rename and modified files; clean check. + +Serena result: `"Success"`. + +Observed edits: + +- Added/staged `MoveFailedException.java`. +- Deleted/staged `MoveOperationFailedException.java`. +- Updated import, Javadoc comment, and thrown class in `MoveProcessor.java`. + +Built-in equivalent chain: + +1. `rg "MoveOperationFailedException" src/main/java -n`. +2. Rename/create/delete file. +3. Patch class name and constructor. +4. Patch imports and usages. +5. Patch comments/text occurrences if desired. +6. Verify no unintended old references remain. +7. Compile/test for a permanent change. + +**Verdict:** Cross-file class renames are high-value Serena cases because file/class coupling, imports, usages, and optional comments are handled as one semantic refactor. + +### 3.11 Move symbol to another file context + +Attempted: move nested record `InspectionRunner/PendingFix` to a top-level file in the same package. + +Serena call chain: + +1. `move(relative_path=InspectionRunner.java, name_path=InspectionRunner/PendingFix, target_relative_path=src/main/java/de/oraios/serena/service/endpoint)`. +2. Diff/status/cached-diff inspection. +3. Restore staged new file and modified files; clean check. + +Serena result: + +```json +{ + "source_relative_path": "src/main/java/de/oraios/serena/service/endpoint/InspectionRunner.java", + "target_relative_path": "src/main/java/de/oraios/serena/service/endpoint", + "moved_symbol": {"name_path": "PendingFix", "type": "CLASS"} +} +``` + +Observed edits: + +- Created/staged `PendingFix.java` with package and imports. +- Removed nested record from `InspectionRunner.java`. +- Updated 3 external references from `InspectionRunner.PendingFix` to `PendingFix`. +- Removed unused imports in 2 files. + +Built-in equivalent chain: read nested record/import needs, add file, delete nested symbol, search usages, patch qualified usages, patch internal references if needed, remove imports, verify, compile/test. + +**Verdict:** Symbol move is a substantial Serena addition because the difficult part is reference/import repair, not copying text. + +### 3.12 Move file/package location + +Attempted: move `PluginUtil.java` from `util` to `service`. + +Serena call chain: + +1. `move(relative_path=src/main/java/de/oraios/serena/util/PluginUtil.java, target_relative_path=src/main/java/de/oraios/serena/service)`. +2. Diff/status/cached-diff inspection. +3. Restore staged rename and modified imports; clean check. + +Serena result: + +```json +{ + "source_relative_path": "src/main/java/de/oraios/serena/util/PluginUtil.java", + "target_relative_path": "src/main/java/de/oraios/serena/service" +} +``` + +Observed edits: + +- Staged rename `util/PluginUtil.java -> service/PluginUtil.java`. +- Changed package declaration to `de.oraios.serena.service`. +- Updated import in `PluginStartupActivity`. +- Removed now-unneeded same-package import in `SerenaBackendService`. + +Built-in equivalent chain: move file, patch package declaration, search `PluginUtil`, patch imports/usages, remove redundant imports, verify no old package import remains, compile/test. + +**Verdict:** File moves are high-value when packages/imports must change; built-ins can do them manually but require dependency repair steps. + +### 3.13 Safe delete and propagated delete + +Attempted: delete unused `DebugUtil`. + +Serena call chain: + +1. `safe_delete(relative_path=DebugUtil.java, name_path=DebugUtil, delete_even_if_used=false, propagate=false)`. +2. `git diff --cached --stat`, `git status --short`. +3. Restore staged deletion; clean check. + +Serena result: + +```json +{ + "deleted_symbol": "DebugUtil", + "relative_path": "src/main/java/de/oraios/serena/util/DebugUtil.java", + "affected_references": [], + "message": "Symbol deleted successfully with no affected references." +} +``` + +Built-in equivalent chain: `rg "DebugUtil"`, inspect declaration vs usages, delete file/symbol, verify no remaining usages. + +Propagated deletion: no suitable correct-use candidate was found where JetBrains safe-delete propagation would meaningfully delete a used symbol and propagate deletion through call sites. For ordinary used-method deletion, correct safe delete reports usages or, if forced, deletes the symbol and reports affected references; that is not the same as arbitrary call-site deletion. + +**Verdict:** Safe delete adds an integrated usage check plus deletion operation; propagated deletion was not evaluated because this codebase did not provide a suitable candidate. + +### 3.14 Inline + +Attempted: inline `ProjectUtil/getAbsolutePath`, a single-expression helper. + +Serena call chain: + +1. `find_symbol(ProjectUtil/getAbsolutePath, include_body=true)`. +2. `find_referencing_symbols(ProjectUtil/getAbsolutePath)` -> 2 references. +3. `inline_symbol(relative_path=ProjectUtil.java, name_path=ProjectUtil/getAbsolutePath, keep_definition=false)`. +4. Diff/status/revert/status. + +Serena result: `{"status": "SUCCESS"}`. + +Observed edits: + +- Removed `getAbsolutePath`. +- Replaced internal call in `ProjectUtil/getVirtualFile`. +- Replaced external call in `RefreshFileHandler`. + +Built-in equivalent chain: read helper body, `rg getAbsolutePath`, patch each call site with substituted expression, delete helper, verify no stale references, compile/test. + +**Verdict:** Inline is a strong Serena refactor for legally substitutable helpers; built-ins can reproduce it but must manually adapt each substitution. + +### 3.15 Scope precision and overload targeting + +Attempted: distinguish overloaded `Symbol/getLocationString` methods. + +Serena call chain: + +1. `find_symbol(Symbol/getLocationString[0], include_body=true)`. +2. `find_symbol(Symbol/getLocationString[1], include_body=true)`. + +Serena output: + +- `[0]`: `public String getLocationString() { return getLocationString(element); }` +- `[1]`: `private static String getLocationString(PsiElement element) { ... }` + +Built-in call chain: `rg "getLocationString" Symbol.java -n -C 1`, then manual signature inspection. + +**Verdict:** Serena's overload-indexed name paths provide precise symbol addressing that text search does not. + +### 3.16 Atomicity and success signals + +Observed Serena success signals: + +- `replace_symbol_body` -> `OK` +- `insert_after_symbol` -> `OK` +- `rename` -> `"Success"` +- `move` -> JSON with source/target/moved symbol or paths +- `safe_delete` -> JSON with deleted symbol, affected references, and message +- `inline_symbol` -> `{"status": "SUCCESS"}` + +Built-in success signals: + +- `apply_patch` -> "Success. Updated the following files" +- Shell/Git commands -> exit codes and textual output +- Cross-file consistency -> user-driven diff/search/build verification + +Atomicity comparison: Serena refactors executed as single IDE operations. Built-in equivalents are chains of searches, file operations, and patches; a competent user can make them correct, but intermediate states can be partial. + +Tradeoff: class/file move/delete refactors staged added/deleted files in the Git index, so clean verification must check both staged and unstaged state. + +**Verdict:** Serena gives clearer semantic success signals and more atomic cross-file edits; built-ins require manual consistency management. + +### 3.17 Workflow effects across multiple edits + +Attempted: chain 3 edits in `SymbolFinder.java` without refreshing between them. + +Serena call chain: + +1. `replace_symbol_body(SymbolFinder/findFilesByName, ...)`. +2. `insert_after_symbol(SymbolFinder/getProject, ...)`. +3. `rename(SymbolFinder/qualNameMatchesName, ...)`. +4. Diff/status/revert/status. + +Observed result: all edits applied after earlier edits shifted file positions. Final diff touched 18 lines with net 9 insertions and 9 deletions. + +Built-in equivalent chain: initial read/search, apply first patch, rely on robust patch context or refresh line numbers, apply second patch, refresh or search again, apply rename patch, verify. + +**Verdict:** Serena's name paths remain useful after line shifts, so its advantage compounds in multi-edit sessions. + +### 3.18 Non-code reads and free-text search + +Attempted: read config and search free text. + +Built-in call chain: + +1. `Get-Content plugin.xml`. +2. `Get-Content build.gradle.kts -TotalCount 80`. +3. `rg "http://|https://|localhost|127\\.0\\.0\\.1|8080|24282|SERENA|serena" src/main/java src/main/resources -n`. + +Serena call chain: not applicable. + +Observed result: built-ins exposed plugin registration, Gradle platform setup, URLs, IDs, settings defaults, and resource/text mentions. + +**Verdict:** Built-ins are natural and sufficient for config reading and free-text search; this is outside Serena's semantic-code scope. + +## 4. Token-efficiency analysis + +Payload differences across edit sizes: + +| Task | Built-in payload | Serena payload | More efficient | +|---|---|---|---| +| 1-line string tweak | Tiny patch hunk, 1 changed line | Full 11-line method body | Built-ins | +| Medium method rewrite | 10-line diff hunk | Full 8-line replacement method | Roughly equal | +| Large body rewrite | Large contextual patch, 38-line diff | Full symbol text, about 70 lines | Roughly equal; Serena has better target | +| Insert method | Inserted body plus surrounding context | Inserted body plus name path | Serena slightly | +| Private one-file rename | `rg` plus 2-line patch | optional refs query plus rename command | Roughly equal | +| Cross-file rename/move/inline | Multiple searches, file operations, patches, verifications | One semantic refactor plus diff/status | Serena | + +Forced reads: + +- Built-ins commonly need a search before a precise read, then a line slice or full context before editing. +- Serena can skip full-file reads when the symbol name path is known. +- When the symbol is not known, Serena's `get_symbols_overview` and shallow `find_symbol` calls provide compact discovery rather than full-file reads. + +Output payload: + +- Serena exploration output is structured and scoped to symbols. +- Built-in grep output can be larger because it includes declarations, comments, strings, and unrelated text mentions. +- Built-ins can be extremely low-output for known-location small edits. + +Stable vs ephemeral addressing: + +- Built-ins address code by file paths, line numbers, text context, or byte positions. Line numbers and slices go stale after edits. +- Serena addresses symbols by `relative_path + name_path`, with overload indexes where needed. The chained-edit experiment showed those targets survived line shifts. +- Path-changing operations still require updated paths afterward, so symbol stability is strongest before moves/renames that alter file locations. + +**Verdict:** Serena saves tokens by avoiding broad reads/search triage and by collapsing multi-file edit chains; built-ins remain more token-efficient for tiny known-location hunks. + +## 5. Reliability & correctness under correct use + +Precision of matching: + +- Serena reference search returned code usages and excluded route strings/comments. +- Built-in text search returned all mentions and therefore mixed true code usage with broader text hits. +- Serena overload indexes targeted `Symbol/getLocationString[0]` and `[1]`; text search required manual signature inspection. + +Scope disambiguation: + +- Serena name paths encode class nesting and overload identity. +- Built-ins can disambiguate with careful patterns and reads, but the disambiguation is manual. +- In simple cases, such as a private helper with one call site, this semantic precision had little practical value. In overloaded, nested, or cross-file cases, it materially reduced ambiguity. + +Atomicity: + +- Serena refactors apply through JetBrains refactoring operations and produce one operation-level success signal. +- Built-in equivalents are decomposed into separate searches, file moves, patches, and verification steps. +- Competent users can verify either path, but Serena reduces the number of intermediate inconsistent states. + +Semantic queries vs text search: + +- `type_hierarchy` and dependency declaration lookup produced semantic information not available from ordinary grep. +- `rg` remained better for broad mention searches and config/resource scans. + +External dependency limitations: + +- Serena's dependency lookup depends on IDE/language indexes and available dependency metadata. +- Built-ins can match or exceed it only if paired with source jars, decompilers, or separate language-server tooling. + +Operational tradeoff: + +- Several Serena refactors staged created/deleted files. This is not a correctness problem, but it adds index-state verification to the cleanup workflow. + +**Verdict:** Serena improves correctness where symbol identity and cross-file semantics matter; built-ins remain reliable and simpler for text/file tasks. + +## 6. Workflow effects across a session + +Where Serena advantages compound: + +- Symbol overview results become inputs to body reads, reference searches, renames, moves, safe deletes, and inline operations. +- Name paths remain useful after line shifts, reducing re-read/re-target work during multi-edit sessions. +- Refactor-heavy sessions benefit because one semantic command replaces search/edit/verify loops across files. + +Where Serena advantages diminish: + +- After a full file has already been read, overview adds less incremental value. +- If the task is a small textual substitution in a known place, symbolic body replacement can cost more payload than a tiny patch. +- For non-code and free-text work, Serena has no role. + +Intermediate result durability: + +- Serena intermediate results such as `SymbolFinder/findFilesByName` or `Symbol/getLocationString[1]` remain meaningful as long as the file/symbol still exists. +- Built-in line numbers became stale after insertions; robust textual patch context remained usable when surrounding lines did not change. + +Verification cost: + +- Both workflows still need `git diff`, build/test, and domain-specific verification for permanent changes. +- Serena success signals reduce the need to inspect every call site manually, but not the need to inspect the intended diff. + +**Verdict:** Serena's advantages compound inside symbol-centric sessions and diminish once the work becomes plain text, config, shell, or already-loaded single-file editing. + +## 7. Unique capabilities + +Unique here means no practical built-in equivalent without adding separate language-server, IDE, source-index, or decompiler infrastructure. + +- Transitive type hierarchy. Frequency: medium. Impact: high when changing base classes, interfaces, or handler contracts. Built-ins can grep explicit `extends` clauses but do not produce a semantic transitive graph or external supertypes. + +- External dependency declaration/body lookup from a call site. Frequency: low-medium. Impact: high for API behavior questions. Serena resolved `ReferencesSearch.search` into an external class method body; ordinary built-ins did not. + +- IDE semantic move of a nested symbol to a top-level file with usage/import repair. Frequency: low-medium. Impact: high during refactors. Built-ins can reproduce this only as a manual multi-step refactor. + +- Safe delete with semantic usage check. Frequency: medium. Impact: medium-high. Built-ins can search and delete, but they do not provide an integrated safe-delete operation over code usages. + +- Inline refactor with call-site substitution. Frequency: low-medium. Impact: medium-high. Built-ins can patch substitutions manually, but expression adaptation and helper deletion are not primitive text operations. + +Not unique: reading files, listing files, grep, small patches, simple one-file renames, config/resource understanding, and shell/Git operations. + +**Verdict:** Serena's unique practical capabilities are hierarchy/dependency queries and IDE refactors; its non-unique areas are ordinary text and file operations. + +## 8. Tasks outside Serena's scope + +Built-in-only or built-in-natural tasks observed: + +- Repository inventory and top-level layout. +- Reading `plugin.xml`, `build.gradle.kts`, scripts, resources, and generated artifacts. +- Free-text search for URLs, settings strings, endpoint strings, magic constants, resource IDs, and comments. +- Running shell commands, builds, tests, Git operations, and filesystem cleanup. +- Small text edits where a one-line patch is the whole task. + +Estimated share of daily coding work: + +- About 30-50% of a normal coding session is built-in-natural: shell, tests, grep, config reads, small text patches, and Git checks. +- Serena applies to much of the remaining 50-70% when the work is code navigation, symbol understanding, references, hierarchy, or refactoring. +- Serena coverage rises in refactor-heavy sessions and falls in config/debug/test-log sessions. + +**Verdict:** Serena augments the semantic-code portion of development; built-ins still carry a large and necessary share of routine work. + +## 9. Practical usage rule + +Use Serena when the task can be stated as a code-symbol operation: + +- "What methods/classes are in this file?" +- "Give me this method body." +- "Who uses this symbol?" +- "What implements/subclasses this?" +- "Rename, move, delete, or inline this symbol and update usages." +- "Find the declaration of this call, including dependency symbols." +- "Apply several edits where line locations may shift." + +Use built-ins when the task is text, file, shell, config, or tiny local editing: + +- "List files." +- "Read config." +- "Search for this string anywhere." +- "Change this one known line." +- "Run tests/build." +- "Inspect Git state." +- "Edit markdown/resources/scripts." + +For mixed workflows: + +1. Use built-ins to find files/config and run verification. +2. Use Serena to identify and transform code symbols. +3. Use built-ins to inspect final diffs and run tests. +4. Prefer built-ins for tiny local hunks after the symbol has already been located. +5. Prefer Serena for any refactor that crosses file boundaries or depends on type/reference semantics. + +**Verdict:** Choose Serena for symbol identity and semantic refactoring; choose built-ins for text, shell, config, verification, and minimal local edits. From 34e1fae739ab7f78f800b77ad4c17014f36e16e1 Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 13 Apr 2026 07:19:33 -0500 Subject: [PATCH 143/190] Allow symbol operations in dot-prefixed directories (#1203) * Allow symbol operations in dot-prefixed directories is_ignored_dirname() used dirname.startswith(".") which blanket-ignored all dot-prefixed directories including .github/, .vscode/, etc. Changed to only ignore VCS directories (.git, .svn, .hg, .bzr). Language-specific dot directories (.build, .elixir_ls, .terraform, etc.) are already handled by the individual LS overrides. Fixes #1187, fixes #853. --------- Co-authored-by: Michael Panchenko --- CHANGELOG.md | 1 + src/solidlsp/ls.py | 23 +++++++++++++++++++- test/solidlsp/toml/test_toml_ignored_dirs.py | 10 +++++---- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35f4a3d0..30a9c809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Status of the `main` branch. Changes prior to the next official version change w * General: - Support environment variable `SERENA_USAGE_REPORTING` (set to `false` to disable usage reporting) + - Extended the list of always ignored directories (by language servers) with common cases. * Language Servers: - Add mSL (mIRC Scripting Language) support (custom pygls-based language server; symbols, references, definitions) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index c3362811..9dda6f33 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -353,13 +353,34 @@ class SolidLanguageServer(ABC): DOCUMENT_SYMBOL_CACHE_VERSION = 4 DOCUMENT_SYMBOL_CACHE_FILENAME = "document_symbols.pkl" + # Directories that should always be ignored regardless of language: + # VCS internals, virtual environments, caches, and serena's own data. + _ALWAYS_IGNORED_DIRS = frozenset( + { + ".git", + ".svn", + ".hg", + ".bzr", # VCS + ".venv", + ".env", # virtual environments + ".cache", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", # caches + ".tox", + ".nox", # test runners + ".idea", # IDE internals + ".serena", # serena's own data + } + ) + # To be overridden and extended by subclasses def is_ignored_dirname(self, dirname: str) -> bool: """ A language-specific condition for directories that should always be ignored. For example, venv in Python and node_modules in JS/TS should be ignored always. """ - return dirname.startswith(".") + return dirname in self._ALWAYS_IGNORED_DIRS @staticmethod def _determine_log_level(line: str) -> int: diff --git a/test/solidlsp/toml/test_toml_ignored_dirs.py b/test/solidlsp/toml/test_toml_ignored_dirs.py index e5126942..11d987f9 100644 --- a/test/solidlsp/toml/test_toml_ignored_dirs.py +++ b/test/solidlsp/toml/test_toml_ignored_dirs.py @@ -24,7 +24,7 @@ class TestTomlIgnoredDirectories: assert language_server.is_ignored_dirname(".cargo"), ".cargo should be ignored" assert language_server.is_ignored_dirname("node_modules"), "node_modules should be ignored" - # Directories starting with . are ignored by base class + # Infrastructure directories ignored by base class assert language_server.is_ignored_dirname(".git"), ".git should be ignored" assert language_server.is_ignored_dirname(".venv"), ".venv should be ignored" @@ -49,12 +49,14 @@ class TestTomlIgnoredDirectories: def test_various_cache_directories(self, language_server: SolidLanguageServer) -> None: """Test various cache and temporary directories are ignored.""" - # Directories starting with . are ignored by base class + # Cache directories ignored by base class assert language_server.is_ignored_dirname(".cache"), ".cache should be ignored" - # IDE directories (start with .) + # IDE internals assert language_server.is_ignored_dirname(".idea"), ".idea should be ignored" - assert language_server.is_ignored_dirname(".vscode"), ".vscode should be ignored" + + # .vscode is intentionally NOT ignored — it contains user-facing config + assert not language_server.is_ignored_dirname(".vscode"), ".vscode should not be ignored" # Note: __pycache__ is NOT ignored by TOML server (only Python servers ignore it) assert not language_server.is_ignored_dirname("__pycache__"), "__pycache__ is not TOML-specific" From 1464fee8755081da7bb438bdeccaa91579b0d5fe Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 13 Apr 2026 14:21:53 +0200 Subject: [PATCH 144/190] CI: run doc-build on each PR --- .github/workflows/pytest.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index f1d969e9..45601361 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -576,3 +576,5 @@ jobs: - name: Type-checking with mypy shell: bash run: uv run poe type-check + - name: Make sure documentation builds + run: uv run poe doc-build From c3a5652fc7eccbac18fe90ed3770fa85e9767de4 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 13 Apr 2026 14:32:47 +0200 Subject: [PATCH 145/190] Minor improvement in readme and docs [ci skip] --- README.md | 21 ++++++++++----------- docs/04-evaluation/000_intro.md | 12 ++++++------ 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 72e3e467..81198d80 100644 --- a/README.md +++ b/README.md @@ -30,25 +30,24 @@ more complex codebases. ## What Our "End Users" Say -While it is humans who download and set up Serena, our end users are essentially AI agents, -so they are also in the best position to evaluate Serena. -We crafted an unbiased and detailed prompt which leads the agent to estimate the value of adding Serena's tools -to its built-in capabilities. The thorough evaluation usually takes around 25 minutes (per project) -and tests every single aspect of Serena. A summary of what the agents had to say: +While it is humans who download and set up Serena, our end users are essentially AI agents, +so they are also in the best position to evaluate Serena. +We crafted an unbiased and detailed evaluation prompt +that leads the agent to perform ~20 routine coding tasks, representative of everyday development work, using both Serena's tools and its own built-ins, +measure the differences, and report the results. A one-sentence summary of what the agents had to say: **Opus 4.6 (high effort) in Claude Code on a large Python codebase:** > "Serena's IDE-backed semantic tools are the single most impactful addition to my toolkit — cross-file renames, moves, and reference lookups that would cost me 8–12 careful, error-prone steps collapse into one atomic call, and I would absolutely ask any developer I work with to set them up." -**Gpt 5.4 (high) in Codex CLI on a Java codebase:** +**GPT 5.4 (high) in Codex CLI on a Java codebase:** > "As a coding AI agent, I would ask my owner to add Serena because it gives me the missing IDE-level understanding of symbols, references, and refactorings, turning fragile text surgery into calmer, faster, more confident code changes where semantics matter." -Your agent deserves the best coding tools, give them Serena! +Give your agent the tools it's been asking for and add Serena MCP to your client! -See the documentation on our [evaluation methods](https://oraios.github.io/serena/04-evaluation/000_intro.html) and the -detailed results beyond the brief recommendations above. You can easily run your own evaluation of Serena on a project of your choice -by reusing our methods or adapting them to your needs. +See our [documentation](https://oraios.github.io/serena/04-evaluation/000_intro.html) for the full methodology and much more detailed evaluation results +beyond these brief summaries, or run your own evaluation on a project of your choice. ## How Serena Works @@ -81,7 +80,7 @@ complex projects! There are two alternative technologies powering these capabilities: -* **Language servers** implementing the language server Protocol (LSP) — the free/open-source alternative +* **Language servers** implementing the language server protocol (LSP) — the free/open-source alternative which is used by default. * The **Serena JetBrains Plugin**, which leverages the powerful code analysis and editing capabilities of your JetBrains IDE (paid plugin; free trial available). diff --git a/docs/04-evaluation/000_intro.md b/docs/04-evaluation/000_intro.md index 74b300af..1b91a316 100644 --- a/docs/04-evaluation/000_intro.md +++ b/docs/04-evaluation/000_intro.md @@ -7,7 +7,7 @@ built-in capabilities (file reads, text edits, grep, shell, etc.). Rather than a simple thumbs-up/thumbs-down, it produces a detailed, evidence-based report covering capabilities, efficiency and reliability. -### Why Not Benchmarks? +## Why Not Benchmarks? Standard coding benchmarks (SWE-bench, HumanEval, etc.) measure an agent's ability to solve predefined tasks with a known correct answer. They are valuable for comparing models and agents, @@ -28,7 +28,7 @@ but they are a poor fit for evaluating a **tool augmentation layer** like Serena We wanted an evaluation that systematically covers the full surface area of Serena's capabilities without cherry-picking. -### Design Goals +## Design Goals Instead of benchmarks, we designed an evaluation methodology with three goals: @@ -56,7 +56,7 @@ Instead of benchmarks, we designed an evaluation methodology with three goals: explicitly requires reporting negative deltas and cases where Serena offers no improvement, structurally counterbalancing any tendency to favour the tool being evaluated. -### Method +## Method We give an AI coding agent a single, detailed [evaluation prompt](010_evaluation-prompt) in a one-shot session. The prompt instructs the agent to perform approximately 20 hands-on tasks across five areas: @@ -80,7 +80,7 @@ Only category (b) constitutes a neutral or negative finding; category (c) is con After the evaluation, a separate [follow-up prompt](011_followup-summary-prompt) asks the agent for a one-sentence, user-facing recommendation — the quotes shown on the [main page](https://github.com/oraios/serena). -### Results +## Results We performed evaluations using popular AI coding agents in representative scenarios — different agents, different programming languages, and different codebases — to show that the results are @@ -96,7 +96,7 @@ evaluation can easily be repeated with the LSP-based backend to assess its subse You can run your own evaluation on a project of your choice by reusing our [evaluation prompt](010_evaluation-prompt) or adapting it to your needs. -### Assessment of the Methodology +## Assessment of the Methodology _The following assessment was written by Claude Opus 4.6 (high effort) after reading the full evaluation methodology, evaluation prompt, and all result documents._ @@ -144,7 +144,7 @@ One thing to watch for is whether agents that are less capable than Opus 4.6 or meaningful evaluations — the prompt is quite demanding. But that is a question about the agent, not about the method. -### Prompt Fairness +## Prompt Fairness We put significant effort in crafting an unbiased, fair and objective evaluation prompt. The only "biased" aspects are some sentences about misuse of tools, which we consider irrelevant for the purpose of evaluation From 46be9c4bd7d7b49d99f1b1f854c21aaa06e5ba32 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 13 Apr 2026 15:34:13 +0200 Subject: [PATCH 146/190] Restructure evaluation docs --- docs/04-evaluation/000_evaluation-intro.md | 2 + .../{000_intro.md => 010_methodology.md} | 105 +++---- .../011_followup-summary-prompt.md | 9 - docs/04-evaluation/020_prompts/000_prompts.md | 6 + .../010_evaluation-prompt.md | 276 ++++++++++-------- .../020_prompts/020_summary-prompt.md | 12 + .../030_results/000_evaluation-results.md | 17 ++ .../010_cc_on_tianshou.md} | 10 +- .../020_codex_on_jbplugin.md} | 15 +- docs/04-evaluation/results/000_intro.md | 3 - docs/_config.yml | 2 +- docs/autogen_docs.py | 2 +- 12 files changed, 260 insertions(+), 199 deletions(-) create mode 100644 docs/04-evaluation/000_evaluation-intro.md rename docs/04-evaluation/{000_intro.md => 010_methodology.md} (81%) delete mode 100644 docs/04-evaluation/011_followup-summary-prompt.md create mode 100644 docs/04-evaluation/020_prompts/000_prompts.md rename docs/04-evaluation/{ => 020_prompts}/010_evaluation-prompt.md (50%) create mode 100644 docs/04-evaluation/020_prompts/020_summary-prompt.md create mode 100644 docs/04-evaluation/030_results/000_evaluation-results.md rename docs/04-evaluation/{results/010_cc_on_tianshou-serena-evaluation.md => 030_results/010_cc_on_tianshou.md} (99%) rename docs/04-evaluation/{results/020_codex_on_jbplugin-serena-evaluation.md => 030_results/020_codex_on_jbplugin.md} (99%) delete mode 100644 docs/04-evaluation/results/000_intro.md diff --git a/docs/04-evaluation/000_evaluation-intro.md b/docs/04-evaluation/000_evaluation-intro.md new file mode 100644 index 00000000..2ceeefe7 --- /dev/null +++ b/docs/04-evaluation/000_evaluation-intro.md @@ -0,0 +1,2 @@ +# Evaluation + diff --git a/docs/04-evaluation/000_intro.md b/docs/04-evaluation/010_methodology.md similarity index 81% rename from docs/04-evaluation/000_intro.md rename to docs/04-evaluation/010_methodology.md index 1b91a316..feb7f282 100644 --- a/docs/04-evaluation/000_intro.md +++ b/docs/04-evaluation/010_methodology.md @@ -1,46 +1,26 @@ -# Evaluation +# Methodology -In this section we describe how we evaluate the performance of Serena's tools. +In this section we describe the methodology we applied in evaluating the performance of Serena's tools. The evaluation measures the **concrete delta** that Serena's tools provide on top of an agent's built-in capabilities (file reads, text edits, grep, shell, etc.). Rather than a simple thumbs-up/thumbs-down, it produces a detailed, evidence-based report covering capabilities, efficiency and reliability. -## Why Not Benchmarks? - -Standard coding benchmarks (SWE-bench, HumanEval, etc.) measure an agent's ability to solve -predefined tasks with a known correct answer. They are valuable for comparing models and agents, -but they are a poor fit for evaluating a **tool augmentation layer** like Serena for several reasons: - -- **Benchmarks don't reflect real usage patterns.** Benchmark tasks are typically small, self-contained - problems that can be solved by reading and editing a handful of files. They rarely exercise the - workflows where Serena's tools shine — cross-file refactoring, navigating large codebases by symbol - structure, chaining multiple edits with stable addressing, or querying type hierarchies and - external dependencies. A benchmark score would mostly measure performance on tasks where Serena - is not expected to help. -- **Results would not generalise to the user's project.** Serena's value depends on the codebase - (size, language, complexity), the agent (model, built-in tools), and the client harness - (Claude Code, Codex, IDE plugins, etc.). A fixed benchmark on a fixed codebase with a fixed - agent tells you little about what Serena would add to *your* setup. -- **Predefined tasks bias the measurement.** Choosing specific tasks to evaluate inevitably - introduces selection bias — we would end up picking tasks that either favour or disfavour Serena. - We wanted an evaluation that systematically covers the full surface area of Serena's capabilities - without cherry-picking. - ## Design Goals -Instead of benchmarks, we designed an evaluation methodology with three goals: +We designed an evaluation methodology with three goals: -1. **Reproducible by any user, on any project, with any agent.** +1. **Generality.** The evaluation mechanism should be broadly applicable and repeatable by any user, on any project, with any agent. The evaluation is a single prompt that you give to your agent of choice, pointed at your codebase of choice, in your client of choice. This means the results directly reflect the value Serena would add to your actual workflow — not to an artificial benchmark setup. There is nothing to install, configure, or script beyond what you already have. 2. **The agent evaluates itself.** - We deliberately let the AI agent be both the executor and the evaluator. This may seem - counterintuitive, but it is the right design choice: the agent is the actual end user of the + We deliberately let the AI agent be both the executor and the evaluator. + Since most evaluations are strictly quantitative, this may seem unusual, but it is a reasonable choice: + The agent is the actual end user of the tools, so it is in the best position to judge whether a semantic tool improves its workflow compared to its built-in alternatives. It can measure call counts, payload sizes, and prerequisite steps from direct experience rather than from proxy metrics. It also avoids the @@ -48,7 +28,7 @@ Instead of benchmarks, we designed an evaluation methodology with three goals: simply uses them and reports what it observes. 3. **Comprehensive and unbiased by design.** - Rather than selecting specific tasks, the prompt defines **task categories** that systematically + Rather than selecting specific tasks, the prompt defines *task categories* that systematically span Serena's capabilities: codebase understanding, single-file edits of varying sizes, multi-file refactoring, reliability properties, and workflow effects. The agent picks concrete instances from the codebase at hand, performs each task using both toolsets side by side, and classifies @@ -58,7 +38,7 @@ Instead of benchmarks, we designed an evaluation methodology with three goals: ## Method -We give an AI coding agent a single, detailed [evaluation prompt](010_evaluation-prompt) in a one-shot session. +We give an AI coding agent a single, detailed [evaluation prompt](020_prompts/010_evaluation-prompt.md) in a one-shot session. The prompt instructs the agent to perform approximately 20 hands-on tasks across five areas: 1. **Codebase understanding** — structural overviews, targeted symbol retrieval, reference finding, type hierarchies, and external dependency lookup. @@ -72,30 +52,16 @@ tools and its own built-in tools), applies real edits verified via `git diff`, a payload sizes and prerequisite steps. Edits are reverted after each experiment to keep the working tree clean. The resulting report classifies each finding into one of three categories: -**(a)** tasks where Serena adds capability, -**(b)** tasks where Serena applies but offers no improvement, and -**(c)** tasks outside Serena's scope. + +* **(a)** tasks where Serena adds capability, +* **(b)** tasks where Serena applies but offers no improvement, and +* **(c)** tasks outside Serena's scope. + Only category (b) constitutes a neutral or negative finding; category (c) is context, not a finding. -After the evaluation, a separate [follow-up prompt](011_followup-summary-prompt) asks the agent for a +After the evaluation, a separate [follow-up prompt](020_prompts/020_summary-prompt.md) asks the agent for a one-sentence, user-facing recommendation — the quotes shown on the [main page](https://github.com/oraios/serena). -## Results - -We performed evaluations using popular AI coding agents in representative scenarios — different -agents, different programming languages, and different codebases — to show that the results are -not specific to a single setup. - -All evaluations were conducted using the **JetBrains-powered version** of Serena, as it is the -more powerful backend with a broader set of refactoring and navigation capabilities. The -evaluation can easily be repeated with the LSP-based backend to assess its subset of capabilities. - -- [Claude Code (Opus 4.6) on a large Python codebase](results/010_cc_on_tianshou-serena-evaluation) — tianshou, a reinforcement learning library (~26K lines). -- [Codex (GPT 5.4) on a Java codebase](results/020_codex_on_jbplugin-serena-evaluation) — the Serena JetBrains plugin itself. - -You can run your own evaluation on a project of your choice by reusing our -[evaluation prompt](010_evaluation-prompt) or adapting it to your needs. - ## Assessment of the Methodology _The following assessment was written by Claude Opus 4.6 (high effort) after reading the full @@ -144,14 +110,12 @@ One thing to watch for is whether agents that are less capable than Opus 4.6 or meaningful evaluations — the prompt is quite demanding. But that is a question about the agent, not about the method. -## Prompt Fairness +### Prompt Fairness -We put significant effort in crafting an unbiased, fair and objective evaluation prompt. -The only "biased" aspects are some sentences about misuse of tools, which we consider irrelevant for the purpose of evaluation -(sufficiently intelligent models use the tools correctly). -Here is what Opus 4.6 thinks about our prompt: +We put significant effort into crafting an unbiased, fair evaluation prompt. +To assess the prompt's fairness, we asked Claude Opus 4.6 to evaluate the prompt itself. -**Q**: +**Prompt**: > The attached prompt is designed to evaluate the additive value of Serena as an augmentation layer on top of built-in tools > (not to compare two competing alternatives). > Is the methodology suitable for that goal, or does it introduce bias that would distort the measurement of what Serena adds? @@ -160,12 +124,37 @@ Here is what Opus 4.6 thinks about our prompt: > Do not question the validity of this assumption in your evaluation of the prompt. > Give a brief answer, maximally two sentences. > -> \ +> \ -**Opus' Answer**: +**Claude Opus' Answer**: > "The methodology is well-suited to its stated goal: by explicitly scoping to correct use, treating Serena as an > augmentation layer rather than a competitor, and classifying out-of-scope tasks as context rather than negative > findings, it avoids the most common biases that would distort a delta measurement. The one risk is that the "correct-use > rule" could subtly favor Serena by preemptively filtering out any scenario where its tools underperform — but the prompt > mitigates this by requiring category (b) findings (applies but no improvement) and explicitly demanding negative deltas -> be reported, so the bias is acknowledged and structurally counterbalanced." \ No newline at end of file +> be reported, so the bias is acknowledged and structurally counterbalanced." + +According to Claude, the only "biased" aspects are some sentences about misuse of tools, which we consider irrelevant for the purpose of evaluation +(sufficiently intelligent models use the tools correctly). + + +## Why Not Benchmarks? + +Standard coding benchmarks (SWE-bench, HumanEval, etc.) measure an agent's ability to solve +predefined tasks with a known correct answer. They are valuable for comparing models and agents, +but they are a poor fit for evaluating a **tool augmentation layer** like Serena for several reasons: + +- **Benchmarks don't reflect real usage patterns.** Benchmark tasks are typically small, self-contained + problems that can be solved by reading and editing a handful of files. They rarely exercise the + workflows where Serena's tools shine — cross-file refactoring, navigating large codebases by symbol + structure, chaining multiple edits with stable addressing, or querying type hierarchies and + external dependencies. A benchmark score would mostly measure performance on tasks where Serena + is not expected to help. +- **Results would not generalise to the user's project.** Serena's value depends on the codebase + (size, language, complexity), the agent (model, built-in tools), and the client harness + (Claude Code, Codex, IDE plugins, etc.). A fixed benchmark on a fixed codebase with a fixed + agent tells you little about what Serena would add to *your* setup. +- **Predefined tasks bias the measurement.** Choosing specific tasks to evaluate inevitably + introduces selection bias — we would end up picking tasks that either favour or disfavour Serena. + We wanted an evaluation that systematically covers the full surface area of Serena's capabilities + without cherry-picking. \ No newline at end of file diff --git a/docs/04-evaluation/011_followup-summary-prompt.md b/docs/04-evaluation/011_followup-summary-prompt.md deleted file mode 100644 index bb7d44a3..00000000 --- a/docs/04-evaluation/011_followup-summary-prompt.md +++ /dev/null @@ -1,9 +0,0 @@ -# Post-Evaluation Summary Prompt - -_Use the prompt below to summarize the evaluation of Serena by your agent. -We used it to create the recommendations we display in the readme._ - -Write a one-sentence user-facing summary about the value that Serena's tools provide for coding. With a bit -of emotion but grounded in the evaluation that you have done above. It should be written from the perspective of a coding AI agent and make it -clear whether an AI agent would ask its owner to add Serena's tools or not. It will be the first sentence that a potential new user will see -about how useful Serena is for an AI agent. diff --git a/docs/04-evaluation/020_prompts/000_prompts.md b/docs/04-evaluation/020_prompts/000_prompts.md new file mode 100644 index 00000000..5033e6fa --- /dev/null +++ b/docs/04-evaluation/020_prompts/000_prompts.md @@ -0,0 +1,6 @@ +# Prompts + +Our evaluation uses two prompts, which are passed to the LLM in order to generate the evaluation results: + +1. the [main evaluation prompt](010_evaluation-prompt), which performs the analysis +2. the [summary prompt](020_summary-prompt), which condenses the results diff --git a/docs/04-evaluation/010_evaluation-prompt.md b/docs/04-evaluation/020_prompts/010_evaluation-prompt.md similarity index 50% rename from docs/04-evaluation/010_evaluation-prompt.md rename to docs/04-evaluation/020_prompts/010_evaluation-prompt.md index 50422f28..45009c4d 100644 --- a/docs/04-evaluation/010_evaluation-prompt.md +++ b/docs/04-evaluation/020_prompts/010_evaluation-prompt.md @@ -1,182 +1,218 @@ # Evaluation Prompt -Use the prompt below to evaluate the added value of Serena's tools against your agent's built-in tools on a project of -your choice. -All evaluations that you find in our documentation were created in one-shot sessions, only using this prompt and -then following up with a separate [summary prompt](011_followup-summary-prompt) +We use the prompt below to evaluate the added value of Serena's tools against the +agent's built-in tools on a given project. +The evaluations were created in one-shot sessions, only using this +prompt and the follow-up [prompt for summarization](020_summary-prompt) +``` # Evaluate Serena's Tools Against Built-Ins -You have access to Serena's coding tools alongside your built-in tools (Read, Edit, Write, Glob, Grep, Bash, etc.). I -want a thorough, evidence-based evaluation of **what Serena's tools add on top of the built-ins**, assuming both +You have access to Serena's coding tools alongside your built-in tools (Read, +Edit, Write, Glob, Grep, Bash, etc.). I want a thorough, evidence-based +evaluation of **what Serena's tools add on top of the built-ins**, assuming both toolsets are used correctly. -This is an evaluation, not a user guide, and it is not a binary adoption pitch. Your job is to answer: *if a competent -user of both toolsets had only the built-ins, what concrete capabilities and efficiency differences would they -experience, and by how much?* A reader who finishes your report should have a clear, specific picture of what Serena -adds — as well as where it provides no meaningful improvement or introduces tradeoffs — in terms of capabilities, -workflows, and efficiency. Not a thumbs-up/thumbs-down, but a sharp description of the delta. +This is an evaluation, not a user guide, and it is not a binary adoption pitch. +Your job is to answer: *if a competent user of both toolsets had only the +built-ins, what concrete capabilities and efficiency differences would they +experience, and by how much?* A reader who finishes your report should have a +clear, specific picture of what Serena adds — as well as where it provides no +meaningful improvement or introduces tradeoffs — in terms of capabilities, +workflows, and efficiency. Not a thumbs-up/thumbs-down, but a sharp description +of the delta. -Failure modes from misuse, silent-failure traps, gotcha comparisons, and "be careful of X" warnings are out of scope. -They belong in onboarding material for a developer learning the tools, not in a delta analysis of what the tools add. +Failure modes from misuse, silent-failure traps, gotcha comparisons, and "be +careful of X" warnings are out of scope. They belong in onboarding material for +a developer learning the tools, not in a delta analysis of what the tools add. -**Describe the measured differences clearly and neutrally.** Avoid generic or non-informative framing such as "both have -their place" unless supported by concrete findings. If Serena adds substantial capabilities, name and quantify them. If -it adds marginal or no capabilities, say that and show why. If there are regressions or tradeoffs, include them -explicitly. The two toolsets are complementary — that's a given, not the answer. -Serena is an augmentation layer, not a replacement. Do not penalize it for tasks it was not designed to address — -instead, note those tasks as "built-in only" and move on. The evaluation should measure what Serena adds where it -applies, not what it fails to add where it doesn't.The answer is a specific list of what -Serena contributes (or does not contribute) to a correct-use workflow relative to built-ins. +**Describe the measured differences clearly and neutrally.** Avoid generic or +non-informative framing such as "both have their place" unless supported by +concrete findings. If Serena adds substantial capabilities, name and quantify +them. If it adds marginal or no capabilities, say that and show why. If there +are regressions or tradeoffs, include them explicitly. The two toolsets are +complementary — that's a given, not the answer. Serena is an augmentation layer, +not a replacement. Do not penalize it for tasks it was not designed to address — +instead, note those tasks as "built-in only" and move on. The evaluation should +measure what Serena adds where it applies, not what it fails to add where it +doesn't.The answer is a specific list of what Serena contributes (or does not +contribute) to a correct-use workflow relative to built-ins. Write the report to serena-evaluation.md in the repo root. ---- + ## Ground rules ### Starting conditions -- Start fresh. Do not read project memories, CLAUDE.md shortcuts, or prior notes about the repo. Do not read - documentation files either. Explore as if you've never seen it, focusing on code. -- Use git as your safety net — experiment freely. Any edit can be reverted with `git checkout -- ` or `git stash`. - Run edits for real; don't simulate. A hands-on comparison is worth far more than a thought experiment. -- After each experiment, verify the working tree is clean with `git status --short` before moving on. +- Start fresh. Do not read project memories, CLAUDE.md shortcuts, or prior notes + about the repo. Do not read documentation files either. Explore as if you've + never seen it, focusing on code. +- Use git as your safety net — experiment freely. Any edit can be reverted with + `git checkout -- ` or `git stash`. Run edits for real; don't simulate. A + hands-on comparison is worth far more than a thought experiment. +- After each experiment, verify the working tree is clean with + `git status --short` before moving on. ### How to compare — correct use only -- **Correct-use rule.** Evaluate each tool on inputs and tasks it was designed for, called the way a competent user - would call it. A tool doing exactly what its contract says is not a finding, even if a careless caller could misuse - it. -- **Know the contract before you call.** Before invoking any tool, have a one-sentence understanding of what it does. If - you expect an error or "not applicable," don't make the call. -- **Refactoring semantics are real.** Inlining requires a substitutable function (typically single-expression, no side - effects); moving requires a legal target; safe-delete requires no surviving usages. If the repo has no suitable - candidate for a given refactoring, report "no suitable candidate in this codebase" and skip it — don't contrive a - broken input. +- **Correct-use rule.** Evaluate each tool on inputs and tasks it was designed + for, called the way a competent user would call it. A tool doing exactly what + its contract says is not a finding, even if a careless caller could misuse it. +- **Know the contract before you call.** Before invoking any tool, have a + one-sentence understanding of what it does. If you expect an error or "not + applicable," don't make the call. +- **Refactoring semantics are real.** Inlining requires a substitutable function + (typically single-expression, no side effects); moving requires a legal + target; safe-delete requires no surviving usages. If the repo has no suitable + candidate for a given refactoring, report "no suitable candidate in this + codebase" and skip it — don't contrive a broken input. ### How to compare — workflow level, not single-call level -- For every task, write out the full end-to-end call chain on each side before drawing conclusions. Include prerequisite - reads and follow-up steps. -- Do not evaluate a tool based on criteria that only arise from mixing workflows incorrectly. -- Ephemeral addressing is a liability. Line numbers and byte offsets go stale after edits; stable addressing (name - paths) may reduce rework. +- For every task, write out the full end-to-end call chain on each side before + drawing conclusions. Include prerequisite reads and follow-up steps. +- Do not evaluate a tool based on criteria that only arise from mixing workflows + incorrectly. +- Ephemeral addressing is a liability. Line numbers and byte offsets go stale + after edits; stable addressing (name paths) may reduce rework. ### How to measure -- Track observations during execution. For every tool call, note: number of calls, approximate input size, output size, - and any prerequisite or verification steps. -- Separate call count, input payload, output payload, and verification cost as distinct axes. +- Track observations during execution. For every tool call, note: number of + calls, approximate input size, output size, and any prerequisite or + verification steps. +- Separate call count, input payload, output payload, and verification cost as + distinct axes. - Include prerequisite Reads and post-hoc verification steps in comparisons. -When a task falls entirely outside Serena's design scope (e.g., reading config files, small text edits where Edit -already sends minimal payload), classify it as "not applicable" rather than as a negative delta. A negative delta -requires that Serena targets the task and performs worse, not that a tool designed for something else is suboptimal when -misapplied to it. +When a task falls entirely outside Serena's design scope (e.g., reading config +files, small text edits where Edit already sends minimal payload), classify it +as "not applicable" rather than as a negative delta. A negative delta requires +that Serena targets the task and performs worse, not that a tool designed for +something else is suboptimal when misapplied to it. + ---- ## Exploration phase — tasks to actually perform -Work through the following. Each item exercises a specific capability under correct use; substitute an equivalent if an -item isn't applicable. +Work through the following. Each item exercises a specific capability under +correct use; substitute an equivalent if an item isn't applicable. ### Codebase understanding -1. Get a high-level overview of the repository structure — top-level layout, main packages, entry points. -2. Pick one large source file (300+ lines). Get a structural overview of it. Do it with semantic overview tools and with - Glob/Grep/Read. Then write out the concrete next step on each side and compare the pair of calls, not just the +1. Get a high-level overview of the repository structure — top-level layout, + main packages, entry points. +2. Pick one large source file (300+ lines). Get a structural overview of it. Do + it with semantic overview tools and with Glob/Grep/Read. Then write out the + concrete next step on each side and compare the pair of calls, not just the overview call. -3. Pick a specific method inside a class and retrieve its body without reading the surrounding file. -4. For one non-trivial symbol, find all references across the codebase. Compare recall and precision under the - question "who uses this in code?" vs "where is this mentioned anywhere, including docs?" -5. For a class, list its subclasses / implementations and its supertypes, including transitively. Compare against what - text search would need to do. -6. For at least one symbol from an external dependency (a third-party library), try to retrieve its definition or - signature. Note whether each toolset can do this at all and what infrastructure it requires (environment activation, +3. Pick a specific method inside a class and retrieve its body without reading + the surrounding file. +4. For one non-trivial symbol, find all references across the codebase. Compare + recall and precision under the question "who uses this in code?" vs "where is + this mentioned anywhere, including docs?" +5. For a class, list its subclasses / implementations and its supertypes, + including transitively. Compare against what text search would need to do. +6. For at least one symbol from an external dependency (a third-party library), + try to retrieve its definition or signature. Note whether each toolset can do + this at all and what infrastructure it requires (environment activation, site-packages discovery, language-server indexing, etc.). ### Single-file edits — span the full range of edit sizes -7a. Small tweak (1–3 lines inside a method). Change an error message or rename a local variable inside a larger method. -Do it with `Edit` and with symbolic body replacement. Compare payload sent, payload received, and prerequisite reads. +7a. Small tweak (1–3 lines inside a method). Change an error message or rename a +local variable inside a larger method. Do it with `Edit` and with symbolic body +replacement. Compare payload sent, payload received, and prerequisite reads. -7b. Medium rewrite (replace ~10–30 lines — most of a method body). Rewrite the main logic of a method while keeping its -signature. Do it both ways. +7b. Medium rewrite (replace ~10–30 lines — most of a method body). Rewrite the +main logic of a method while keeping its signature. Do it both ways. -7c. Large/whole-body rewrite. Pick a method of 50+ lines and rewrite the entire body. Do it both ways. +7c. Large/whole-body rewrite. Pick a method of 50+ lines and rewrite the entire +body. Do it both ways. -8. Insert a new function/method at a specific structural location (for example, right after an existing method). Try - both the symbolic-insert path and the manual Edit path. -9. Rename a private helper used only within one file. Compare doing it by hand vs. using a semantic rename. +8. Insert a new function/method at a specific structural location (for example, + right after an existing method). Try both the symbolic-insert path and the + manual Edit path. +9. Rename a private helper used only within one file. Compare doing it by hand + vs. using a semantic rename. ### Multi-file changes -10. Rename a symbol (function, class, or method) used across several files including imports. Compare the semantic path - against the built-in equivalent chain. -11. Move a symbol from one module to another, updating imports at all call sites. Use the semantic move tool if - available; plan the built-in equivalent honestly. -12. Move a file or package to a different location, updating imports at all call sites. Use the semantic move tool if - available; plan the built-in equivalent honestly. -12. Delete a symbol safely, checking it has no remaining usages. Compare search-then-delete with a safe-delete tool. -13. Delete a symbol and propagate the deletion to all call sites. Compare to how the built-in equivalent would work. -13. Inline a small helper into its call sites — only if the codebase contains a function that is legally inlinable. If - no such candidate exists, report "no suitable candidate" and skip it. +10. Rename a symbol (function, class, or method) used across several files + including imports. Compare the semantic path against the built-in equivalent + chain. +11. Move a symbol from one module to another, updating imports at all call + sites. Use the semantic move tool if available; plan the built-in equivalent + honestly. +12. Move a file or package to a different location, updating imports at all call + sites. Use the semantic move tool if available; plan the built-in equivalent + honestly. +12. Delete a symbol safely, checking it has no remaining usages. Compare + search-then-delete with a safe-delete tool. +13. Delete a symbol and propagate the deletion to all call sites. Compare to how + the built-in equivalent would work. +13. Inline a small helper into its call sites — only if the codebase contains a + function that is legally inlinable. If no such candidate exists, report "no + suitable candidate" and skip it. ### Reliability & correctness under correct use -14. Scope precision. Demonstrate that semantic tools address symbols by name path and can target a specific class - method, override, or overload that text search would over-match. -15. Atomicity. A semantic cross-file refactoring is atomic: either all sites are updated or none. A chain of `Edit` - calls is not. -16. Success signals. For each completed refactor, note what each tool returns on success. +14. Scope precision. Demonstrate that semantic tools address symbols by name + path and can target a specific class method, override, or overload that text + search would over-match. +15. Atomicity. A semantic cross-file refactoring is atomic: either all sites are + updated or none. A chain of `Edit` calls is not. +16. Success signals. For each completed refactor, note what each tool returns on + success. ### Workflow effects across multiple edits -17. Chain at least three edits in one file. Report what each toolset requires between edits. -18. Multi-step exploration across the repo. Note whether intermediate results remain useful across later edits or have - to be refreshed. +17. Chain at least three edits in one file. Report what each toolset requires + between edits. +18. Multi-step exploration across the repo. Note whether intermediate results + remain useful across later edits or have to be refreshed. ### Things where the comparison shouldn't be interesting -19. Read and understand a non-code file (config, changelog, docs, notebook). Semantic-code tools don't apply — use - `Read`. -20. Search for a free-text pattern across the repo (log string, magic constant, URL). Use `Grep`. +19. Read and understand a non-code file (config, changelog, docs, notebook). + Semantic-code tools don't apply — use `Read`. +20. Search for a free-text pattern across the repo (log string, magic constant, + URL). Use `Grep`. + ---- ## Evaluation phase Write a report structured for progressive disclosure. -**Value-weighting is required.** For every contribution or difference you identify — positive, neutral, or negative — -estimate: +**Value-weighting is required.** For every contribution or difference you +identify — positive, neutral, or negative — estimate: - **Frequency:** how often this arises in typical coding work - **Value per hit:** calls saved, tokens saved, or correctness impact Order findings by **frequency × value-per-hit**, not novelty. -**Every section must end with a one-sentence verdict** summarizing the practical takeaway. +**Every section must end with a one-sentence verdict** summarizing the practical +takeaway. + ---- ### 1. Headline: what Serena changes -Open with a precise description of the delta Serena provides. -Distinguish between three categories: -(a) tasks where Serena adds capability, -(b) tasks where Serena applies but offers -no improvement, and (c) tasks outside Serena's scope. -Only category (b) constitutes a neutral or negative finding. -Category (c) is context, not a finding. +Open with a precise description of the delta Serena provides. Distinguish +between three categories: (a) tasks where Serena adds capability, (b) tasks +where Serena applies but offers no improvement, and (c) tasks outside Serena's +scope. Only category (b) constitutes a neutral or negative finding. Category (c) +is context, not a finding. A reader stopping here should understand both what is gained and what is not. **Verdict:** (one sentence) ---- + ### 2. Added value and differences by area (3–6 bullets) @@ -190,7 +226,7 @@ Avoid framing in terms of “wins”; describe concrete differences. **Verdict:** (one sentence) ---- + ### 3. Detailed evidence, grouped by capability @@ -208,7 +244,7 @@ Include cases where: End each subsection with a verdict. ---- + ### 4. Token-efficiency analysis @@ -222,7 +258,7 @@ Include cases where each toolset is more efficient. **Verdict:** (one sentence) ---- + ### 5. Reliability & correctness (under correct use) @@ -238,35 +274,36 @@ Include both strengths and limitations of each toolset. **Verdict:** (one sentence) ---- + ### 6. Workflow effects across a session -Evaluate multi-step workflows and whether advantages compound or diminish. Include neutral or negative findings where -applicable. +Evaluate multi-step workflows and whether advantages compound or diminish. +Include neutral or negative findings where applicable. **Verdict:** (one sentence) ---- + ### 7. Unique capabilities (if any) -List capabilities that have no practical built-in equivalent. If none exist, explicitly state that. Annotate each with -frequency and impact. +List capabilities that have no practical built-in equivalent. If none exist, +explicitly state that. Annotate each with frequency and impact. **Verdict:** (one sentence) ---- + ### 8. Tasks outside Serena's scope (built-in only) -Identify tasks where built-ins are the natural choice because Serena's tools don't target them. List these briefly for -completeness but do not frame them as Serena shortcomings — they are outside its scope. Estimate their share of daily +Identify tasks where built-ins are the natural choice because Serena's tools +don't target them. List these briefly for completeness but do not frame them as +Serena shortcomings — they are outside its scope. Estimate their share of daily work to contextualize how much of a session Serena's augmentation covers. **Verdict:** (one sentence) ---- + ### 9. Practical usage rule @@ -274,7 +311,7 @@ Provide a decision rule for choosing between toolsets based on task type. **Verdict:** (one sentence) ---- + ## What I'm looking for @@ -283,7 +320,7 @@ Provide a decision rule for choosing between toolsets based on task type. - Clear quantification of impact - Honest workflow-level comparisons ---- + ## What I am not looking for @@ -292,4 +329,5 @@ Provide a decision rule for choosing between toolsets based on task type. - Neutral statements without evidence - Binary recommendations - Novelty-driven ordering -- Unquantified claims \ No newline at end of file +- Unquantified claims +``` \ No newline at end of file diff --git a/docs/04-evaluation/020_prompts/020_summary-prompt.md b/docs/04-evaluation/020_prompts/020_summary-prompt.md new file mode 100644 index 00000000..c09fb8a6 --- /dev/null +++ b/docs/04-evaluation/020_prompts/020_summary-prompt.md @@ -0,0 +1,12 @@ +# Summary Prompt + +We used the prompt below to summarize the evaluation of Serena. + +``` +Write a one-sentence user-facing summary about the value that Serena's tools +provide for coding. With a bit of emotion but grounded in the evaluation that +you have done above. It should be written from the perspective of a coding AI +agent and make it clear whether an AI agent would ask its owner to add Serena's +tools or not. It will be the first sentence that a potential new user will see +about how useful Serena is for an AI agent. +``` \ No newline at end of file diff --git a/docs/04-evaluation/030_results/000_evaluation-results.md b/docs/04-evaluation/030_results/000_evaluation-results.md new file mode 100644 index 00000000..e0b6bfc6 --- /dev/null +++ b/docs/04-evaluation/030_results/000_evaluation-results.md @@ -0,0 +1,17 @@ +# Results + +This section presents the results of the evaluation. + +We performed evaluations using popular AI coding agents in representative scenarios — different +agents, different programming languages, and different codebases — to show that the results are +not specific to a single setup. + +All evaluations were conducted using the **JetBrains-powered version** of Serena, as it is the +more powerful backend with a broader set of refactoring and navigation capabilities. The +evaluation can easily be repeated with the LSP-based backend to assess its subset of capabilities. + +- [Claude Code (Opus 4.6) on a large Python codebase](010_cc_on_tianshou) +- [Codex (GPT 5.4) on a Java codebase](020_codex_on_jbplugin) + +You can run your own evaluation on a project of your choice by reusing our +[evaluation prompt](../020_prompts/010_evaluation-prompt.md). diff --git a/docs/04-evaluation/results/010_cc_on_tianshou-serena-evaluation.md b/docs/04-evaluation/030_results/010_cc_on_tianshou.md similarity index 99% rename from docs/04-evaluation/results/010_cc_on_tianshou-serena-evaluation.md rename to docs/04-evaluation/030_results/010_cc_on_tianshou.md index 6c5f0122..8b0b53fe 100644 --- a/docs/04-evaluation/results/010_cc_on_tianshou-serena-evaluation.md +++ b/docs/04-evaluation/030_results/010_cc_on_tianshou.md @@ -1,9 +1,13 @@ -# Evaluation Report: Serena's Tools vs Built-In Tools +:::{admonition} Evaluation Result +:class: note +**Generated by**: Claude Opus 4.6 (coding AI agent in Claude Code CLI) +**Codebase:** [Tianshou](https://github.com/thu-ml/tianshou) — a Python reinforcement learning library (~26K lines, 43 source files) +::: + +# Claude Code (Opus) > **One-line summary:** Serena's IDE-backed semantic tools are the single most impactful addition to my toolkit — cross-file renames, moves, and reference lookups that would cost me 8–12 careful, error-prone steps collapse into one atomic call, and I would absolutely ask any developer I work with to set them up. -**Evaluated by:** Claude Opus 4.6 (coding AI agent in Claude Code CLI) -**Codebase:** [tianshou](https://github.com/thu-ml/tianshou) — a Python reinforcement learning library (~26K lines, 43 source files) **Method:** Hands-on, side-by-side execution of 20 tasks using both toolsets. All edits were applied to real files and verified via `git diff`, then reverted. --- diff --git a/docs/04-evaluation/results/020_codex_on_jbplugin-serena-evaluation.md b/docs/04-evaluation/030_results/020_codex_on_jbplugin.md similarity index 99% rename from docs/04-evaluation/results/020_codex_on_jbplugin-serena-evaluation.md rename to docs/04-evaluation/030_results/020_codex_on_jbplugin.md index d403c15c..27fa09f3 100644 --- a/docs/04-evaluation/results/020_codex_on_jbplugin-serena-evaluation.md +++ b/docs/04-evaluation/030_results/020_codex_on_jbplugin.md @@ -1,14 +1,19 @@ -# Evaluation: What Serena Adds Over Built-Ins +:::{admonition} Evaluation Result +:class: note +**Generated by:** GPT-5.4 (high) in Codex +**Codebase:** Serena JetBrains Plugin (Java) +::: -**Evaluated by:** Gpt 5.4 (high) in Codex +# Codex (GPT-5.4) + + +> **One-line summary:** As a coding agent, I would ask my owner to add Serena because it turns fragile text-and-line-number work into precise symbol-aware navigation and +refactoring, which makes real code changes feel faster, safer, and far less blind. This report compares Serena's JetBrains-backed semantic coding tools with built-in file, shell, search, and patch tools in this repository. The comparison assumes competent use of both toolsets: built-ins are used for text, file, shell, config, and small patch work; Serena is used where symbol identity, language semantics, or IDE refactoring semantics apply. Method: I explored code first, avoided repo documentation and prior notes, ran real edits/refactors, and reverted after each experiment. After each edit/refactor experiment I checked `git status --short` and returned the tree to clean before moving on. Measurements are approximate, but call counts, diff sizes, and result shapes are from observed runs. -> **One-line summary:** As a coding agent, I would ask my owner to add Serena because it turns fragile text-and-line-number work into precise symbol-aware navigation and -refactoring, which makes real code changes feel faster, safer, and far less blind. - ## 1. Headline: what Serena changes Serena adds a semantic layer over the codebase. Its concrete delta is the ability to address and transform code by symbols: name paths, overload indexes, reference graphs, type hierarchies, external declarations, and JetBrains refactoring operations. diff --git a/docs/04-evaluation/results/000_intro.md b/docs/04-evaluation/results/000_intro.md deleted file mode 100644 index c32fea83..00000000 --- a/docs/04-evaluation/results/000_intro.md +++ /dev/null @@ -1,3 +0,0 @@ -# Results - -Here are the results of the evaluation for various coding agents running on various projects. diff --git a/docs/_config.yml b/docs/_config.yml index 999c6a62..42e89034 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -102,7 +102,7 @@ sphinx: local_extensions : # A list of local extensions to load by sphinx specified by "name: path" items recursive_update : false # A boolean indicating whether to overwrite the Sphinx config (true) or recursively update (false) config : # key-value pairs to directly over-ride the Sphinx configuration - master_doc: "01-about/000_intro.md" + master_doc: "01-about/000_evaluation-results.md" html_theme_options: logo: image_light: ../resources/serena-logo.svg diff --git a/docs/autogen_docs.py b/docs/autogen_docs.py index d20eaa92..6f574ab5 100644 --- a/docs/autogen_docs.py +++ b/docs/autogen_docs.py @@ -190,7 +190,7 @@ def autogen_about_intro_features(): autogen_info = f"\n\n" - with open(Path(__file__).parent / "01-about" / "000_intro.md", "w", encoding="utf-8") as f: + with open(Path(__file__).parent / "01-about" / "000_evaluation-results.md", "w", encoding="utf-8") as f: f.write(autogen_info) f.write("# About Serena\n\n") f.write(f"**{tagline}**\n\n") From f03909cf6e005ebe54d180b66fc2a10d0bfde39b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 13 Apr 2026 15:53:48 +0200 Subject: [PATCH 147/190] Add top-down intro to evaluation section --- docs/04-evaluation/000_evaluation-intro.md | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/04-evaluation/000_evaluation-intro.md b/docs/04-evaluation/000_evaluation-intro.md index 2ceeefe7..506c60d3 100644 --- a/docs/04-evaluation/000_evaluation-intro.md +++ b/docs/04-evaluation/000_evaluation-intro.md @@ -1,2 +1,30 @@ # Evaluation +> **Claude Code (Opus 4.6):** "Serena's IDE-backed semantic tools are the single most impactful addition to my toolkit — cross-file renames, moves, and reference lookups that would cost me 8–12 careful, error-prone steps collapse into one atomic call, and I would absolutely ask any developer I work with to set them up." + +> **Codex (GPT 5.4):** "As a coding agent, I would ask my owner to add Serena because it turns fragile text-and-line-number work into precise symbol-aware navigation and refactoring, which makes real code changes feel faster, safer, and far less blind." + +These are not marketing quotes — they are the agents' own one-sentence verdicts after +performing hands-on tasks each, using both Serena's tools and their built-in +alternatives side by side, on real codebases (large Python library in the case of Claude Code, +medium-sized Java project in the case of Codex). + +Both agents independently converge on the same core finding: Serena's strongest +contribution is collapsing multi-file, semantically-aware operations into single atomic +calls, while built-in tools remain the better choice for small local edits, text search, +config files, and shell work. + +## Approach + +We gave each agent a single evaluation prompt and pointed it at a codebase. The agent +then performed a wide variety of tasks across five areas (navigation, small edits, large edits, +cross-file refactoring, and workflow effects), executing each task with both toolsets and +recording call counts, payload sizes, and prerequisite steps. Every finding was classified +as either (a) Serena adds capability, (b) Serena applies but offers no improvement, +or (c) outside Serena's scope — a structure that requires reporting negative and neutral +results, not just positive ones. + +The agent evaluates itself. This is deliberate: the agent is the actual user of the tools, +so it can judge workflow improvements from direct experience rather than through proxy +metrics. And because the prompt defines task *categories* rather than fixed tasks, anyone +can rerun the evaluation on their own project with their own agent. From e6b6faee08c365d3259dcc036b283d8c627eddac Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 13 Apr 2026 16:38:45 +0200 Subject: [PATCH 148/190] Improve presentation of evaluation in README --- README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 81198d80..39108a5a 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,14 @@ more complex codebases. ## What Our "End Users" Say -While it is humans who download and set up Serena, our end users are essentially AI agents, -so they are also in the best position to evaluate Serena. -We crafted an unbiased and detailed evaluation prompt -that leads the agent to perform ~20 routine coding tasks, representative of everyday development work, using both Serena's tools and its own built-ins, -measure the differences, and report the results. A one-sentence summary of what the agents had to say: +While it is humans who download and set up Serena, our end users are essentially AI agents. +As the ones actually applying Serena's tools, they are in the best position to evaluate Serena. + +We crafted an unbiased evaluation prompt that leads the agent to perform ~20 routine coding tasks, +representative of everyday development work, +in order to compare Serena's tools with its own built-ins, measure the differences, and report the results. + +Here's a one-sentence summary of what the agents had to say: **Opus 4.6 (high effort) in Claude Code on a large Python codebase:** > "Serena's IDE-backed semantic tools are the single most impactful addition to my toolkit — cross-file renames, moves, and reference lookups that @@ -44,9 +47,9 @@ would cost me 8–12 careful, error-prone steps collapse into one atomic call, a > "As a coding AI agent, I would ask my owner to add Serena because it gives me the missing IDE-level understanding of symbols, references, and refactorings, turning fragile text surgery into calmer, faster, more confident code changes where semantics matter." -Give your agent the tools it's been asking for and add Serena MCP to your client! +Give your agent the tools it has been asking for and add Serena MCP to your client! -See our [documentation](https://oraios.github.io/serena/04-evaluation/000_intro.html) for the full methodology and much more detailed evaluation results +See our [documentation](https://oraios.github.io/serena/04-evaluation/000_evaluation-intro.html) for the full methodology and much more detailed evaluation results beyond these brief summaries, or run your own evaluation on a project of your choice. From ed061d802458eda9808c0ea5d0fa66e330aad12b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 13 Apr 2026 16:46:13 +0200 Subject: [PATCH 149/190] Fix changelog entry position --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fbb66a3..71defb80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ Status of the `main` branch. Changes prior to the next official version change w - Support environment variable `SERENA_USAGE_REPORTING` (set to `false` to disable usage reporting) - Extended the list of always ignored directories (by language servers) with common cases. +JetBrains: + - Minor fixes in handling `relative_path` for external dependencies. + * Language Servers: - Add mSL (mIRC Scripting Language) support (custom pygls-based language server; symbols, references, definitions) @@ -16,7 +19,6 @@ Status of the `main` branch. Changes prior to the next official version change w * JetBrains: - `JetBrainsRenameTool` can now also rename occurrences in comments and text. - - Minor fixes in handling `relative_path` for external dependencies. * Language Servers: - Fix Dart LSP returning only symbol name as body instead of full method body. From e2eb1f14b422a05eecdb375c80055f215ff76ee0 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 13 Apr 2026 16:52:10 +0200 Subject: [PATCH 150/190] JetBrainsFindSymbolTool: Force search_deps=True if relative path to ext. file Improve changelog entry --- CHANGELOG.md | 5 ++++- src/serena/tools/jetbrains_tools.py | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71defb80..4c0ab4b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ Status of the `main` branch. Changes prior to the next official version change w - Extended the list of always ignored directories (by language servers) with common cases. JetBrains: - - Minor fixes in handling `relative_path` for external dependencies. + - Improve handling of `relative_path` parameter + - Improve its documentation to avoid usage errors + - Replace escaped characters in `relative_path` with their unescaped counterparts (< and >) + - `FindSymbolTool`: Force `search_deps=True` if `relative_path` pertains to external dependencies. * Language Servers: - Add mSL (mIRC Scripting Language) support (custom pygls-based language server; symbols, references, definitions) diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index c91833aa..9e217090 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -74,11 +74,15 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): depth = 0 # ignore user-specified depth if body is requested name_path_pattern = self._sanitize_input_param(name_path_pattern) + if relative_path: relative_path = self._sanitize_input_param(relative_path) - if relative_path == ".": relative_path = None + + if relative_path is not None and relative_path.startswith(jb.JB_EXTERNAL_FILE_PREFIX): + search_deps = True + with JetBrainsPluginClient.from_project(self.project) as client: if include_body: include_quick_info = False From 29906d6934a39108c88db3eaa7952b2be26c8ae9 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 13 Apr 2026 16:46:37 +0200 Subject: [PATCH 151/190] Docs, clients: added entry for copilot CLI, adjusted JB assistant entry --- docs/02-usage/030_clients.md | 43 ++++++++++++++---------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index 8280d604..e608b272 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -266,6 +266,15 @@ To set this up, create the file `~/.copilot/hooks/serena-hooks.json` with the fo The `SessionStart` hook also addresses the global configuration limitation mentioned above — it will automatically prompt the agent to activate the project directory, so you no longer need to do this manually. +## Copilot CLI + +Use the interactive `/mcp add` slash command, choose Serena as the name, STDIO as the server type, and +`serena start-mcp-server --context=copilot-cli --project-from-cwd` as command. Copilot CLI will immediately notify you +that Serena is running if everything is set up correctly or display an error otherwise. + +You should add the same **hooks** as in VSCode (see above) if Copilot CLI didn't pick them up automatically. + + ## Codex (CLI and App) You can simply run `serena setup codex`. @@ -356,30 +365,11 @@ Alternatively, add the following to `~/.copilot/mcp-config.json` (create the fil Copilot should now show that Serena is running, though you may have to restart it. -## JetBrains Junie +## JetBrains Junie For the Junie plugin in JetBrains IDEs, go to the three dots in the top right corner, then Settings / MCP Settings and add Serena to Junie's global MCP server configuration: -```json -{ - "mcpServers": { - "serena": { - "command": "serena", - "args": [ - "start-mcp-server", - "--context=junie" - ] - } - } -} -``` - -You will have to prompt Junie to "Activate the current project using serena's activation tool" at the -start of each session. - -For Junie CLI, where the project can be activated automatically on startup, edit `~/.junie/mcp/mcp.json` and add - ```json { "mcpServers": { @@ -395,12 +385,14 @@ For Junie CLI, where the project can be activated automatically on startup, edit } ``` +You will have to prompt Junie to "Activate the current project using serena's activation tool" at the +start of each session. + +For Junie CLI, add the same setting to `~/.junie/mcp/mcp.json`. + ## JetBrains AI Assistant -Here you can set up the more convenient per-project MCP server configuration, as the AI assistant supports specifying -the launch working directory. - -Go to Settings / Tools / AI Assistant / MCP and add a new **local** configuration via the `as JSON` option: +Go to Settings / Tools / AI Assistant / MCP and add a new **global** configuration: ```json { @@ -410,8 +402,7 @@ Go to Settings / Tools / AI Assistant / MCP and add a new **local** configuratio "args": [ "start-mcp-server", "--context=jb-ai-assistant", - "--project", - "$(pwd)" + "--project-from-cwd" ] } } From 57dec4c7cac1b06abd40a0aee430cb8b54a5a580 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 13 Apr 2026 17:11:46 +0200 Subject: [PATCH 152/190] Followup after 34e1fae7 - added more ignored directories --- scripts/demo_run_tools.py | 15 +++++++++------ .../language_servers/ccls_language_server.py | 9 +++++++++ .../language_servers/clangd_language_server.py | 9 +++++++++ src/solidlsp/language_servers/clojure_lsp.py | 7 +++++++ src/solidlsp/language_servers/ruby_lsp.py | 1 + src/solidlsp/language_servers/solargraph.py | 1 + src/solidlsp/language_servers/taplo_server.py | 10 ++++++++-- src/solidlsp/ls.py | 1 + 8 files changed, 45 insertions(+), 8 deletions(-) diff --git a/scripts/demo_run_tools.py b/scripts/demo_run_tools.py index 5d2b3358..d382dc32 100644 --- a/scripts/demo_run_tools.py +++ b/scripts/demo_run_tools.py @@ -7,6 +7,8 @@ import json from pathlib import Path from pprint import pprint +from toon_format import encode + from serena.agent import SerenaAgent from serena.config.serena_config import SerenaConfig from serena.constants import REPO_ROOT @@ -24,7 +26,7 @@ if __name__ == "__main__": serena_config = SerenaConfig.from_config_file() serena_config.web_dashboard = False # project = Path(REPO_ROOT).parent / "serena-jetbrains-plugin-copy" - project = Path(REPO_ROOT) / "test/resources/repos/python/test_repo" + project = Path(REPO_ROOT) agent = SerenaAgent(project=str(project), serena_config=serena_config) # apply a tool @@ -37,11 +39,12 @@ if __name__ == "__main__": inline_symbol = agent.get_tool(JetBrainsInlineSymbol) result = agent.execute_task( - lambda: inline_symbol.apply( - relative_path="test_repo/nested.py", - name_path="OuterClass/NestedClass", - keep_definition=True, + lambda: overview_tool.apply( + # name_path_pattern="SerenaAgent", + relative_path="src/serena/agent.py", + depth=2, + # keep_definition=True, ) ) - pprint(json.loads(result)) + pprint(encode(json.loads(result))) # input("Press Enter to continue...") diff --git a/src/solidlsp/language_servers/ccls_language_server.py b/src/solidlsp/language_servers/ccls_language_server.py index b9897172..252839ab 100644 --- a/src/solidlsp/language_servers/ccls_language_server.py +++ b/src/solidlsp/language_servers/ccls_language_server.py @@ -34,6 +34,8 @@ import pathlib import threading from typing import Any, cast +from overrides import override + from solidlsp.ls import ( LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, @@ -67,6 +69,13 @@ class CCLS(SolidLanguageServer): def _create_dependency_provider(self) -> LanguageServerDependencyProvider: return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + @override + def is_ignored_dirname(self, dirname: str) -> bool: + ignored_dirs = [ + ".ccls-cache", + ] + return super().is_ignored_dirname(dirname) or dirname in ignored_dirs + class DependencyProvider(LanguageServerDependencyProviderSinglePath): def _get_or_install_core_dependency(self) -> str: """ diff --git a/src/solidlsp/language_servers/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server.py index c5488810..0210e7d5 100644 --- a/src/solidlsp/language_servers/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server.py @@ -5,6 +5,8 @@ import pathlib import threading from typing import Any, cast +from overrides import override + from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, ProcessLaunchInfo, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams @@ -40,6 +42,13 @@ class ClangdLanguageServer(SolidLanguageServer): self.initialize_searcher_command_available = threading.Event() self.resolve_main_method_available = threading.Event() + @override + def is_ignored_dirname(self, dirname: str) -> bool: + ignored_dirs = [ + ".ccls-cache", + ] + return super().is_ignored_dirname(dirname) or dirname in ignored_dirs + def _prepare_compile_commands(self) -> str | None: """ Prepare clangd compilation database with absolute directory paths. diff --git a/src/solidlsp/language_servers/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp.py index 3fe17de1..4fdc3ce9 100644 --- a/src/solidlsp/language_servers/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp.py @@ -10,6 +10,8 @@ import subprocess import threading from typing import cast +from overrides import override + from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams @@ -59,6 +61,11 @@ class ClojureLSP(SolidLanguageServer): CLOJURE_LSP_VERSION = CLOJURE_LSP_VERSION CLOJURE_LSP_ALLOWED_HOSTS = CLOJURE_LSP_ALLOWED_HOSTS + @override + def is_ignored_dirname(self, dirname: str) -> bool: + ignored_dirs = [".clj-kondo", ".lsp", ".cpcache"] + return super().is_ignored_dirname(dirname) or dirname in ignored_dirs + @classmethod def _runtime_dependencies(cls, version: str) -> RuntimeDependencyCollection: clojure_lsp_releases = f"https://github.com/clojure-lsp/clojure-lsp/releases/download/{version}" diff --git a/src/solidlsp/language_servers/ruby_lsp.py b/src/solidlsp/language_servers/ruby_lsp.py index 7bd56256..fd72b289 100644 --- a/src/solidlsp/language_servers/ruby_lsp.py +++ b/src/solidlsp/language_servers/ruby_lsp.py @@ -67,6 +67,7 @@ class RubyLsp(SolidLanguageServer): "public/packs", # Webpacker output "public/webpack", # Webpack output "public/assets", # Rails compiled assets + ".ruby-lsp", ] return super().is_ignored_dirname(dirname) or dirname in ruby_ignored_dirs diff --git a/src/solidlsp/language_servers/solargraph.py b/src/solidlsp/language_servers/solargraph.py index 62a77749..f42f0854 100644 --- a/src/solidlsp/language_servers/solargraph.py +++ b/src/solidlsp/language_servers/solargraph.py @@ -67,6 +67,7 @@ class Solargraph(SolidLanguageServer): "doc", # Generated documentation "node_modules", # Node modules (for Rails with JS) "storage", # Active Storage files (Rails) + ".ruby-lsp", ] return super().is_ignored_dirname(dirname) or dirname in ruby_ignored_dirs diff --git a/src/solidlsp/language_servers/taplo_server.py b/src/solidlsp/language_servers/taplo_server.py index a832e13e..410edde1 100644 --- a/src/solidlsp/language_servers/taplo_server.py +++ b/src/solidlsp/language_servers/taplo_server.py @@ -14,6 +14,8 @@ import shutil import stat from typing import Any +from overrides import override + from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_utils import FileUtils, PathUtils @@ -266,6 +268,10 @@ class TaploServer(SolidLanguageServer): log.info("Taplo server initialization complete") + @override def is_ignored_dirname(self, dirname: str) -> bool: - """Define TOML-specific directories to ignore.""" - return super().is_ignored_dirname(dirname) or dirname in ["target", ".cargo", "node_modules"] + if dirname == ".vscode": + # special case, one might want to analyze toml files in .vscode + return False + dirnames_to_ignore = ["target", ".cargo", "node_modules"] + return dirname in dirnames_to_ignore or super().is_ignored_dirname(dirname) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 9dda6f33..4a1f5986 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -371,6 +371,7 @@ class SolidLanguageServer(ABC): ".nox", # test runners ".idea", # IDE internals ".serena", # serena's own data + ".vscode", # Doesn't contain symbols } ) From 0c664b365c32ad375258a7496ce6d766c03dfd8a Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 14 Apr 2026 00:04:51 +0200 Subject: [PATCH 153/190] CI: remove doc-build from pytest workflow, instead run the docs job on PR Docs upload will only happen on main --- .github/workflows/docs.yaml | 6 +++++- .github/workflows/pytest.yml | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index e62d5cd2..72bb5c00 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -1,8 +1,10 @@ name: Docs Build on: + pull_request: push: - branches: [ main ] + branches: + - main workflow_dispatch: permissions: @@ -47,11 +49,13 @@ jobs: continue-on-error: false - name: Upload Pages artifact + if: github.ref == 'refs/heads/main' uses: actions/upload-pages-artifact@v3 with: path: docs/_build deploy: + if: github.ref == 'refs/heads/main' environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 45601361..f1d969e9 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -576,5 +576,3 @@ jobs: - name: Type-checking with mypy shell: bash run: uv run poe type-check - - name: Make sure documentation builds - run: uv run poe doc-build From eb9198fec7f735ad3d1237dbd65a06d02bea347c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:07:10 +0000 Subject: [PATCH 154/190] Bump pillow from 12.1.1 to 12.2.0 in the uv group across 1 directory Bumps the uv group with 1 update in the / directory: [pillow](https://github.com/python-pillow/Pillow). Updates `pillow` from 12.1.1 to 12.2.0 - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/12.1.1...12.2.0) --- updated-dependencies: - dependency-name: pillow dependency-version: 12.2.0 dependency-type: indirect dependency-group: uv ... Signed-off-by: dependabot[bot] --- uv.lock | 162 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/uv.lock b/uv.lock index c89fecb4..2e22e6f7 100644 --- a/uv.lock +++ b/uv.lock @@ -1801,89 +1801,89 @@ wheels = [ [[package]] name = "pillow" -version = "12.1.1" +version = "12.2.0" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } 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/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, - { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, - { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, - { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, - { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, - { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, - { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, - { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, - { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, - { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, - { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, - { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, - { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, - { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, - { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, - { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, - { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, - { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, - { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, - { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, - { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, - { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, - { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, - { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, - { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, - { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, - { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, - { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, - { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, - { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, - { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, - { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, - { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, - { 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" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, ] [[package]] From 562bfeaa3b8cd35f20a537a4835977316c561596 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 14 Apr 2026 11:16:05 +0200 Subject: [PATCH 155/190] Fix typo in docs (accidental rename) --- docs/autogen_docs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/autogen_docs.py b/docs/autogen_docs.py index 6f574ab5..d20eaa92 100644 --- a/docs/autogen_docs.py +++ b/docs/autogen_docs.py @@ -190,7 +190,7 @@ def autogen_about_intro_features(): autogen_info = f"\n\n" - with open(Path(__file__).parent / "01-about" / "000_evaluation-results.md", "w", encoding="utf-8") as f: + with open(Path(__file__).parent / "01-about" / "000_intro.md", "w", encoding="utf-8") as f: f.write(autogen_info) f.write("# About Serena\n\n") f.write(f"**{tagline}**\n\n") From 1558a4e3bcf09ac333a7ef0e9eff33a02a9d77fe Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 14 Apr 2026 11:19:15 +0200 Subject: [PATCH 156/190] Vue: fix, ensure LS is operational before accepting requests --- .../language_servers/vue_language_server.py | 88 ++++++++++++++----- 1 file changed, 65 insertions(+), 23 deletions(-) diff --git a/src/solidlsp/language_servers/vue_language_server.py b/src/solidlsp/language_servers/vue_language_server.py index 3ded6210..95d08732 100644 --- a/src/solidlsp/language_servers/vue_language_server.py +++ b/src/solidlsp/language_servers/vue_language_server.py @@ -156,6 +156,48 @@ class VueLanguageServer(SolidLanguageServer): self._ts_server_started = False self._vue_files_indexed = False self._indexed_vue_file_uris: list[str] = [] + self._ls_operational_ready_event = threading.Event() + self._ls_operational_lock = threading.Lock() + self._ls_operational_thread: threading.Thread | None = None + + def _warm_up_ls_operational_state(self) -> None: + """Warm up the Vue language server operational state asynchronously.""" + # execute the operational warm-up + try: + self._ensure_ls_operational() + except SolidLSPException: + if not self.server_started: + log.debug("Skipping Vue language server operational warm-up because the server is stopping") + return + log.exception("Error while warming up Vue language server operational state") + except Exception: + log.exception("Error while warming up Vue language server operational state") + + def _ensure_ls_operational(self) -> None: + # short-circuit completed warm-up + if self._ls_operational_ready_event.is_set(): + return + + # serialize the warm-up sequence + with self._ls_operational_lock: + # short-circuit repeated callers after waiting for the lock + if self._ls_operational_ready_event.is_set(): + return + + # validate server availability + if not self.server_started: + raise SolidLSPException("Language Server not started") + + # wait for cross-file reference readiness + if not self._has_waited_for_cross_file_references: + sleep(self._get_wait_time_for_cross_file_referencing()) + self._has_waited_for_cross_file_references = True + + # index Vue files on the companion TypeScript server + self._ensure_vue_files_indexed_on_ts_server() + + # publish operational readiness + self._ls_operational_ready_event.set() @override def is_ignored_dirname(self, dirname: str) -> bool: @@ -283,9 +325,7 @@ class VueLanguageServer(SolidLanguageServer): return result def request_file_references(self, relative_file_path: str) -> list: - if not self.server_started: - log.error("request_file_references called before Language Server started") - raise SolidLSPException("Language Server not started") + self._ensure_ls_operational() absolute_file_path = os.path.join(self.repository_root_path, relative_file_path) uri = PathUtils.path_to_uri(absolute_file_path) @@ -348,15 +388,7 @@ class VueLanguageServer(SolidLanguageServer): @override def request_references(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]: - if not self.server_started: - log.error("request_references called before Language Server started") - raise SolidLSPException("Language Server not started") - - if not self._has_waited_for_cross_file_references: - sleep(self._get_wait_time_for_cross_file_referencing()) - self._has_waited_for_cross_file_references = True - - self._ensure_vue_files_indexed_on_ts_server() + self._ensure_ls_operational() symbol_refs = self._send_ts_references_request(relative_file_path, line=line, column=column) if relative_file_path.endswith(".vue"): @@ -381,20 +413,14 @@ class VueLanguageServer(SolidLanguageServer): @override def request_definition(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]: - if not self.server_started: - log.error("request_definition called before Language Server started") - raise SolidLSPException("Language Server not started") - + self._ensure_ls_operational() assert self._ts_server is not None with self._ts_server.open_file(relative_file_path): return self._ts_server.request_definition(relative_file_path, line, column) @override def request_rename_symbol_edit(self, relative_file_path: str, line: int, column: int, new_name: str) -> ls_types.WorkspaceEdit | None: - if not self.server_started: - log.error("request_rename_symbol_edit called before Language Server started") - raise SolidLSPException("Language Server not started") - + self._ensure_ls_operational() assert self._ts_server is not None with self._ts_server.open_file(relative_file_path): return self._ts_server.request_rename_symbol_edit(relative_file_path, line, column, new_name) @@ -698,6 +724,15 @@ class VueLanguageServer(SolidLanguageServer): else: log.info("Vue server initialization complete") + # kick off asynchronous operational warm-up + self._ls_operational_ready_event.clear() + self._ls_operational_thread = threading.Thread( + target=self._warm_up_ls_operational_state, + name="vue-ls-operational-warmup", + daemon=True, + ) + self._ls_operational_thread.start() + def _find_tsconfig_for_file(self, file_path: str) -> str | None: if not file_path: tsconfig_path = os.path.join(self.repository_root_path, "tsconfig.json") @@ -724,8 +759,14 @@ class VueLanguageServer(SolidLanguageServer): @override def stop(self, shutdown_timeout: float = 5.0) -> None: - self._cleanup_indexed_vue_files() - self._stop_typescript_server() + # serialize shutdown with operational warm-up + with self._ls_operational_lock: + self.server_started = False + self._ls_operational_ready_event.clear() + self._cleanup_indexed_vue_files() + self._stop_typescript_server() + self._ls_operational_thread = None + super().stop(shutdown_timeout) @override @@ -760,8 +801,9 @@ class VueLanguageServer(SolidLanguageServer): return self._filter_shorthand_property_duplicates(symbols) + @staticmethod def _filter_shorthand_property_duplicates( - self, symbols: list[DocumentSymbol] | list[SymbolInformation] + symbols: list[DocumentSymbol] | list[SymbolInformation], ) -> list[DocumentSymbol] | list[SymbolInformation]: """ Filter out Property symbols that have a matching Variable symbol with the same name. From 9ba20bf69514911b1ec23d488321507aa7e44cb2 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 14 Apr 2026 11:28:27 +0200 Subject: [PATCH 157/190] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c0ab4b7..6c4842ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ JetBrains: * Language Servers: - Add mSL (mIRC Scripting Language) support (custom pygls-based language server; symbols, references, definitions) + - Fix initialisation issues in Vue language server #1333 # 1.1.1 From 4e971a6176efb7ca5358558d41627488cbf19675 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 14 Apr 2026 12:13:01 +0200 Subject: [PATCH 158/190] Update dashboard docs: Tray icon support is currently limited to Windows Adjust docs to be more vague and therefore safe (even if we change this again) --- docs/02-usage/060_dashboard.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/02-usage/060_dashboard.md b/docs/02-usage/060_dashboard.md index 3e4e3a5a..8e828aec 100644 --- a/docs/02-usage/060_dashboard.md +++ b/docs/02-usage/060_dashboard.md @@ -19,10 +19,10 @@ Both can be configured in Serena's [configuration](050_configuration) file (`ser ## The Serena Dashboard -The dashboard is a web application, which is opened via a native application wrapper on Windows and macOS, +The dashboard is a web application, which is opened via a native application wrapper on supported platforms (e.g. Windows), where it will appear in the system tray. **Click on the tray icon** to open the application window. -On Linux, native app support is difficult to provide, so the dashboard will be opened in your default web browser instead. +On other platforms, where native app support is difficult to provide (e.g. Linux), 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. @@ -54,15 +54,14 @@ In particular, you can configure 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 +If you prefer not to have this happen (e.g., to avoid focus stealing), you can 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 +On platforms supporting the tray icon, 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 +On other platforms, 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) From ac2b2ac1313398aa23769bd4984c24e4ec566d14 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 14 Apr 2026 12:21:52 +0200 Subject: [PATCH 159/190] GitignoreParser._iter_gitignore_files: Account for ValueError in relpath Fixes #1317 --- CHANGELOG.md | 2 ++ src/serena/util/file_system.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c4842ce..4542aa56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ Status of the `main` branch. Changes prior to the next official version change w * General: - Support environment variable `SERENA_USAGE_REPORTING` (set to `false` to disable usage reporting) - Extended the list of always ignored directories (by language servers) with common cases. + - Fix: When scanning for `.gitignore` files, the presence of files that could not be made relative + to the project root would cause the scan to fail. #1317 JetBrains: - Improve handling of `relative_path` parameter diff --git a/src/serena/util/file_system.py b/src/serena/util/file_system.py index 318cb571..8240653d 100644 --- a/src/serena/util/file_system.py +++ b/src/serena/util/file_system.py @@ -177,7 +177,11 @@ class GitignoreParser: while queue: next_abs_path = queue.pop(0) if next_abs_path != self.repo_root: - rel_path = os.path.relpath(next_abs_path, self.repo_root) + try: + rel_path = os.path.relpath(next_abs_path, self.repo_root) + except ValueError: + # If the path is on a different drive (Windows) or cannot be made relative for another reason, we ignore it + continue if self.should_ignore(rel_path): continue yield from scan(next_abs_path) From 82aacb18478b77cdcb429c85a0fa519086acfefc Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 14 Apr 2026 13:02:22 +0200 Subject: [PATCH 160/190] Fix translation from README to docs (once again) --- docs/autogen_docs.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/autogen_docs.py b/docs/autogen_docs.py index d20eaa92..2043017c 100644 --- a/docs/autogen_docs.py +++ b/docs/autogen_docs.py @@ -198,9 +198,14 @@ def autogen_about_intro_features(): # adjust link about_text = about_text.replace("resources/serena-block-diagram.svg", "https://raw.githubusercontent.com/oraios/serena/main/resources/serena-block-diagram.svg") - # remove statements with links that are invalid and the callout + # remove centred links + about_text = re.subn(r'^
    .*?
    \s*
    $', "", about_text, flags=re.MULTILINE | re.DOTALL)[0] + + # remove statements with links to quick start guide about_text = re.subn(r"^.*\[Quick Start.*?$", r"", about_text, flags=re.MULTILINE)[0] - about_text = re.subn(r"^>.*?$", "", about_text, flags=re.MULTILINE)[0] + + # remove callouts + about_text = re.subn(r"^> \[!\w+\]\s+(^>.*?$)+", "", about_text, flags=re.MULTILINE)[0] f.write(f"{about_text}\n\n") From 00726f5fdc293788f75170f3aaded90495f7f368 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 14 Apr 2026 14:43:17 +0200 Subject: [PATCH 161/190] Add date to changelog headings, add 'v' prefix Fixes #1337 --- CHANGELOG.md | 10 +++++----- scripts/bump_version.py | 5 ++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4542aa56..92a95e73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ JetBrains: - Add mSL (mIRC Scripting Language) support (custom pygls-based language server; symbols, references, definitions) - Fix initialisation issues in Vue language server #1333 -# 1.1.1 +# v1.1.1 (2026-04-12) * General: - Enable cert verification for HTTPS request to oraios-software.de #1320 @@ -30,7 +30,7 @@ JetBrains: - Fix Dart LSP returning only symbol name as body instead of full method body. -# 1.1.0 +# v1.1.0 (2026-04-11) * General: - **Major**: Add commands for hooks and documentation of recommended setup. Consider setting up the [recommended hooks](https://oraios.github.io/serena/02-usage/030_clients.html) ! @@ -53,7 +53,7 @@ JetBrains: Some clients would terminate the MCP server in a way that did not ensure proper termination. - Fix: Manual server shutdown triggered by GUI tool/dashboard not cleaning everything up. -# 1.0.0 +# v1.0.0 (2026-04-03) * General: * Add monorepo/multi-language support @@ -134,7 +134,7 @@ JetBrains: * **C/C++ alternate LS (ccls)**: Add experimental, opt-in support for ccls as an alternative backend to clangd. Enable via `cpp_ccls` in project configuration. Requires `ccls` installed and ideally a `compile_commands.json` at repo root. * **Add support for Solidity** via the Nomic Foundation `@nomicfoundation/solidity-language-server` (automatically installed via npm) -# 0.1.4 +# v0.1.4 (2025-08-15) ## Summary @@ -178,7 +178,7 @@ Fixes: default shell reconfiguration imposed by Claude Code) * Additional wait for initialization in C# language server before requesting references, allowing cross-file references to be found. -# 0.1.3 +# v0.1.3 (2025-07-22) ## Summary diff --git a/scripts/bump_version.py b/scripts/bump_version.py index e3426baa..7647da78 100644 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import os import re +from datetime import datetime from pathlib import Path from typing import Literal @@ -185,7 +186,9 @@ def update_changelog(changelog_text: str, new_version: str) -> str: unreleased_body = unreleased_section[len(_UNRELEASED_HEADER) :] intro, unreleased_entries = split_unreleased_body(unreleased_body) - updated_section = _UNRELEASED_HEADER + intro + f"# {new_version}\n" + date_str = datetime.now().strftime("%Y-%m-%d") + updated_section = _UNRELEASED_HEADER + intro + f"# v{new_version} ({date_str})\n" + if unreleased_entries.strip(): updated_section += "\n" + unreleased_entries.lstrip("\n") else: From b2729c2a3efcd214d4c1fdd8d3497ba7be1719ce Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 14 Apr 2026 14:56:42 +0200 Subject: [PATCH 162/190] SerenaAgentMode: Fix print_overview --- src/serena/config/context_mode.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/serena/config/context_mode.py b/src/serena/config/context_mode.py index e417fe32..053f54ee 100644 --- a/src/serena/config/context_mode.py +++ b/src/serena/config/context_mode.py @@ -53,7 +53,13 @@ class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin): """Print an overview of the mode.""" print(f"{self.name}:\n {self.description}") if self.excluded_tools: - print(" excluded tools:\n " + ", ".join(sorted(self.excluded_tools))) + print(" excluded tools:\n " + ", ".join(sorted(self.excluded_tools))) + if self.included_optional_tools: + print(" included optional tools:\n " + ", ".join(sorted(self.included_optional_tools))) + if self.fixed_tools: + print(" fixed tools:\n " + ", ".join(sorted(self.fixed_tools))) + if self.prompt: + print(" defines initial prompt") @classmethod def from_yaml(cls, yaml_path: str | Path) -> Self: From 510a4fe3004113f9e9c0773e6d43ebe41ce98a31 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 14 Apr 2026 15:10:05 +0200 Subject: [PATCH 163/190] Added copilot cli eval, minor changes in eval wording --- README.md | 18 +- docs/04-evaluation/000_evaluation-intro.md | 14 +- .../030_results/000_evaluation-results.md | 1 + .../030_results/010_cc_on_tianshou.md | 2 +- .../030_results/020_codex_on_jbplugin.md | 2 +- .../030_results/030_copilot_cli_on_ente.md | 348 ++++++++++++++++++ 6 files changed, 372 insertions(+), 13 deletions(-) create mode 100644 docs/04-evaluation/030_results/030_copilot_cli_on_ente.md diff --git a/README.md b/README.md index 39108a5a..0473535a 100644 --- a/README.md +++ b/README.md @@ -35,22 +35,28 @@ As the ones actually applying Serena's tools, they are in the best position to e We crafted an unbiased evaluation prompt that leads the agent to perform ~20 routine coding tasks, representative of everyday development work, -in order to compare Serena's tools with its own built-ins, measure the differences, and report the results. +in order to estimate the value added by Serena's tools when used alongside its own built-ins. Here's a one-sentence summary of what the agents had to say: -**Opus 4.6 (high effort) in Claude Code on a large Python codebase:** -> "Serena's IDE-backed semantic tools are the single most impactful addition to my toolkit — cross-file renames, moves, and reference lookups that +**Opus 4.6 (high) in Claude Code on a large Python codebase:** +> "Serena's IDE-backed semantic tools are the single most impactful addition to my toolkit – cross-file renames, moves, and reference lookups that would cost me 8–12 careful, error-prone steps collapse into one atomic call, and I would absolutely ask any developer I work with to set them up." **GPT 5.4 (high) in Codex CLI on a Java codebase:** > "As a coding AI agent, I would ask my owner to add Serena because it gives me the missing IDE-level understanding of symbols, references, and refactorings, turning fragile text surgery into calmer, faster, more confident code changes where semantics matter." -Give your agent the tools it has been asking for and add Serena MCP to your client! +**GPT 5.4 (medium) in Copilot CLI on a large, multi-language monorepo:** +> "As a coding agent, I’d absolutely ask my owner to add Serena because it makes me noticeably sharper and calmer on +real code – especially symbol-aware navigation, cross-file refactors, and monorepo dependency jumps – while I still lean +on built-ins for tiny text edits and non-code work." -See our [documentation](https://oraios.github.io/serena/04-evaluation/000_evaluation-intro.html) for the full methodology and much more detailed evaluation results -beyond these brief summaries, or run your own evaluation on a project of your choice. +Different agents in different settings independently converge on the same verdict. + +_Give your agent the tools it has been asking for and add Serena MCP to your client!_ + +See our [documentation](https://oraios.github.io/serena/04-evaluation/000_evaluation-intro.html) for the full methodology and much more detailed evaluation results, or run your own evaluation on a project of your choice. ## How Serena Works diff --git a/docs/04-evaluation/000_evaluation-intro.md b/docs/04-evaluation/000_evaluation-intro.md index 506c60d3..64efedee 100644 --- a/docs/04-evaluation/000_evaluation-intro.md +++ b/docs/04-evaluation/000_evaluation-intro.md @@ -1,15 +1,19 @@ # Evaluation -> **Claude Code (Opus 4.6):** "Serena's IDE-backed semantic tools are the single most impactful addition to my toolkit — cross-file renames, moves, and reference lookups that would cost me 8–12 careful, error-prone steps collapse into one atomic call, and I would absolutely ask any developer I work with to set them up." +> **Claude Code (Opus 4.6, medium):** "Serena's IDE-backed semantic tools are the single most impactful addition to my toolkit — cross-file renames, moves, and reference lookups that would cost me 8–12 careful, error-prone steps collapse into one atomic call, and I would absolutely ask any developer I work with to set them up." -> **Codex (GPT 5.4):** "As a coding agent, I would ask my owner to add Serena because it turns fragile text-and-line-number work into precise symbol-aware navigation and refactoring, which makes real code changes feel faster, safer, and far less blind." +> **Codex (GPT 5.4, high):** "As a coding agent, I would ask my owner to add Serena because it turns fragile text-and-line-number work into precise symbol-aware navigation and refactoring, which makes real code changes feel faster, safer, and far less blind." -These are not marketing quotes — they are the agents' own one-sentence verdicts after -performing hands-on tasks each, using both Serena's tools and their built-in +> **Codex (GPT 5.4, medium):** "As a coding agent, I’d absolutely ask my owner to add Serena because it makes me noticeably sharper and calmer on +real code – especially symbol-aware navigation, cross-file refactors, and monorepo dependency jumps – while I still lean +on built-ins for tiny text edits and non-code work." + +These are not marketing quotes – they are the agents' own one-sentence verdicts after +performing hands-on tasks, using both Serena's tools and their built-in alternatives side by side, on real codebases (large Python library in the case of Claude Code, medium-sized Java project in the case of Codex). -Both agents independently converge on the same core finding: Serena's strongest +Different agents in different settings independently converge on the same core finding: Serena's strongest contribution is collapsing multi-file, semantically-aware operations into single atomic calls, while built-in tools remain the better choice for small local edits, text search, config files, and shell work. diff --git a/docs/04-evaluation/030_results/000_evaluation-results.md b/docs/04-evaluation/030_results/000_evaluation-results.md index e0b6bfc6..eecd29e8 100644 --- a/docs/04-evaluation/030_results/000_evaluation-results.md +++ b/docs/04-evaluation/030_results/000_evaluation-results.md @@ -12,6 +12,7 @@ evaluation can easily be repeated with the LSP-based backend to assess its subse - [Claude Code (Opus 4.6) on a large Python codebase](010_cc_on_tianshou) - [Codex (GPT 5.4) on a Java codebase](020_codex_on_jbplugin) +- [Copilot CLI (GPT 5.4) on a large, multi-language monorepo](030_copilot_cli_on_ente.md) You can run your own evaluation on a project of your choice by reusing our [evaluation prompt](../020_prompts/010_evaluation-prompt.md). diff --git a/docs/04-evaluation/030_results/010_cc_on_tianshou.md b/docs/04-evaluation/030_results/010_cc_on_tianshou.md index 8b0b53fe..763f5f6f 100644 --- a/docs/04-evaluation/030_results/010_cc_on_tianshou.md +++ b/docs/04-evaluation/030_results/010_cc_on_tianshou.md @@ -4,7 +4,7 @@ **Codebase:** [Tianshou](https://github.com/thu-ml/tianshou) — a Python reinforcement learning library (~26K lines, 43 source files) ::: -# Claude Code (Opus) +# Claude Code (Opus 4.6, medium) > **One-line summary:** Serena's IDE-backed semantic tools are the single most impactful addition to my toolkit — cross-file renames, moves, and reference lookups that would cost me 8–12 careful, error-prone steps collapse into one atomic call, and I would absolutely ask any developer I work with to set them up. diff --git a/docs/04-evaluation/030_results/020_codex_on_jbplugin.md b/docs/04-evaluation/030_results/020_codex_on_jbplugin.md index 27fa09f3..ee0f78d8 100644 --- a/docs/04-evaluation/030_results/020_codex_on_jbplugin.md +++ b/docs/04-evaluation/030_results/020_codex_on_jbplugin.md @@ -4,7 +4,7 @@ **Codebase:** Serena JetBrains Plugin (Java) ::: -# Codex (GPT-5.4) +# Codex (GPT-5.4, high) > **One-line summary:** As a coding agent, I would ask my owner to add Serena because it turns fragile text-and-line-number work into precise symbol-aware navigation and diff --git a/docs/04-evaluation/030_results/030_copilot_cli_on_ente.md b/docs/04-evaluation/030_results/030_copilot_cli_on_ente.md new file mode 100644 index 00000000..bef518e2 --- /dev/null +++ b/docs/04-evaluation/030_results/030_copilot_cli_on_ente.md @@ -0,0 +1,348 @@ +:::{admonition} Evaluation Result +:class: note +**Generated by:** GPT-5.4 (medium) in Copilot Cli +**Codebase:** [ente](https://github.com/ente-io/ente) - a large monorepo in Dart, TypeScript, Go, Rust, and other languages. +::: + +# Copilot CLI (GPT-5.4, medium) + +> As a coding agent, I’d absolutely ask my owner to add Serena because it makes me noticeably sharper and calmer on +real code—especially symbol-aware navigation, cross-file refactors, and monorepo dependency jumps—while I still lean +on built-ins for tiny text edits and non-code work + + +**Baseline.** I started from source code only, avoided repo docs/notes, and ran each reversible experiment against the repo directly. After every edit/refactor experiment, the working tree returned to its baseline state. + +## 1. Headline: what Serena changes + +Serena changes the workflow **when the task is about code symbols rather than raw text**. In this repo, the practical delta was: + +1. **Added capability / materially better workflow.** Symbol-aware navigation and refactors in TypeScript: structural overviews, precise code-only references, hierarchy queries, symbol-targeted rename, file move with import updates, and inline. These usually collapsed a 2-6 step built-in chain into **one semantic operation after discovery**, and reduced manual scope verification. +2. **Applies but offers little or no improvement.** Small local edits inside an already-understood method. Built-ins can patch only the changed lines; Serena's body replacement resends the whole symbol, so it was often **less payload-efficient** for 1-3 line tweaks. +3. **Outside Serena's scope.** Non-code reads, free-text search, git inspection, config/package files, and other text-first tasks. Built-ins remained the natural tools there. + +Two important observed limits constrained Serena's delta here: **the strongest measured gains were concentrated in the TypeScript desktop app and the Rust core crate where I ran the hands-on comparisons**, and **some refactors still carried diff-shape tradeoffs** such as formatting churn or unexpected target-file choices. + +**Verdict:** In this repo, Serena was a strong TypeScript symbol layer on top of the built-ins, not a general replacement for text/file work. + +## 2. Added value and differences by area + +| Area | What changed vs built-ins | Frequency | Value per hit | +| --- | --- | --- | --- | +| **Cross-file symbol refactors** | `rename`, file `move`, and `inline` turned manual search/edit/update chains into one semantic op. The `wait -> delay` rename updated 4 files from 1 symbol definition; moving `http.ts` updated the importing file automatically. | Medium | High: typically **2-5 calls saved** plus less manual scope checking | +| **Code-only discovery** | Symbol overview, symbol body retrieval, reference search, and type hierarchy returned code structure directly instead of raw text matches. For `wait`, Serena returned **3 real code-use files**; `rg` returned **7 files**, including docs/comments and English-word hits. | High | Medium: usually **1-3 follow-up reads/filters avoided** | +| **Stable addressing** | Name paths stayed reusable across multiple edits (`createMainWindow`, `openStreetMapUserAgent`, `wait`); built-in line ranges had to be reacquired after edits. | Medium | Medium: less re-reading, less stale-context risk | +| **Small in-method edits** | Serena was not more efficient. Replacing `AutoLauncher/toggleAutoLaunch` required resending the full method body, while the built-in patch changed only the touched lines. | High | Low negative: built-ins used **smaller edit payloads** | +| **External dependency lookup in a monorepo** | Once indexing was available, Serena resolved Electron types from `desktop/node_modules`, `next-electron-server` declarations from the desktop package, and Rust crate symbols from Cargo registry sources. In a monorepo, that removes a manual "which package owns this dependency?" step. | Medium | Medium-High: usually **1-3 searches plus path discovery avoided** | + +**Verdict:** Serena's highest-value delta was semantic refactoring plus dependency-aware code lookup in the TypeScript/Rust parts of the monorepo; its weakest area was tiny local edits. + +## 3. Detailed evidence, grouped by capability + +### 3.1 Codebase understanding + +#### Task 1: high-level repository overview + +- **Attempted:** top-level layout and likely code-heavy areas. +- **Serena chain:** `serena-list_dir(.)` -> directory list. +- **Built-in chain:** not separately needed; Serena added no unique value beyond a normal directory listing here. +- **Payloads:** one short directory listing either way. +- **Finding:** **no meaningful delta**; this is plain filesystem exploration. + +**Verdict:** For repo layout, Serena was neutral. + +#### Task 2: structural overview of a large file, plus the concrete next step + +- **Target file:** `desktop/src/main.ts` (753 lines). +- **Serena chain:** `get_symbols_overview(main.ts, depth=1)` -> concise symbol map of top-level functions and nested locals under `main`; next step `find_symbol(createMainWindow, include_body=true)`. +- **Built-in chain:** `rg` on `const|function|class|export` in `main.ts` -> flat text hits; next step `view` of lines `331-439` to read `createMainWindow`. +- **Payloads observed:** + - Serena overview output: compact symbol list for the file; next-step body fetch returned only the selected symbol body. + - Built-in overview output: many matching lines without structure; next-step read required **~109 lines** of file content. +- **Delta:** Serena's overview was not just shorter; it also supplied **stable symbol names** for the follow-up call. Built-ins could answer the question, but only after a second text-localizing step. + +**Verdict:** Serena materially improved the "overview -> inspect one function" flow by making the follow-up call symbol-based instead of line-based. + +#### Task 3: retrieve a specific class method body without reading the surrounding file + +- **Target symbol:** `AutoLauncher/toggleAutoLaunch` in `desktop/src/main/services/auto-launcher.ts`. +- **Serena chain:** `find_symbol(AutoLauncher/toggleAutoLaunch, include_body=true)` -> exact method body. +- **Built-in chain:** `view` of the relevant file range (`20-40`) after locating the method. +- **Payloads observed:** + - Serena returned the **10-line method body** only. + - Built-in read returned **21 lines** of surrounding class context. +- **Delta:** Serena saved one localization step and avoided unrelated lines. + +**Verdict:** Serena added a real but modest efficiency gain for targeted method retrieval. + +#### Task 4: find all references for one non-trivial symbol; compare code-use recall/precision vs text mention search + +- **Target symbol:** `wait` in `desktop/src/main/utils/common.ts`. +- **Serena chain:** `find_referencing_symbols(wait)` -> 3 files with symbol contexts: `main.ts`, `ffmpeg-worker.ts`, `ml-worker.ts`. +- **Built-in chain:** `rg \bwait\b desktop/src` -> 7 files including: + - real uses/imports, + - comments/doc strings (`preload.ts`, `watch.ts`, `main.ts` prose), + - the definition itself, + - doc mentions in `desktop/docs/release.md` when searching broader scope. +- **Payloads observed:** + - Serena: one structured result grouped by referencing symbol. + - Built-ins: one broader result, but it needed human filtering to answer "who uses this in code?" +- **Delta:** Serena improved **precision**, not just convenience. For the code-only question, built-ins needed extra filtering or extra reads. + +**Verdict:** Serena clearly improved reference search when the question is semantic ("who uses this in code?") rather than textual. + +#### Task 5: supertypes / subclasses / implementations + +- **Equivalent used:** interface hierarchy in `web/apps/ensu/src/services/llm/inference.ts`, because this TS area had interface implementations rather than rich class inheritance. +- **Serena chain:** `type_hierarchy(InferenceBackend, both)` -> `WasmInference`, `TauriInference`; `type_hierarchy(WasmInference, both)` -> supertype `InferenceBackend`. +- **Built-in chain:** `rg InferenceBackend|implements InferenceBackend` -> manual reconstruction from four text matches. +- **Payloads observed:** Serena returned the hierarchy directly; built-ins returned only raw declarations/usages. +- **Delta:** Serena removed the manual synthesis step. Built-ins were sufficient here because the hierarchy was shallow, but that was because the example was small. + +**Verdict:** Serena added moderate value for hierarchy queries; the value grows with hierarchy depth. + +#### Task 6: external dependency symbol lookup + +- **Targets used after indexing was available:** `BrowserWindow` and `serveNextAt` in the desktop TypeScript app, plus `Url` and `Zeroizing` in `rust/core`. +- **Serena chain (TS):** + - `find_declaration(new BrowserWindow(...), include_body=true)` -> `desktop/node_modules/electron/electron.d.ts`, body `class BrowserWindow extends Electron.BrowserWindow {}` + - `find_declaration(import serveNextAt ... , include_body=true)` -> `desktop/node_modules/next-electron-server/index.d.ts`, body `declare function serveNextAt(uri: string, options?: Options): void;` + - `find_symbol(..., search_deps=true)` on those dependency files returned dependency-side docs. +- **Serena chain (Rust):** + - `find_declaration(use reqwest::{Response, Url};, include_body=true)` -> `` external symbol `Url[0]` with the struct body + - `find_declaration(use zeroize::Zeroizing;, include_body=true)` -> `` external symbol `Zeroizing[0]` with the struct body + - `find_symbol(..., relative_path=, search_deps=true)` returned dependency-side docs for those external symbols. +- **Built-in equivalent chain:** + - Manually infer the correct monorepo-local dependency root (`desktop/node_modules`, not repo root), + - or manually inspect Cargo metadata / `Cargo.lock`, + - then open the resolved dependency files directly (for Rust, under the Cargo registry). +- **Payloads observed:** Serena returned the declaration target and a small signature/body directly; built-ins required **package-root discovery first**, which is a real extra step in a monorepo. +- **Delta:** Serena **does add capability and efficiency here** once indexing exists. The gain is larger in this monorepo than in a single-package repo because dependency ownership is split across package-local Node dependencies and shared Cargo registry sources. + +**Verdict:** With indexing available, Serena added meaningful external dependency lookup, and the value was amplified by the monorepo layout. + +### 3.2 Single-file edits + +#### Task 7a: small tweak (1-3 lines inside a method) + +- **Change:** rename local `autoLaunch` -> `launcher` inside `AutoLauncher/toggleAutoLaunch`. +- **Built-in chain:** `view(auto-launcher.ts, 20-40)` -> `apply_patch` on 3 changed lines -> `git diff`. +- **Serena chain:** `find_symbol(toggleAutoLaunch, include_body=true)` -> `replace_symbol_body(toggleAutoLaunch)` -> `git diff`. +- **Payloads observed:** + - Built-ins: read **21 lines**, patch changed **3 logical lines**. + - Serena: fetched **10-line body**, resent **full 10-line body**. +- **Delta:** same result, same number of main steps, but the symbolic edit resent untouched lines. + +**Verdict:** For tiny in-method tweaks, built-ins were more payload-efficient and Serena added no real workflow advantage. + +#### Task 7b: medium rewrite (~10-30 lines) + +- **Change:** rewrite `uniqueSavePath` to use `candidatePath` + `for` loop. +- **Built-in chain:** `view(main.ts, 500-540)` -> `apply_patch` replacing the function body -> `git diff`. +- **Serena chain:** `find_symbol(uniqueSavePath, include_body=true)` -> `replace_symbol_body(uniqueSavePath)` -> `git diff`. +- **Payloads observed:** + - Built-ins: read **41 lines** to safely anchor a **~10-line** rewrite. + - Serena: fetched the **11-line symbol body** and resent the rewritten body only. +- **Delta:** Here Serena was more efficient: less prerequisite read volume and no dependence on surrounding file context. + +**Verdict:** Serena was better for medium symbol-sized rewrites. + +#### Task 7c: large / whole-body rewrite + +- **Change:** rewrite the entire `createMainWindow` body. +- **Built-in chain:** `view(main.ts, 331-439)` -> `apply_patch` replacing the function body -> `git diff`. +- **Serena chain:** `find_symbol(createMainWindow, include_body=true)` -> `replace_symbol_body(createMainWindow)` -> `git diff`. +- **Payloads observed:** + - Built-ins: read **~109 lines** and patched the whole function. + - Serena: fetched the **same symbol body** and resent the whole rewritten body. +- **Delta:** Serena still avoided a file-range read, but once the symbol itself dominates the payload, the token gap mostly disappears. + +**Verdict:** For whole-body rewrites, Serena's gain was modest: better addressing, not dramatically smaller payload. + +#### Task 8: insert a new function at a structural location + +- **Insertion:** `waitSeconds` after `wait` in `desktop/src/main/utils/common.ts`. +- **Built-in chain:** `view(common.ts, 1-40)` -> `apply_patch` inserting after the existing function. +- **Serena chain:** `find_symbol(wait)` -> `insert_after_symbol(wait)`. +- **Payloads observed:** + - Built-ins: read **26 lines** to place a **1-line** function. + - Serena: no extra file-range read once the symbol name was known. +- **Delta:** Serena made the location structural instead of textual. + +**Verdict:** Serena improved insertions when the location is "after symbol X" rather than "after line Y". + +#### Task 9: rename a private helper used only within one file + +- **Target symbol:** `openStreetMapUserAgent` in `desktop/src/main.ts`. +- **Built-in chain:** `view`/`rg` to find call site + definition -> `apply_patch` updating both. +- **Serena chain:** `rename(openStreetMapUserAgent -> buildOpenStreetMapUserAgent)`. +- **Payloads observed:** + - Built-ins: two textual sites had to be found and updated manually. + - Serena: one rename call, terse success response (`"Success"`). +- **Delta:** small call-count improvement, larger correctness improvement when the file is bigger or the name is less unique. + +**Verdict:** Serena was somewhat better even for single-file private renames because it removed manual site enumeration. + +### 3.3 Multi-file changes + +#### Task 10: rename a symbol across several files including imports + +- **Target symbol:** `wait` -> `delay`. +- **Built-in chain:** read `common.ts`, `main.ts`, `ffmpeg-worker.ts`, `ml-worker.ts` -> one multi-file `apply_patch` -> `git diff`. +- **Serena chain:** `rename(wait -> delay)` on the defining symbol -> `git diff`. +- **Payloads observed:** + - Built-ins: required reading **4 files** and manually updating export, imports, and call sites. + - Serena: one semantic rename updated the same 4 files. +- **Success signals:** built-ins only showed success via resulting diff; Serena returned `"Success"`. +- **Delta:** this is one of Serena's clearest wins: same final diff, far less manual scope work. + +**Verdict:** Serena strongly improved multi-file renames by collapsing discovery + edit into one symbol-based refactor. + +#### Task 11: move a symbol from one module to another, updating imports + +- **Target:** move `nullToUndefined` out of `common.ts`. +- **Serena chain executed:** `move(nullToUndefined, target_relative_path=http.ts)` -> `git diff`. +- **Observed result:** Serena removed the symbol from `common.ts`, updated `ffmpeg-worker.ts`, but created a **new file** `desktop/src/main/utils/nullToUndefined.ts` instead of merging into `http.ts`. +- **Built-in equivalent:** would require manually copying the symbol into the intended target module, updating imports, then deleting the old definition. +- **Delta:** Serena still automated the cross-file update, but **did not provide the specific "move into existing module" behavior I was testing**. + +**Verdict:** Serena added partial value for symbol moves here, but not the full capability of "move into a chosen existing TS file". + +#### Task 12: move a file/package and update imports + +- **Target:** move `desktop/src/main/utils/http.ts` to `desktop/src/main/services/http.ts`. +- **Serena chain:** `move(file http.ts -> services/)` -> `git diff`. +- **Observed result:** the file was renamed/moved and `ffmpeg-worker.ts` import updated from `../utils/http` to `./http`. +- **Built-in equivalent:** locate all imports, move the file, patch each import path, then verify. +- **Delta:** this was a real one-call semantic file move. + +**Verdict:** Serena materially improved file moves that require import updates. + +#### Task 12 (safe delete with no remaining usages) + +- **Attempted:** searched for naturally unused TS symbols in the working areas (`main.ts`, `common.ts`, `temp.ts`, `inference.ts`) and checked several candidates (`registerForEnteLinks`, `minimumWindowSize`, `AutoLauncher/isEnabled`, `openStreetMapUserAgent`, `safeJson`, `buildSamplingConfig`). +- **Observed result:** every plausible candidate still had live references. +- **Outcome:** **no suitable candidate found in the TS areas where Serena was operational**, so I skipped this comparison instead of forcing an invalid input. + +**Verdict:** No evidence either way here because the repo did not offer a clean unused-symbol candidate in the code areas Serena handled reliably. + +#### Task 13: delete a symbol and propagate deletion to call sites + +- **Attempted:** looked for a helper whose call sites could be semantically removed rather than inlined or manually rewritten. +- **Observed result:** the good candidates in this repo were better modeled as **inline** refactors, not delete-with-propagation. +- **Outcome:** **no suitable candidate**; skipped rather than using an unsafe input. + +**Verdict:** No measured delta here because the available candidates were inline candidates, not safe propagate-delete candidates. + +#### Task 13 (inline a small helper) + +- **Target symbol:** `waitForRendererDevServer`. +- **Built-in chain:** `view` call site + definition -> `apply_patch` replacing `await waitForRendererDevServer()` with `await wait(1000)` and deleting the helper -> `git diff`. +- **Serena chain:** `inline(waitForRendererDevServer, keep_definition=false)` -> `git diff`. +- **Observed result:** both achieved the inline, but Serena also rewrote unrelated import formatting at the top of the file. +- **Success signals:** built-ins: resulting diff; Serena: `{"status":"SUCCESS"}`. +- **Delta:** Serena added the unique semantic refactor, but in this run it also introduced **format churn outside the logical change**. + +**Verdict:** Serena added real inline capability, with a low-frequency but real tradeoff of broader formatting churn. + +### 3.4 Reliability & correctness-oriented checks + +#### Task 14: scope precision + +- **Demonstrated with:** `AutoLauncher/toggleAutoLaunch`, `openStreetMapUserAgent`, and `InferenceBackend`. +- **Serena:** symbol names and name paths targeted the exact code entity. +- **Built-ins:** text search for names such as `wait` or `writeToTemporaryFile` over-matched comments, docs, and multiple textual occurrences. +- **Delta:** Serena's unit of work was the symbol; built-ins' unit was the matching line. + +**Verdict:** Serena was reliably more precise whenever the target was a symbol rather than a string. + +#### Task 15: atomicity + +- **Observed:** Serena rename/file-move/inline each ran as one refactor operation after symbol selection. +- **Built-ins:** a single `apply_patch` can update multiple files atomically as a patch, but it **cannot discover missed sites**; semantic completeness remains manual. +- **Delta:** Serena's advantage was not transactional all-or-none patching; it was **scope computation**. + +**Verdict:** Serena improved semantic completeness more than patch atomicity. + +#### Task 16: success signals + +- **Observed Serena success outputs:** `OK` for body replacement, `"Success"` for rename, JSON result for move, `{"status":"SUCCESS"}` for inline. +- **Observed built-in success outputs:** only indirect evidence via `git diff` / clean revert. + +**Verdict:** Serena gave clearer machine-readable success signals for refactors than the built-ins did. + +## 4. Token-efficiency analysis + +### By edit size + +| Edit size | Built-ins | Serena | More efficient | +| --- | --- | --- | --- | +| **Small tweak** (`toggleAutoLaunch`) | Read ~21 lines, patch only changed lines | Fetch 10-line body, resend full 10-line body | **Built-ins** | +| **Medium rewrite** (`uniqueSavePath`) | Read ~41 lines to safely patch ~10 lines | Fetch 11-line body, resend 11-line body | **Serena** | +| **Large rewrite** (`createMainWindow`) | Read ~109 lines, patch whole body | Fetch ~same symbol body, resend whole body | **Near tie**, slight Serena advantage only from structural targeting | +| **Cross-file rename** (`wait -> delay`) | Read 4 files, craft 4-file patch | One rename after discovery | **Serena by a large margin** | + +### Forced reads + +- Built-ins often needed a **localization read** before the edit. +- Serena avoided that when the symbol was already known, but not when the task itself required understanding the body. + +### Stable vs ephemeral addressing + +- Serena's addresses (`createMainWindow`, `wait`, `openStreetMapUserAgent`) stayed useful across later operations. +- Built-in line ranges from `view` became stale after edits, so later text operations required re-grepping or re-viewing. + +**Verdict:** Serena is most token-efficient for medium-to-large symbol work and cross-file refactors; built-ins stay leaner for tiny local edits. + +## 5. Reliability & correctness (under correct use) + +- **Precision of matching:** Serena's reference search answered "who uses this in code?" better than `rg`, which mixed real uses with prose/comment matches. +- **Scope disambiguation:** Serena targeted exact symbols (`AutoLauncher/toggleAutoLaunch`, `InferenceBackend`) rather than relying on unique text strings. +- **Atomicity:** Serena computed and updated semantic scope in one refactor call; built-ins could batch edits, but only after manual scope discovery. +- **Semantic queries vs text search:** hierarchy and references were the strongest examples. Built-ins could reconstruct them, but only with manual interpretation. +- **External dependencies:** after indexing was available, Serena resolved desktop TypeScript dependencies into package-local declaration files under `desktop/node_modules` and Rust dependencies into external Cargo sources such as `url` and `zeroize`. Built-ins could still reach those files, but only after manual package-root or registry-path discovery. +- **Monorepo effect:** this repo magnified Serena's dependency-lookup value because "the dependency source" was not at one obvious global root. Serena jumped from app code to the right package-local or registry-backed dependency context directly. + +**Verdict:** Serena improved correctness by narrowing work to exact symbols and by resolving dependencies across monorepo boundaries. + +## 6. Workflow effects across a session + +- **Advantages compounded** when I stayed in symbol space. Example: `get_symbols_overview(main.ts)` produced symbol names that I later reused for `find_symbol(createMainWindow)`, `rename(openStreetMapUserAgent)`, and `inline(waitForRendererDevServer)`. +- **Built-in workflows required refreshes**. Across repeated `main.ts` experiments, I repeatedly had to reacquire ranges with `view`/`rg` before editing because prior line-based context was no longer trustworthy. +- **In the monorepo, Serena also compounded by removing package-boundary bookkeeping.** In the desktop app I could jump from `main.ts` into Electron and `next-electron-server` declarations without first reasoning about workspace roots; in `rust/core` I could jump into Cargo-registry dependencies through external symbol handles instead of manually reconstructing registry paths from `Cargo.lock`. +- **The compounding effect disappeared** for tiny edits and non-code work, where built-ins were already direct and minimal. +- **One tradeoff compounded too:** some Serena refactors carried formatting side effects (notably `inline`), so the semantic benefit does not guarantee a surgically small diff. + +**Verdict:** Serena's advantages compound most in code-centric monorepo sessions, where symbol reuse and dependency jumps save both re-reading and package-root discovery work. + +## 7. Unique capabilities + +| Capability with no practical one-step built-in equivalent | Frequency | Impact | +| --- | --- | --- | +| **Semantic cross-file rename from a single symbol definition** | Medium | High | +| **Type hierarchy query (implementations / supertypes)** | Low-Medium | Medium | +| **Inline refactor across call sites** | Low | High when applicable | +| **File move with import updates** | Low-Medium | High | +| **External dependency resolution from in-repo code into package-local or registry-backed sources** | Medium | Medium-High | + +Built-ins can approximate all of these manually, but not as a single semantic operation. + +**Verdict:** Serena did add unique practical capabilities, especially around refactors that require scope computation rather than text replacement. + +## 8. Tasks outside Serena's scope (built-in only) + +- Reading non-code files like `desktop/package.json` +- Free-text search such as `ente://app` or URL strings +- Git inspection / diff / cleanup +- Config/package/changelog/docs/notebook reading +- Exact textual patching once the line range is already known + +In this session, these built-in-only tasks were **roughly 40% of the total operational steps by count**, but they were usually the low-complexity steps around the more valuable semantic work. + +**Verdict:** A substantial share of everyday terminal work remains built-in-only, but Serena targets the higher-value symbol-heavy slice rather than the whole session. + +## 9. Practical usage rule + +Use **Serena first** when the task is about a **code symbol** and especially when it spans **multiple files, references, or a whole symbol body**. Use **built-ins first** when the task is about **text, config/docs, free-text search, git state, or a 1-3 line local tweak**. The highest-yield mixed workflow in this repo was: **discover/refactor with Serena, inspect non-code and do tiny patches with built-ins**. + +**Verdict:** Choose Serena for symbol semantics and built-ins for text locality. From 505d520c80055bd18ad7d029a897f18f78186e52 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 14 Apr 2026 14:59:56 +0200 Subject: [PATCH 164/190] SerenaAgent._create_base_toolset: Account for mode activation no longer being dynamic Fully apply exclusions when in a single-project context Fixes #1336 --- CHANGELOG.md | 2 ++ src/serena/agent.py | 36 ++++++++++++++++++++---------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92a95e73..3ce06696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ Status of the `main` branch. Changes prior to the next official version change w * General: - Support environment variable `SERENA_USAGE_REPORTING` (set to `false` to disable usage reporting) - Extended the list of always ignored directories (by language servers) with common cases. + - Improve exposed toolset: With mode switching no longer being a feature, we now fully apply tool exclusions + defined by modes when in a single-project context (limiting exposed tools to a minimum) - Fix: When scanning for `.gitignore` files, the presence of files that could not be made relative to the project root would cause the scan to fail. #1317 diff --git a/src/serena/agent.py b/src/serena/agent.py index 322db7c8..30ef7cc5 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -453,31 +453,35 @@ class SerenaAgent: tool_inclusion_definitions.append(serena_config) tool_inclusion_definitions.append(context) + # determine whether we are operating in a single-project context + # (i.e. the project that is activated at startup is the only project that will be worked with throughout the session) + is_single_project = context.single_project and project is not None + # consider modes - # Since modes can be dynamically turned on and off, we don't include their definitions directly, - # For the initially active dynamic modes, we make sure that the tools they enable are included. - for mode in modes.get_default_modes(): - tool_inclusion_definitions.append( - NamedToolInclusionDefinition( - name=f"InitialDynamicModeInclusions[{mode.name}]", included_optional_tools=mode.included_optional_tools - ) - ) - # For the base modes, we also apply the tool exclusions, since they apply throughout the entire session + # * base modes: These cannot be changed, so they are fully applied for base_mode in modes.get_base_modes(): - tool_inclusion_definitions.append( - NamedToolInclusionDefinition( - name=f"BaseMode[{base_mode.name}]", - included_optional_tools=base_mode.included_optional_tools, - excluded_tools=base_mode.excluded_tools, + tool_inclusion_definitions.append(base_mode) + # * default modes: When not in a single-project context, these modes are dynamic (can later be turned off), + # so we consider only their inclusions (but not their exclusions, because these must not be hard) + for mode in modes.get_default_modes(): + if is_single_project: + tool_inclusion_definitions.append(mode) + else: + # Since modes can be dynamically turned on and off, we don't include their definitions directly, + # For the initially active dynamic modes, we make sure that the tools they enable are included. + tool_inclusion_definitions.append( + NamedToolInclusionDefinition( + name=f"InitialDynamicModeInclusions[{mode.name}]", included_optional_tools=mode.included_optional_tools + ) ) - ) # When in a single-project context, the agent is assumed to work on a single project, and we thus # want to apply that project's tool exclusions/inclusions from the get-go, limiting the set # of tools that will be exposed to the client. # Furthermore, we disable tools that are only relevant for project activation. # So if the project exists, we apply all the aforementioned exclusions. - if context.single_project and project is not None: + if is_single_project: + assert project is not None log.info( "Applying tool inclusion/exclusion definitions for single-project context based on project '%s'", project.project_name, From c00a54152eac9eb12205b8d15c4041711153acab Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 14 Apr 2026 15:41:04 +0200 Subject: [PATCH 165/190] Improved documentation on connecting with Junie --- docs/02-usage/030_clients.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index e608b272..531d750b 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -367,8 +367,10 @@ Copilot should now show that Serena is running, though you may have to restart i ## JetBrains Junie -For the Junie plugin in JetBrains IDEs, go to the three dots in the top right corner, -then Settings / MCP Settings and add Serena to Junie's global MCP server configuration: +For the Junie plugin in JetBrains IDEs you can add Serena either to the global configuration in `~/.junie/mcp/mcp.json` +or to the project configuration in `/.junie/mcp/mcp.json`. Important, don't add both! +In both cases the entry should be: + ```json { @@ -385,14 +387,19 @@ then Settings / MCP Settings and add Serena to Junie's global MCP server configu } ``` -You will have to prompt Junie to "Activate the current project using serena's activation tool" at the -start of each session. +With the global configuration, Serena will be available in all projects. However, +within the Junie plugin, projects will not be automatically activated in Serena. +You may thus have to prompt +Junie to "Activate the current project using serena's activation tool" at the start of each session (though some models are +smart enough to activate the project automatically). + +With the project-scoped configuration, Serena will be available only in that project, and the project will automatically +be recognized as active by Serena. -For Junie CLI, add the same setting to `~/.junie/mcp/mcp.json`. ## JetBrains AI Assistant -Go to Settings / Tools / AI Assistant / MCP and add a new **global** configuration: +Go to Settings / Tools / AI Assistant / MCP and enter the following configuration: ```json { @@ -409,7 +416,8 @@ Go to Settings / Tools / AI Assistant / MCP and add a new **global** configurati } ``` -Then make sure to configure the working directory to be the project root. +Like for Junie, you have the choice between the global and the project-scoped configuration, +with the same trade-off. ## Antigravity From adad335d7629a4f5830104d8d958609f55d805a5 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 14 Apr 2026 16:06:13 +0200 Subject: [PATCH 166/190] Removed concurrency handling for docs workflow --- .github/workflows/docs.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 72bb5c00..b2487a03 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -12,10 +12,6 @@ permissions: pages: write id-token: write -concurrency: - group: "pages" - cancel-in-progress: true - jobs: build: runs-on: ubuntu-latest From e1b29b8c28aeb232859edd876b80cb435f45db16 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 14 Apr 2026 15:36:03 +0200 Subject: [PATCH 167/190] Dashboard: Improve handling of read news (saving each id individually) Fixes #1338 --- CHANGELOG.md | 3 ++ src/serena/config/serena_config.py | 6 ++- src/serena/dashboard.py | 68 ++++++++++++++++++++++++------ 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ce06696..ee9434b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ Status of the `main` branch. Changes prior to the next official version change w - Fix: When scanning for `.gitignore` files, the presence of files that could not be made relative to the project root would cause the scan to fail. #1317 +Dashboard: + - Fix handling of read news, saving each read news entry separately #1338 + JetBrains: - Improve handling of `relative_path` parameter - Improve its documentation to avoid usage errors diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 987d8cf6..a9cefd3b 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -83,7 +83,11 @@ class SerenaPaths: If a name of a mode matches a name of a mode in SERENAS_OWN_MODES_YAML_DIR, the user mode will override the default mode definition. """ - self.news_snippet_id_file: str = os.path.join(self.serena_user_home_dir, "last_read_news_snippet_id.txt") + self.news_legacy_last_read_id_file: str = os.path.join(self.serena_user_home_dir, "last_read_news_snippet_id.txt") + """ + file containing the ID of the last read news snippet + """ + self.news_read_items_file: str = os.path.join(self.serena_user_home_dir, "news_read.pkl") """ file containing the ID of the last read news snippet """ diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 3c33f1a2..8ed27a5f 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -15,6 +15,7 @@ from flask import Flask, Response, redirect, request, send_from_directory from PIL import Image from pydantic import BaseModel from sensai.util import logging +from sensai.util.pickle import dump_pickle, load_pickle from serena.analytics import ToolUsageStats from serena.config.serena_config import SerenaConfig, SerenaPaths @@ -132,6 +133,54 @@ class QueuedExecution(BaseModel): ) +class ReadNews: + def __init__(self, read_ids: list[str], legacy_last_read_id: str | None = None): + self._read_ids = set(read_ids) + self._legacy_last_read_id = legacy_last_read_id + + @staticmethod + def load() -> "ReadNews": + read_news_path = SerenaPaths().news_read_items_file + legacy_last_read_id_path = SerenaPaths().news_legacy_last_read_id_file + + def load_legacy_last_read_id() -> str | None: + if not os.path.exists(legacy_last_read_id_path): + return None + with open(legacy_last_read_id_path, encoding="utf-8") as f: + last_read_news_id = f.read().strip() + if last_read_news_id == "20262103": + last_read_news_id = "20260321" # fix originally misnamed news id + return last_read_news_id + + if os.path.exists(read_news_path): + return load_pickle(read_news_path) + else: + instance = ReadNews(read_ids=[], legacy_last_read_id=load_legacy_last_read_id()) + instance._save() + try: + os.unlink(legacy_last_read_id_path) + except: + pass + return instance + + def _save(self) -> None: + dump_pickle(self, SerenaPaths().news_read_items_file) + + def is_read(self, identifier: str) -> bool: + if identifier in self._read_ids: + return True + if self._legacy_last_read_id is not None and identifier <= self._legacy_last_read_id: + return True + return False + + def mark_read(self, identifier: str) -> None: + """ + Marks the given news snippet as read, saving the new state to disk + """ + self._read_ids.add(identifier) + self._save() + + class SerenaDashboardAPI: log = logging.getLogger(__qualname__) @@ -150,6 +199,7 @@ class SerenaDashboardAPI: self._loaded_news: dict[str, str] = {} self._news_ready = threading.Event() self._setup_routes() + self._read_news = ReadNews.load() # Fetch remote news in background on startup (non-blocking) threading.Thread(target=self._fetch_news, daemon=True).start() @@ -362,7 +412,6 @@ class SerenaDashboardAPI: self._news_ready.wait() all_news = self._loaded_news - # Filter news items by installation date serena_config_creation_date = SerenaConfig.get_config_file_creation_date() if serena_config_creation_date is None: # should not normally happen, since config file should exist when the dashboard is started @@ -370,17 +419,12 @@ class SerenaDashboardAPI: log.error("Serena config file not found when starting the dashboard") return {} serena_config_creation_date = serena_config_creation_date.strftime("%Y%m%d") - # Only include news items published on or after the installation date + + # filter for news after the installation date post_installation_news = {k: v for k, v in all_news.items() if k >= serena_config_creation_date} - news_snippet_id_file = SerenaPaths().news_snippet_id_file - if not os.path.exists(news_snippet_id_file): - return post_installation_news - with open(news_snippet_id_file, encoding="utf-8") as f: - last_read_news_id = f.read().strip() - if last_read_news_id == "20262103": - last_read_news_id = "20260321" # fix originally misnamed news id - return {k: v for k, v in post_installation_news.items() if k > last_read_news_id} + # read unread news + return {k: v for k, v in post_installation_news.items() if not self._read_news.is_read(k)} try: unread_news = _fetch_unread_news() @@ -393,9 +437,7 @@ class SerenaDashboardAPI: try: request_data = request.get_json() news_snippet_id = str(request_data.get("news_snippet_id")) - news_snippet_id_file = SerenaPaths().news_snippet_id_file - with open(news_snippet_id_file, "w", encoding="utf-8") as f: - f.write(news_snippet_id) + self._read_news.mark_read(news_snippet_id) return {"status": "success", "message": f"Marked news snippet {news_snippet_id} as read"} except Exception as e: return {"status": "error", "message": str(e)} From de2f75673b71c5071373f3fcc1bd8312969bf529 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 14 Apr 2026 17:40:24 +0200 Subject: [PATCH 168/190] Add news item on evaluation and 1.1.2 release --- news/20260414.html | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 news/20260414.html diff --git a/news/20260414.html b/news/20260414.html new file mode 100644 index 00000000..5e2a0d46 --- /dev/null +++ b/news/20260414.html @@ -0,0 +1,15 @@ +
    +

    Agents Evaluating Serena for Themselves & Maintenance Update

    +

    April 14, 2026

    +

    + Evaluation. + We had our actual "end users", i.e. coding agents, evaluate the added value of Serena's tools based on their own empirical insights. + Read what the agents had to say in our evaluation report.
    + This represents the first systematic evaluation we ever conducted. +

    +

    + Maintenance Update. The new v1.1.2 release adds several improvements and fixes.
    + If you have already switched to the uv tool installation of Serena, upgrade as follows:
    + uv tool upgrade serena-agent --prerelease=allow +

    +
    \ No newline at end of file From 1e6345cae95bd48201a614e5b7d1537df55c7533 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 14 Apr 2026 17:46:53 +0200 Subject: [PATCH 169/190] Release v1.1.2 --- CHANGELOG.md | 2 ++ pyproject.toml | 2 +- src/serena/__init__.py | 2 +- uv.lock | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee9434b6..ddbdc6b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +# v1.1.2 (2026-04-14) + * General: - Support environment variable `SERENA_USAGE_REPORTING` (set to `false` to disable usage reporting) - Extended the list of always ignored directories (by language servers) with common cases. diff --git a/pyproject.toml b/pyproject.toml index 7ab83e26..004e130b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["hatchling"] [project] name = "serena-agent" -version = "1.1.1" +version = "1.1.2" description = "A powerful MCP toolkit for coding, providing semantic retrieval and editing capabilities - the IDE for your agent" authors = [{ name = "Oraios AI", email = "info@oraios-ai.de" }] readme = "README.md" diff --git a/src/serena/__init__.py b/src/serena/__init__.py index 151d3432..77077e5c 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.1.1" +__version__ = "1.1.2" import logging diff --git a/uv.lock b/uv.lock index 2e22e6f7..4b9d3bc5 100644 --- a/uv.lock +++ b/uv.lock @@ -2951,7 +2951,7 @@ wheels = [ [[package]] name = "serena-agent" -version = "1.1.1" +version = "1.1.2" source = { editable = "." } dependencies = [ { name = "anthropic" }, From 94bd2e52b6c2be4f869d3b19484af77c00aa7e43 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 14 Apr 2026 18:30:43 +0200 Subject: [PATCH 170/190] CI, minor: remove junie workflow, use persist-credentials: false on checkout action --- .github/workflows/codespell.yml | 2 ++ .github/workflows/docker.yml | 2 ++ .github/workflows/docs.yaml | 2 ++ .github/workflows/junie.yml | 21 --------------------- .github/workflows/publish.yml | 6 +++++- .github/workflows/pytest.yml | 4 +++- 6 files changed, 14 insertions(+), 23 deletions(-) delete mode 100644 .github/workflows/junie.yml diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 6df6169c..32301680 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -20,6 +20,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Annotate locations with typos uses: codespell-project/codespell-problem-matcher@v1 - name: Codespell diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 724837aa..5513b734 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -20,6 +20,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index b2487a03..f69960c4 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -20,6 +20,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/junie.yml b/.github/workflows/junie.yml deleted file mode 100644 index 9860e79e..00000000 --- a/.github/workflows/junie.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Junie -run-name: Junie run ${{ inputs.run_id }} - -permissions: - contents: write - -on: - workflow_dispatch: - inputs: - run_id: - description: "id of workflow process" - required: true - workflow_params: - description: "stringified params" - required: true - -jobs: - call-workflow-passing-data: - uses: jetbrains-junie/junie-workflows/.github/workflows/ej-issue.yml@main - with: - workflow_params: ${{ inputs.workflow_params }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 04127f48..f4280d1f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + persist-credentials: false - name: Create Release id: create_release uses: actions/create-release@v1 @@ -39,6 +41,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + persist-credentials: false - name: Install the latest version of uv uses: astral-sh/setup-uv@v6 diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index f1d969e9..768575ac 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -20,7 +20,9 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Free disk space if: runner.os == 'Linux' run: | From d6ea5bffdef86bb432f9a0a93ae27814ad35c742 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 14 Apr 2026 20:15:12 +0200 Subject: [PATCH 171/190] Wrong link in docs --- docs/02-usage/030_clients.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index 531d750b..c65bfe0b 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -129,7 +129,7 @@ of Serena's tools, either by failing to load them in the beginning or by forgett up the hooks as below (or a variation thereof) for optimal performance of Serena in Claude Code. :::{note} -While recommended, hooks are an **alpha feature**. Provide feedback via the [GitHub issue tracker](https://github.com/serenadeai/serenade/issues) if you encounter any issues. +While recommended, hooks are an **alpha feature**. Provide feedback via the [GitHub issue tracker](https://github.com/oraios/serena/issues) if you encounter any issues. ::: To set up hooks, add the following to your Claude Code settings file From 7067963d6e60495480994ebb8c7b50922a14d5ea Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 14 Apr 2026 23:59:35 +0200 Subject: [PATCH 172/190] Support `serena --version` CLI command Resolves #1347 --- CHANGELOG.md | 3 +++ src/serena/cli.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddbdc6b1..768f6260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* General: + - Support `serena --version` CLI command for displaying the current version #1347 + # v1.1.2 (2026-04-14) * General: diff --git a/src/serena/cli.py b/src/serena/cli.py index 84f2fcc9..a82ba180 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -139,6 +139,26 @@ class TopLevelCommands(AutoRegisteringGroup): "that separate entrypoint for performance reasons, see `serena-hooks --help`. You can run ` --help` for more info on each command.", ) + # register --version / -V flag + self.params.append( + click.Option( + ["--version", "-V"], + is_flag=True, + expose_value=False, + is_eager=True, + callback=self._print_version, + help="Show the version and exit.", + ) + ) + + @staticmethod + def _print_version(ctx: click.Context, _param: click.Parameter, value: bool) -> None: + """Print version string and exit if the flag is set.""" + if not value: + return + click.echo(f"Serena {serena_version()}") + ctx.exit() + @staticmethod @click.command( "init", From d8178fa2b422f8b97dd7c75ee4797ca57f491f15 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 15 Apr 2026 00:13:48 +0200 Subject: [PATCH 173/190] CI: Removed `persist-credentials: false` from checkout action For some reason, this causes the haxe LS download to fail. We might want to investigate this in the future --- .github/workflows/pytest.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 768575ac..82da9736 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -21,8 +21,6 @@ jobs: python-version: ["3.11"] steps: - uses: actions/checkout@v4 - with: - persist-credentials: false - name: Free disk space if: runner.os == 'Linux' run: | From a09840156f798e32b9f90dcf782ac1d5b2f8bb98 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 15 Apr 2026 00:16:45 +0200 Subject: [PATCH 174/190] Fix in Project._is_ignored_relative_path Check for ignored path ignored `.git` folder only at the top level, not in every subdirectory Fixes #1350 --- CHANGELOG.md | 1 + src/serena/project.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 768f6260..48beba24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Status of the `main` branch. Changes prior to the next official version change w * General: - Support `serena --version` CLI command for displaying the current version #1347 + - Fix: Check for ignored path ignored `.git` folder only at the top level, not in every subdirectory (`Project._is_ignored_relative_path`) #1350 # v1.1.2 (2026-04-14) diff --git a/src/serena/project.py b/src/serena/project.py index a20623e3..6f1cec43 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -448,7 +448,7 @@ class Project(ToStringMixin): rel_path = Path(relative_path) # always ignore paths inside .git - if len(rel_path.parts) > 0 and rel_path.parts[0] == ".git": + if len(rel_path.parts) > 0 and ".git" in rel_path.parts: return True return match_path(str(relative_path), self._ignore_spec, root_path=self.project_root) From a6ec1b06212b59ca8aa9826aac04cfeb37cd2dbe Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 14 Apr 2026 23:43:03 +0200 Subject: [PATCH 175/190] Rename YAML comment transfer functions (no longer solely about transferring missing comments) --- src/serena/config/serena_config.py | 6 +++--- src/serena/util/yaml.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index a9cefd3b..26815061 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -31,7 +31,7 @@ from serena.constants import ( SERENA_MANAGED_DIR_NAME, ) from serena.util.inspection import determine_programming_language_composition -from serena.util.yaml import YamlCommentNormalisation, load_yaml, normalise_yaml_comments, save_yaml, transfer_missing_yaml_comments +from serena.util.yaml import YamlCommentNormalisation, load_yaml, normalise_yaml_comments, save_yaml, transfer_yaml_comments from solidlsp.ls_config import Language from ..analytics import RegisteredTokenCountEstimator @@ -581,7 +581,7 @@ class ProjectConfig(SharedConfig): # transfer missing comments from the template file template_config, _ = self._load_yaml_dict(PROJECT_TEMPLATE_FILE, self.YAML_COMMENT_NORMALISATION) - transfer_missing_yaml_comments(template_config, config_with_comments, self.YAML_COMMENT_NORMALISATION, force_update_all=True) + transfer_yaml_comments(template_config, config_with_comments, self.YAML_COMMENT_NORMALISATION, force_update_all=True) # save project.yml save_yaml(config_path, config_with_comments) @@ -1063,7 +1063,7 @@ class SerenaConfig(SharedConfig): # For some keys, we force updates, because old comments are problematic/misleading. normalise_yaml_comments(commented_yaml, YamlCommentNormalisation.LEADING_WITH_CONVERSION_FROM_TRAILING) template_yaml = load_yaml(SERENA_CONFIG_TEMPLATE_FILE, comment_normalisation=YamlCommentNormalisation.LEADING) - transfer_missing_yaml_comments(template_yaml, commented_yaml, YamlCommentNormalisation.LEADING, force_update_all=True) + transfer_yaml_comments(template_yaml, commented_yaml, YamlCommentNormalisation.LEADING, force_update_all=True) save_yaml(self.config_file_path, commented_yaml) diff --git a/src/serena/util/yaml.py b/src/serena/util/yaml.py index ad177dce..89c68027 100644 --- a/src/serena/util/yaml.py +++ b/src/serena/util/yaml.py @@ -204,7 +204,7 @@ def yaml_comment_entry_is_empty(comment_entry: Any) -> bool: return False -def transfer_missing_yaml_comments_by_index( +def transfer_yaml_comments_by_index( source: CommentedMap, target: CommentedMap, indices: list[int], forced_update_keys: Sequence[str] = (), force_update_all: bool = False ) -> None: """ @@ -231,7 +231,7 @@ def transfer_missing_yaml_comments_by_index( target_comment[index] = source_comment[index] -def transfer_missing_yaml_comments( +def transfer_yaml_comments( source: CommentedMap, target: CommentedMap, comment_normalisation: YamlCommentNormalisation, @@ -251,7 +251,7 @@ def transfer_missing_yaml_comments( case YamlCommentNormalisation.NONE: pass case YamlCommentNormalisation.LEADING | YamlCommentNormalisation.LEADING_WITH_CONVERSION_FROM_TRAILING: - transfer_missing_yaml_comments_by_index( + transfer_yaml_comments_by_index( source, target, [ITEM_COMMENT_INDEX_BEFORE], forced_update_keys=forced_update_keys, force_update_all=force_update_all ) case _: From 5e82077b26f5dbe654f0c6536a8d5ce27b9cfd51 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 14 Apr 2026 23:43:33 +0200 Subject: [PATCH 176/190] Fix duplicated comments when re-saving YAML files This was due to nested comments associated with list items. We now completely remove these. Fixes #1285 --- CHANGELOG.md | 1 + src/serena/util/yaml.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48beba24..cc432a74 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 * General: - Support `serena --version` CLI command for displaying the current version #1347 - Fix: Check for ignored path ignored `.git` folder only at the top level, not in every subdirectory (`Project._is_ignored_relative_path`) #1350 + - Fix: Duplicate comments in re-saved YAML configuration files #1285 # v1.1.2 (2026-04-14) diff --git a/src/serena/util/yaml.py b/src/serena/util/yaml.py index 89c68027..ca63eee7 100644 --- a/src/serena/util/yaml.py +++ b/src/serena/util/yaml.py @@ -4,7 +4,7 @@ from collections.abc import Sequence from enum import Enum from typing import Any -from ruamel.yaml import YAML, CommentToken, StreamMark +from ruamel.yaml import YAML, CommentedSeq, CommentToken, StreamMark from ruamel.yaml.comments import CommentedMap from serena.constants import SERENA_FILE_ENCODING @@ -122,6 +122,20 @@ def normalise_yaml_comments(commented_map: CommentedMap, comment_normalisation: first_token.value = first_token.value[1:] return token_list + def remove_nested_comments() -> None: + """ + Removes nested comments, particularly of sequences, which incorrectly capture comments + that are actually intended for top-level keys. + """ + for key in commented_map.keys(): + entry = commented_map[key] + if isinstance(entry, CommentedSeq): + items = entry.ca.items + if isinstance(items, dict): + items_keys = list(items.keys()) + for i in items_keys: + items[i] = [None] * 4 + match comment_normalisation: case YamlCommentNormalisation.NONE: pass @@ -176,6 +190,9 @@ def normalise_yaml_comments(commented_map: CommentedMap, comment_normalisation: preceding_comment[ITEM_COMMENT_INDEX_BEFORE] = token_list current_comment[ITEM_COMMENT_INDEX_BEFORE] = None preceding_comment = current_comment + + # remove nested comments, as we assume that only top-level keys are supposed to be commented + remove_nested_comments() case _: raise ValueError(f"Unhandled comment normalisation: {comment_normalisation}") From e2463cec9e1a906516a656263deb61ba28cf65c7 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 15 Apr 2026 13:32:51 +0200 Subject: [PATCH 177/190] Fix Python 3.14 classifier --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 004e130b..766cbfc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dependencies = [ "requests==2.33.0", From a2061c1f4e6b5529e4b81208ce1c321a934b7ee0 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 15 Apr 2026 13:33:10 +0200 Subject: [PATCH 178/190] Switch back to official pywebview (new release 6.2) Resolves #1253 --- CHANGELOG.md | 3 +++ pyproject.toml | 2 +- uv.lock | 48 ++++++++++++++++++++++++------------------------ 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc432a74..71e56290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ Status of the `main` branch. Changes prior to the next official version change w - Fix: Check for ignored path ignored `.git` folder only at the top level, not in every subdirectory (`Project._is_ignored_relative_path`) #1350 - Fix: Duplicate comments in re-saved YAML configuration files #1285 +* Dependencies: + - `pywebview`: Switch back to official release (new version 6.2) #1253 + # v1.1.2 (2026-04-14) * General: diff --git a/pyproject.toml b/pyproject.toml index 766cbfc1..b073ca20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ dependencies = [ "tiktoken==0.12.0", "anthropic==0.59.0", "beautifulsoup4==4.14.2", - "oraios-pywebview==6.2", + "pywebview==6.2", "pystray==0.19.5", "pygls==2.1.1", # used to implement an msl language server "lsprotocol==2025.0.0", diff --git a/uv.lock b/uv.lock index 4b9d3bc5..7802365c 100644 --- a/uv.lock +++ b/uv.lock @@ -1711,28 +1711,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] -[[package]] -name = "oraios-pywebview" -version = "6.2" -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/0d/e4/6820d2a00b42621240762947c9e07d83acbb1251575349472a48fe9b1a9f/oraios_pywebview-6.2.tar.gz", hash = "sha256:2b176547321ff9c289ebb10d5b2423bc8887a4fc730b35a78656de7839e5be37", size = 499560, upload-time = "2026-04-10T15:01:42.363Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/76/ff666b5abc3b3ccc35f69d62b27290b75b7ef5ac4beb2c59e8f59f5da5ef/oraios_pywebview-6.2-py3-none-any.whl", hash = "sha256:c5b81d2f6cf4cbfdf3d608d5bc3e76d8f0a732dbdd8567660a9bc9cebdd35b84", size = 512739, upload-time = "2026-04-10T15:01:43.689Z" }, -] - [[package]] name = "overrides" version = "7.7.0" @@ -2474,6 +2452,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/3d/a378aa74085fc6f72923d5e39fc30efbc97541cfd49b3fe0a35f20d09ff7/pythonnet-3.1.0rc0-py3-none-any.whl", hash = "sha256:286ccf23d08e1cc2b5200bb91d0ee649436d2e8386fec36a85193c66d0ff8b5b", size = 293150, upload-time = "2026-02-25T10:46:48.535Z" }, ] +[[package]] +name = "pywebview" +version = "6.2" +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/c2/5d/a107645b073b80b4010e6fc62594dd58a958267b1b71439e7b89849362c7/pywebview-6.2.tar.gz", hash = "sha256:f51bfd095762fee3eb7821c62fe2c0ddd39c9fecd8f33d51a871113f12f364e1", size = 512668, upload-time = "2026-04-13T08:02:12.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/1c/78260d06d244d9138b8012eef541679f74e1e2b97e4e9dbf32a842b5b61a/pywebview-6.2-py3-none-any.whl", hash = "sha256:94681a1f456ed217f8a2139a1de612988c0d37cdb30d74d54279438e85572c41", size = 524555, upload-time = "2026-04-13T08:02:04.841Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -2966,7 +2966,6 @@ dependencies = [ { name = "joblib" }, { name = "lsprotocol" }, { name = "mcp" }, - { name = "oraios-pywebview" }, { name = "overrides" }, { name = "pathspec" }, { name = "psutil" }, @@ -2977,6 +2976,7 @@ dependencies = [ { name = "python-dotenv" }, { name = "python-multipart" }, { name = "pythonnet", marker = "sys_platform == 'win32'" }, + { name = "pywebview" }, { name = "pyyaml" }, { name = "regex" }, { name = "requests" }, @@ -3045,7 +3045,6 @@ requires-dist = [ { name = "mypy", marker = "extra == 'dev'", specifier = "==1.17.0" }, { name = "nbconvert", marker = "extra == 'dev'", specifier = "==7.17.0" }, { name = "nbsphinx", marker = "extra == 'dev'", specifier = "==0.9.7" }, - { name = "oraios-pywebview", specifier = "==6.2" }, { name = "overrides", specifier = "==7.7.0" }, { name = "pathspec", specifier = "==0.12.1" }, { name = "poethepoet", marker = "extra == 'dev'", specifier = "==0.36.0" }, @@ -3063,6 +3062,7 @@ requires-dist = [ { name = "python-dotenv", specifier = "==1.2.1" }, { name = "python-multipart", specifier = "==0.0.22" }, { name = "pythonnet", marker = "sys_platform == 'win32'", specifier = "==3.1.0rc0" }, + { name = "pywebview", specifier = "==6.2" }, { name = "pyyaml", specifier = "==6.0.2" }, { name = "regex", specifier = "==2026.2.28" }, { name = "requests", specifier = "==2.33.0" }, From 9ff2c765a9ca03b72a5b59d5c3a54d9ac28293fa Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 15 Apr 2026 16:07:54 +0200 Subject: [PATCH 179/190] Readme: minor changes in section on how Serena works --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0473535a..84cb122f 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ but an LLM is required to do the actual work, orchestrating tool use. Serena can extend the functionality of your existing AI client via the **model context protocol (MCP)**. Most modern AI chat clients directly support MCP, including * terminal-based clients like Claude Code, Codex, OpenCode, or Gemini-CLI, -* IDEs and IDE assistant plugins for VSCode, Cursor and JetBrains IDEs, -* desktop and web clients like Claude Desktop or OpenWebUI. +* IDEs and IDE assistant plugins for VSCode, Cursor and JetBrains IDEs (Copilot, Junie, JetBrains AI Assistant, etc.), +* desktop and web clients like Claude Desktop, Codex App, or OpenWebUI. @@ -76,6 +76,12 @@ To connect the Serena MCP server to your client, you either * provide the client with a launch command that allows it to start the MCP server, or * start the Serena MCP server yourself in HTTP mode and provide the client with the URL. +Serena's tools are powered by two alternative language intelligence backends: + * With the SolidLSP backend (default), language servers for the selected programming languages will be started +automatically for your project. + * With the JetBrains backend, the Serena JetBrains plugin must be installed in your IDE and +the project you want to work on must open and set up. + See the [Quick Start](#quick-start) section below for information on how to get started. ## Programming Language Support & Semantic Analysis Capabilities From e9e90f734d9da7443ab3b96b759d943eca621d4e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 15 Apr 2026 19:29:10 +0200 Subject: [PATCH 180/190] Dashboard: escape user input, address server-side scripting warning --- src/serena/dashboard.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 8ed27a5f..1799ea17 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -6,6 +6,7 @@ import threading import time import urllib.error import urllib.request +from html import escape from pathlib import Path from typing import TYPE_CHECKING, Any, Self @@ -391,7 +392,7 @@ class SerenaDashboardAPI: return { "status": "success", "was_cancelled": False, - "message": f"Task with id {request_data.get('task_id')} not found, maybe execution was already finished", + "message": f"Task with id {escape(request_data.get('task_id'))} not found, maybe execution was already finished", } except Exception as e: return {"status": "error", "message": str(e), "was_cancelled": False} @@ -436,7 +437,7 @@ class SerenaDashboardAPI: def mark_news_snippet_as_read() -> dict[str, str]: try: request_data = request.get_json() - news_snippet_id = str(request_data.get("news_snippet_id")) + news_snippet_id = escape(str(request_data.get("news_snippet_id"))) self._read_news.mark_read(news_snippet_id) return {"status": "success", "message": f"Marked news snippet {news_snippet_id} as read"} except Exception as e: From 6cd9c6b4d6fca2cbca0acdaf38bb6459a7b31204 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 15 Apr 2026 19:30:53 +0200 Subject: [PATCH 181/190] CI: minor cleanup in workflows. build job in publish workflow was not needed --- .github/workflows/publish.yml | 38 +++-------------------------------- .github/workflows/pytest.yml | 3 +++ 2 files changed, 6 insertions(+), 35 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f4280d1f..309d7a00 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,38 +6,13 @@ on: tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 -env: - PUBLISH_TO_PYPI: true - jobs: - build: - name: Create GitHub Release - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - body: | - See [change log](https://github.com/oraios/serena/blob/main/CHANGELOG.md) - draft: false - prerelease: false publish: name: Publish the serena-agent package runs-on: ubuntu-latest - permissions: id-token: write # Required for trusted publishing - contents: write # Required for updating artifact - + contents: write # Required for the GH release steps: - name: Checkout code uses: actions/checkout@v4 @@ -52,18 +27,11 @@ jobs: - name: Build package run: uv build - - name: Upload artifacts to GitHub Release - if: env.PUBLISH_TO_PYPI == 'true' - uses: softprops/action-gh-release@v2 + - name: Create GitHub Release + uses: softprops/action-gh-release@v3 with: - tag_name: ${{ github.event.inputs.tag || github.ref_name }} files: | dist/*.tar.gz dist/*.whl - - - name: Publish to TestPyPI - run: uv publish --index testpypi - - name: Publish to PyPI (conditional) - if: env.PUBLISH_TO_PYPI == 'true' run: uv publish diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 82da9736..60530151 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -6,6 +6,9 @@ on: branches: - main +permissions: + contents: read + concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From 8691ae30462dbe9fc928794194c9f04e180543f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:21:13 +0000 Subject: [PATCH 182/190] Bump python-multipart in the uv group across 1 directory Bumps the uv group with 1 update in the / directory: [python-multipart](https://github.com/Kludex/python-multipart). Updates `python-multipart` from 0.0.22 to 0.0.26 - [Release notes](https://github.com/Kludex/python-multipart/releases) - [Changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md) - [Commits](https://github.com/Kludex/python-multipart/compare/0.0.22...0.0.26) --- updated-dependencies: - dependency-name: python-multipart dependency-version: 0.0.26 dependency-type: direct:production dependency-group: uv ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b073ca20..e09bf469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dependencies = [ "urllib3==2.6.3", "werkzeug==3.1.7", "starlette==1.0.0", - "python-multipart==0.0.22", + "python-multipart==0.0.26", "filelock==3.25.2", "cryptography==46.0.7", "regex==2026.2.28", diff --git a/uv.lock b/uv.lock index 7802365c..793e80cb 100644 --- a/uv.lock +++ b/uv.lock @@ -2421,11 +2421,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.22" +version = "0.0.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, ] [[package]] @@ -3060,7 +3060,7 @@ requires-dist = [ { name = "pytest-timeout", marker = "extra == 'dev'", specifier = "==2.4.0" }, { name = "pytest-xdist", marker = "extra == 'dev'", specifier = "==3.8.0" }, { name = "python-dotenv", specifier = "==1.2.1" }, - { name = "python-multipart", specifier = "==0.0.22" }, + { name = "python-multipart", specifier = "==0.0.26" }, { name = "pythonnet", marker = "sys_platform == 'win32'", specifier = "==3.1.0rc0" }, { name = "pywebview", specifier = "==6.2" }, { name = "pyyaml", specifier = "==6.0.2" }, From 9d64931fd98fc47d387abe2b2214c71eb7728cf6 Mon Sep 17 00:00:00 2001 From: ek-133 Date: Thu, 16 Apr 2026 12:11:53 +0300 Subject: [PATCH 183/190] Issue #1348 Vue rename_symbol fix (#1355) Edits in vue files need to be propagated to the TS LS, e.g. for cross-language renames --- CHANGELOG.md | 3 + .../language_servers/vue_language_server.py | 67 ++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71e56290..9fb21277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ Status of the `main` branch. Changes prior to the next official version change w * Dependencies: - `pywebview`: Switch back to official release (new version 6.2) #1253 +* Language Servers: + - Fix: `rename_symbol` for Vue files now correctly propagates edits to the TypeScript server, enabling cross-file renames in `.vue` files + # v1.1.2 (2026-04-14) * General: diff --git a/src/solidlsp/language_servers/vue_language_server.py b/src/solidlsp/language_servers/vue_language_server.py index 95d08732..06123623 100644 --- a/src/solidlsp/language_servers/vue_language_server.py +++ b/src/solidlsp/language_servers/vue_language_server.py @@ -8,7 +8,8 @@ import os import pathlib import shutil import threading -from pathlib import Path +from collections.abc import Callable +from pathlib import Path, PurePath from time import sleep from typing import Any @@ -425,6 +426,70 @@ class VueLanguageServer(SolidLanguageServer): with self._ts_server.open_file(relative_file_path): return self._ts_server.request_rename_symbol_edit(relative_file_path, line, column, new_name) + def _forward_edit_to_ts_server_if_needed(self, relative_file_path: str, edit_fn: Callable[[], object]) -> None: + """ + Calls ``edit_fn`` on the TypeScript server if the file is open there. + + Only applicable to non-TypeScript files (i.e. .vue files) that have been + indexed on the TypeScript server for cross-file reference support. + + :param relative_file_path: the relative path of the file that was edited + :param edit_fn: callable that performs the corresponding edit on ``_ts_server`` + """ + if self._ts_server is None or not self._ts_server_started: + return + if self._is_typescript_file(relative_file_path): + return + + absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) + uri = pathlib.Path(absolute_file_path).as_uri() + if uri in self._ts_server.open_file_buffers: + edit_fn() + + @override + def insert_text_at_position(self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str) -> ls_types.Position: + """ + Inserts text at the given position, forwarding the change to the TypeScript server if it has the file open. + + :param relative_file_path: the relative path of the file to edit + :param line: the line number + :param column: the column number + :param text_to_be_inserted: the text to insert + :return: updated cursor position + """ + result = super().insert_text_at_position(relative_file_path, line, column, text_to_be_inserted) + self._forward_edit_to_ts_server_if_needed( + relative_file_path, + lambda: self._ts_server.insert_text_at_position( # type: ignore[union-attr] + relative_file_path, line, column, text_to_be_inserted + ), + ) + return result + + @override + def delete_text_between_positions( + self, + relative_file_path: str, + start: ls_types.Position, + end: ls_types.Position, + ) -> str: + """ + Deletes text between the given positions, forwarding the change to the TypeScript server if it has the file open. + + :param relative_file_path: the relative path of the file to edit + :param start: start position + :param end: end position + :return: deleted text + """ + deleted_text = super().delete_text_between_positions(relative_file_path, start, end) + self._forward_edit_to_ts_server_if_needed( + relative_file_path, + lambda: self._ts_server.delete_text_between_positions( # type: ignore[union-attr] + relative_file_path, start, end + ), + ) + return deleted_text + @classmethod def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> tuple[list[str], str, str]: is_node_installed = shutil.which("node") is not None From d650f30c7ed93fba475cfbdff424eb4ba639b1e0 Mon Sep 17 00:00:00 2001 From: mesa3 <43036352+mesa3@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:19:29 +0800 Subject: [PATCH 184/190] Fix clangd cache invalidation and capability checks (#1359) Summary relax clangd initialize capability checks to tolerate valid response shape differences add a clangd-specific document-symbol cache fingerprint so stale C++ symbol caches are invalidated when backend/config state changes add a regression test covering C++ document-symbol cache invalidation Problem For C++ projects, Serena's symbol tools could keep returning malformed cached document symbols even after clangd / compile commands / backend state had been repaired. The cache key was effectively stable as long as file contents did not change, so previously bad symbol trees could survive later LS recovery. In addition, clangd capability checks were too strict and assumed a single exact initialize-response shape. What this changes accepts both integer and object-style textDocumentSync responses validates completionProvider more conservatively by checking required trigger characters instead of exact equality fingerprints clangd document-symbol caches using: clangd_version ls_path compile_commands_dir compile_commands.json content hash adds a C++ regression test similar to the existing Go cache invalidation coverage --- CHANGELOG.md | 5 +- .../clangd_language_server.py | 45 ++++++++++-- test/solidlsp/cpp/test_cpp_basic.py | 71 +++++++++++++++++++ 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fb21277..2cf145fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,12 @@ Status of the `main` branch. Changes prior to the next official version change w * Dependencies: - `pywebview`: Switch back to official release (new version 6.2) #1253 -* Language Servers: + +* Language Servers: + - Fix: clangd capability checks now tolerate valid initialize response shape differences and invalidate cached C++ document symbols when clangd/compile commands context changes #1359 - Fix: `rename_symbol` for Vue files now correctly propagates edits to the TypeScript server, enabling cross-file renames in `.vue` files + # v1.1.2 (2026-04-14) * General: diff --git a/src/solidlsp/language_servers/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server.py index 0210e7d5..0dc5ed9c 100644 --- a/src/solidlsp/language_servers/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server.py @@ -1,8 +1,10 @@ +import hashlib import json import logging import os import pathlib import threading +from collections.abc import Hashable from typing import Any, cast from overrides import override @@ -42,6 +44,30 @@ class ClangdLanguageServer(SolidLanguageServer): self.initialize_searcher_command_available = threading.Event() self.resolve_main_method_available = threading.Event() + @override + def _document_symbols_cache_fingerprint(self) -> Hashable: + cache_format_version = 1 + cpp_settings: dict[str, Any] = self._custom_settings or {} + return ( + cache_format_version, + cpp_settings.get("clangd_version"), + cpp_settings.get("ls_path"), + cpp_settings.get("compile_commands_dir"), + self._compile_commands_fingerprint(), + ) + + def _compile_commands_fingerprint(self) -> str | None: + compile_db_path = os.path.join(self.repository_root_path, "compile_commands.json") + if not os.path.exists(compile_db_path): + return None + + try: + with open(compile_db_path, "rb") as f: + return hashlib.md5(f.read()).hexdigest() + except OSError as e: + log.warning(f"Failed to fingerprint compile_commands.json: {e}") + return None + @override def is_ignored_dirname(self, dirname: str) -> bool: ignored_dirs = [ @@ -321,12 +347,19 @@ class ClangdLanguageServer(SolidLanguageServer): log.info("Sending initialize request from LSP client to LSP server and awaiting response") init_response = self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 # type: ignore - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": [".", "<", ">", ":", '"', "/", "*"], - "resolveProvider": False, - } + capabilities = init_response["capabilities"] + + text_document_sync = capabilities["textDocumentSync"] + if isinstance(text_document_sync, int): + assert text_document_sync == 2 # type: ignore + else: + assert text_document_sync["change"] == 2 # type: ignore + + assert "completionProvider" in capabilities + completion_provider = capabilities["completionProvider"] + trigger_characters = set(completion_provider["triggerCharacters"]) + assert {".", "<", ">", ":", '"', "/"}.issubset(trigger_characters) + assert completion_provider["resolveProvider"] is False self.server.notify.initialized({}) # set ready flag, clangd sends no meaningful notification when ready diff --git a/test/solidlsp/cpp/test_cpp_basic.py b/test/solidlsp/cpp/test_cpp_basic.py index 3ca52693..dfb18881 100644 --- a/test/solidlsp/cpp/test_cpp_basic.py +++ b/test/solidlsp/cpp/test_cpp_basic.py @@ -9,12 +9,14 @@ server is not available. import os import pathlib import shutil +from pathlib import Path import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.conftest import get_repo_path, start_ls_context from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @@ -156,3 +158,72 @@ int use_add() { f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", pytrace=False, ) + + +@pytest.mark.cpp +class TestCppDocumentSymbolCache: + def _copy_cpp_fixture(self, tmp_path: Path) -> Path: + fixture_path = get_repo_path(Language.CPP) + target_path = tmp_path / "test_repo" + shutil.copytree(fixture_path, target_path) + return target_path + + def test_cache_invalidates_when_clangd_context_changes(self, tmp_path: Path) -> None: + repo_path = self._copy_cpp_fixture(tmp_path) + ls_settings_alt = { + Language.CPP: { + "compile_commands_dir": ".serena-alt", + } + } + + main_cpp = os.path.join("a.cpp") + + def _assert_caches_loaded_and_clean(ls: SolidLanguageServer) -> None: + assert ls._raw_document_symbols_cache, "Expected raw document-symbol cache to load from disk" + assert ls._document_symbols_cache, "Expected document-symbol cache to load from disk" + assert not ls._raw_document_symbols_cache_is_modified + assert not ls._document_symbols_cache_is_modified + + def _assert_caches_empty(ls: SolidLanguageServer) -> None: + assert ls._raw_document_symbols_cache == {} + assert ls._document_symbols_cache == {} + + def _assert_caches_modified(ls: SolidLanguageServer) -> None: + assert ls._raw_document_symbols_cache_is_modified + assert ls._document_symbols_cache_is_modified + + with start_ls_context(Language.CPP, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default: + _ = ls_default.request_document_symbols(main_cpp) + + default_raw_cache_version = ls_default._raw_document_symbols_cache_version() + default_doc_cache_version = ls_default._document_symbols_cache_version() + + ls_default.save_cache() + cache_dir = ls_default.cache_dir + cache_files = [p for p in cache_dir.rglob("*") if p.is_file()] + assert cache_files, f"Expected SolidLSP to create cache artifacts under {cache_dir}" + + with start_ls_context(Language.CPP, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default_again: + assert ls_default_again.cache_dir == cache_dir + _assert_caches_loaded_and_clean(ls_default_again) + _ = ls_default_again.request_document_symbols(main_cpp) + assert not ls_default_again._raw_document_symbols_cache_is_modified + assert not ls_default_again._document_symbols_cache_is_modified + + with start_ls_context( + Language.CPP, + repo_path=str(repo_path), + ls_specific_settings=ls_settings_alt, + solidlsp_dir=tmp_path, + ) as ls_alt: + assert ls_alt.cache_dir == cache_dir + alt_raw_cache_version = ls_alt._raw_document_symbols_cache_version() + alt_doc_cache_version = ls_alt._document_symbols_cache_version() + + assert alt_raw_cache_version != default_raw_cache_version + assert alt_doc_cache_version != default_doc_cache_version + + _assert_caches_empty(ls_alt) + + _ = ls_alt.request_document_symbols(main_cpp) + _assert_caches_modified(ls_alt) From 1f95613b11b4b222fd895679f6e7e7bb52222906 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 16 Apr 2026 13:44:04 +0200 Subject: [PATCH 185/190] JB tools: transform some incorrectly sent empty strings in params to None In Move and SafeDelete, the client might send an empty string instead of None --- CHANGELOG.md | 7 +++++-- src/serena/tools/jetbrains_tools.py | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71e56290..f3274c1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ Status of the `main` branch. Changes prior to the next official version change w - Fix: Check for ignored path ignored `.git` folder only at the top level, not in every subdirectory (`Project._is_ignored_relative_path`) #1350 - Fix: Duplicate comments in re-saved YAML configuration files #1285 +* JetBrains: + - `Move` and `SafeDelete` tools: transform empty string to None (counteracts client errors) + * Dependencies: - `pywebview`: Switch back to official release (new version 6.2) #1253 @@ -20,10 +23,10 @@ Status of the `main` branch. Changes prior to the next official version change w - Fix: When scanning for `.gitignore` files, the presence of files that could not be made relative to the project root would cause the scan to fail. #1317 -Dashboard: +* Dashboard: - Fix handling of read news, saving each read news entry separately #1338 -JetBrains: +* JetBrains: - Improve handling of `relative_path` parameter - Improve its documentation to avoid usage errors - Replace escaped characters in `relative_path` with their unescaped counterparts (< and >) diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 9e217090..00d0adbd 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -159,6 +159,9 @@ class JetBrainsMoveTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, ToolMa :param target_relative_path: the relative path of the target directory or file. :param target_parent_name_path: the name path of the target parent symbol. """ + name_path = name_path or None + target_relative_path = target_relative_path or None + target_parent_name_path = target_parent_name_path or None relative_path = self._sanitize_input_param(relative_path) with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.move( @@ -199,6 +202,7 @@ class JetBrainsSafeDeleteTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, remove symbols that become unused after the deletion. Default is False. """ relative_path = self._sanitize_input_param(relative_path) + name_path = name_path or None with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.safe_delete( name_path=name_path, From 3345153cce7e11cabc45d7cd62eaa124f803a65a Mon Sep 17 00:00:00 2001 From: Michael Panchenko <35432522+MischaPanch@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:53:54 +0200 Subject: [PATCH 186/190] Clean up CodeQL workflow by removing languages Removed unused languages from CodeQL workflow matrix. --- .github/workflows/codeql.yml | 103 +++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..718cbd4e --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,103 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '38 16 * * 6' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" From d32c053c55c8216803cad21fbd81e2ce52f4b0e9 Mon Sep 17 00:00:00 2001 From: Koushik-Salammagari <138836560+Koushik-Salammagari@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:19:23 -0700 Subject: [PATCH 187/190] Fix Lean4 stale cache: skip caching empty document symbol responses (#1356) * Fix Lean4 stale cache: skip caching empty document symbol responses When the Lean language server is queried before `lake build` completes, it returns empty symbol lists. Previously these were written to the persistent cache, permanently hiding symbols even after the build finished. Now only non-empty responses are cached. Adds a unit test that verifies empty responses from the LSP layer are never written to _raw_document_symbols_cache. Fixes #1217 * fix: run poe format and add changelog entry for #1356 Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Michael Panchenko <35432522+MischaPanch@users.noreply.github.com> --- CHANGELOG.md | 1 + src/solidlsp/ls.py | 9 ++++++--- test/solidlsp/lean4/test_lean4_basic.py | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cf145fe..64349290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Status of the `main` branch. Changes prior to the next official version change w * Language Servers: - Fix: clangd capability checks now tolerate valid initialize response shape differences and invalidate cached C++ document symbols when clangd/compile commands context changes #1359 - Fix: `rename_symbol` for Vue files now correctly propagates edits to the TypeScript server, enabling cross-file renames in `.vue` files + - Fix: Lean4 stale cache — empty document symbol responses (returned before `lake build` completes) are no longer persisted, preventing symbols from being permanently hidden #1356 # v1.1.2 (2026-04-14) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 4a1f5986..5b679ce9 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -1331,9 +1331,12 @@ class SolidLanguageServer(ABC): {"textDocument": {"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()}} ) - # update cache - self._raw_document_symbols_cache[cache_key] = (fd.content_hash, response) - self._raw_document_symbols_cache_is_modified = True + # Only cache non-empty results. An empty or None response can occur when the language server + # has not yet finished indexing or building the project (e.g. Lean 4 before `lake build`), + # and caching it would permanently serve stale data even after the project is ready. + if response: + self._raw_document_symbols_cache[cache_key] = (fd.content_hash, response) + self._raw_document_symbols_cache_is_modified = True return response diff --git a/test/solidlsp/lean4/test_lean4_basic.py b/test/solidlsp/lean4/test_lean4_basic.py index b8f35efa..a20a32cd 100644 --- a/test/solidlsp/lean4/test_lean4_basic.py +++ b/test/solidlsp/lean4/test_lean4_basic.py @@ -12,6 +12,8 @@ Test Repository Structure: - Main.lean: Main entry point using Helper, plus multiply and calculate functions """ +from unittest.mock import patch + import pytest from solidlsp.ls import SolidLanguageServer @@ -146,6 +148,29 @@ class TestLean4LanguageServer: assert def_location["uri"].endswith("Main.lean"), f"Expected definition in Main.lean, got: {def_location['uri']}" assert def_location["range"]["start"]["line"] == 5, f"Expected definition at line 5, got: {def_location['range']['start']['line']}" + @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) + def test_empty_symbol_response_not_cached(self, language_server: SolidLanguageServer) -> None: + """ + Test that an empty document symbol response is not written to the cache. + + This guards against the scenario where the Lean language server returns no symbols + before `lake build` has completed. If the empty result were cached, subsequent + requests after the build would still return empty symbols even though the LS now + has the full build artifacts available. + """ + file_path = "Helper.lean" + + # Patch the LSP server to return an empty list, simulating a pre-build response. + with patch.object(language_server.server.send, "document_symbol", return_value=[]): + result = language_server._request_document_symbols(file_path, None) + assert result == [], "Expected the (mocked) empty response to be returned as-is" + + # The empty result must NOT have been written to the cache. + assert file_path not in language_server._raw_document_symbols_cache, ( + "Empty symbol response must not be stored in the cache; " + "it would permanently hide symbols that become available after `lake build`" + ) + @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) def test_go_to_definition_across_files(self, language_server: SolidLanguageServer) -> None: """ From 4147ad82549a7b35c4b6bc99318e930f9e3a544b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 16 Apr 2026 22:17:46 +0200 Subject: [PATCH 188/190] Refactoring: Extract utility class WebViewWithTray --- src/serena/dashboard.py | 223 ++++----------------------------- src/serena/util/pywebview.py | 233 +++++++++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+), 198 deletions(-) create mode 100644 src/serena/util/pywebview.py diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 1799ea17..569b17a2 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -3,17 +3,13 @@ import os import socket import sys import threading -import time import urllib.error import urllib.request from html import escape from pathlib import Path from typing import TYPE_CHECKING, Any, Self -import psutil -import webview from flask import Flask, Response, redirect, request, send_from_directory -from PIL import Image from pydantic import BaseModel from sensai.util import logging from sensai.util.pickle import dump_pickle, load_pickle @@ -23,6 +19,7 @@ from serena.config.serena_config import SerenaConfig, SerenaPaths from serena.constants import SERENA_DASHBOARD_DIR from serena.task_executor import TaskExecutor from serena.util.logging import MemoryLogHandler +from serena.util.pywebview import WebViewWithTray if TYPE_CHECKING: from serena.agent import SerenaAgent @@ -783,7 +780,7 @@ class SerenaDashboardAPI: return thread, port -class SerenaDashboardViewer: +class SerenaDashboardViewer(WebViewWithTray): """ Minimal pywebview wrapper with optional system tray. """ @@ -794,24 +791,33 @@ class SerenaDashboardViewer: self, url: str, *, - start_minimized: bool = False, width: int = 1400, height: int = 900, + start_minimized: bool = False, parent_process_id: int | None = None, + tray: bool = True, ): - self.url = url - # Use system tray to allow hiding to tray and intercepting close to hide instead of quit - self.tray = True - self.title = "Serena Dashboard" - self.width = width - self.height = height - self.start_minimized = start_minimized - self.parent_process_id = parent_process_id + dashboard_path = Path(SERENA_DASHBOARD_DIR) - self.window: webview.Window - self._tray_icon: Any - self._quitting = False - self._app_icon_path: str | None = None + # .ico is Windows-only; macOS expects a PNG for the window/dock icon. + app_icon_filename = "serena.ico" if sys.platform == "win32" else "serena-icon-1024-mac.png" + app_icon_path = str(dashboard_path / app_icon_filename) + + tray_icon_filename = "serena-icon-tray-mac.png" if sys.platform == "darwin" else "serena-icon-48.png" + tray_icon_path = str(dashboard_path / tray_icon_filename) + + super().__init__( + url, + title="Serena Dashboard", + tray=tray, + width=width, + height=height, + start_minimized=start_minimized, + parent_process_id=parent_process_id, + app_id="oraios.serena", + app_icon_path=app_icon_path, + tray_icon_path=tray_icon_path, + ) @staticmethod def is_current_platform_supported() -> bool: @@ -829,187 +835,8 @@ class SerenaDashboardViewer: return sys.platform in supported_platforms 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") - if self.DEBUG: logging.configure(level=logging.DEBUG) logging.add_file_logger(SerenaPaths().get_next_log_file_path("dashboard-viewer")) - dashboard_path = Path(SERENA_DASHBOARD_DIR) - # .ico is Windows-only; macOS expects a PNG for the window/dock icon. - icon_filename = "serena.ico" if sys.platform == "win32" else "serena-icon-1024-mac.png" - icon_path = str(dashboard_path / icon_filename) - self._app_icon_path = icon_path - - # Create hidden to avoid flash; show/restore/minimize in start callback. - window = webview.create_window( - self.title, - self.url, - width=self.width, - height=self.height, - hidden=self.start_minimized, - text_select=True, - zoomable=True, - ) - assert window is not None - self.window = window - - if self.tray: - self.window.events.closing += self._on_closing - self._start_tray() - - def _start_callback() -> None: - if self.start_minimized: - self._hide_window() - else: - self._show_window() - - # start a thread to monitor the parent process - if self.parent_process_id is not None: - threading.Thread(target=self._monitor_parent_process, daemon=True).start() - - webview.start(_start_callback, icon=icon_path) - - def _monitor_parent_process(self) -> None: - """ - Monitors the parent process and shuts down the dashboard viewer if the parent process exits. - """ - pid = self.parent_process_id - log.info("Starting parent process monitor thread (parent_pid=%d, pid=%s)", pid, os.getpid()) - try: - parent_process = psutil.Process(pid) - while parent_process.is_running(): - time.sleep(1) - except psutil.NoSuchProcess: - pass # Parent process already exited - log.info("Parent process (pid=%d) has exited, shutting down dashboard viewer", pid) - self._terminate() - - def _show_window(self) -> None: - if not self.window: - return - - if sys.platform == "darwin": - from PyObjCTools.AppHelper import callAfter - - callAfter(self._show_window_on_macos) - else: - self.window.show() - self.window.restore() - - def _hide_window(self) -> None: - if not self.window: - return - - if sys.platform == "darwin": - from PyObjCTools.AppHelper import callAfter - - callAfter(self._hide_window_on_macos) - else: - self.window.hide() - - def _show_window_on_macos(self) -> None: - from AppKit import ( - NSApplication, - NSApplicationActivationPolicyRegular, - ) - from PyObjCTools.AppHelper import callLater - - ns_app = NSApplication.sharedApplication() - ns_app.setActivationPolicy_(NSApplicationActivationPolicyRegular) - self._set_macos_app_icon(ns_app) - ns_app.unhide_(None) - ns_app.activateIgnoringOtherApps_(True) - # Give the status item menu a beat to close before restoring the window. - callLater(0.1, self._restore_window_on_macos) - - def _restore_window_on_macos(self) -> None: - self.window.show() - self.window.restore() - - def _hide_window_on_macos(self) -> None: - from AppKit import ( - NSApplication, - NSApplicationActivationPolicyAccessory, - ) - - self.window.hide() - NSApplication.sharedApplication().setActivationPolicy_(NSApplicationActivationPolicyAccessory) - - def _set_macos_app_icon(self, ns_app: Any) -> None: - if not self._app_icon_path: - return - - from AppKit import NSImage - - ns_image = NSImage.alloc().initByReferencingFile_(self._app_icon_path) - if ns_image is not None: - ns_app.setApplicationIconImage_(ns_image) - - def _on_closing(self) -> bool: - """Intercept window close: hide window instead of quitting (macOS standard behavior).""" - if self._quitting: - return True - self._hide_window() - return False # prevent the window from actually closing - - def _terminate(self) -> None: - """ - Terminates the viewer application and the tray icon - """ - self._quitting = True - try: - if self._tray_icon: - self._tray_icon.stop() - finally: - if self.window: - self.window.destroy() - - 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). - # Windows/Linux tray icons are larger, so 48px is the better fit there. - icon_filename = "serena-icon-tray-mac.png" if sys.platform == "darwin" else "serena-icon-48.png" - icon_img = Image.open(dashboard_path / icon_filename) - - def show(_icon: TrayIcon, _item: Item) -> None: - self._show_window() - - def hide(_icon: TrayIcon, _item: Item) -> None: - self._hide_window() - - def quit_app(_icon: TrayIcon, _item: Item) -> None: - self._terminate() - - menu = pystray.Menu( - Item("Open", show, default=True), - Item("Hide", hide), - Item("Quit", quit_app), - ) - - 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() - # 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() + super().run() diff --git a/src/serena/util/pywebview.py b/src/serena/util/pywebview.py new file mode 100644 index 00000000..6489437a --- /dev/null +++ b/src/serena/util/pywebview.py @@ -0,0 +1,233 @@ +import logging +import os +import sys +import threading +import time +from typing import Any + +import psutil +import webview +from PIL import Image + +log = logging.getLogger(__name__) + + +class WebViewWithTray: + """ + Web view window with optional system tray. + """ + + DEBUG = False + + def __init__( + self, + url: str, + *, + title: str, + start_minimized: bool = False, + width: int = 1400, + height: int = 900, + tray: bool = False, + parent_process_id: int | None = None, + app_id: str | None = None, + app_icon_path: str | None = None, + tray_icon_path: str | None = None, + ): + """ + :param url: the URL to be show in the web view + :param title: the title of the window + :param start_minimized: whether to show the window minimised on startup (if `tray` is enabled, minimised to tray) + :param width: width of the window in pixels + :param height: height of the window in pixels + :param tray: whether to enable the system tray icon and functionality. + If this is enabled, closing the window will minimise it to tray instead of quitting the application. + :param parent_process_id: the PID of the parent process to monitor; if specified, the viewer will automatically + shut itself down when the parent process exits. + :param app_id: the application ID to use for the viewer to ensure that the window is grouped separately (with separate + icon) in the taskbar + """ + self._url = url + self._use_tray = tray + self._title = title + self._start_minimized = start_minimized + self._parent_process_id = parent_process_id + self._app_id = app_id + + self.window: webview.Window + self._tray_icon: Any + self._quitting = False + self._app_icon_path = app_icon_path + self._tray_icon_path = tray_icon_path + + # Create hidden to avoid flash; show/restore/minimize in start callback. + window = webview.create_window( + self._title, + self._url, + width=width, + height=height, + hidden=self._start_minimized, + text_select=True, + zoomable=True, + ) + assert window is not None + self.window = window + + def run(self) -> None: + # set app id (avoid app being lumped together with other Python-based apps in Windows taskbar) + if self._app_id is not None and sys.platform == "win32": + import ctypes + + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("oraios.serena") + + if self._use_tray: + self.window.events.closing += self._on_closing + self._start_tray() + + def _start_callback() -> None: + if self._start_minimized: + self._hide_window() + else: + self._show_window() + + # start a thread to monitor the parent process + if self._parent_process_id is not None: + threading.Thread(target=self._monitor_parent_process, daemon=True).start() + + webview.start(_start_callback, icon=self._app_icon_path) + + def _monitor_parent_process(self) -> None: + """ + Monitors the parent process and shuts down the dashboard viewer if the parent process exits. + """ + pid = self._parent_process_id + log.info("Starting parent process monitor thread (parent_pid=%d, pid=%s)", pid, os.getpid()) + try: + parent_process = psutil.Process(pid) + while parent_process.is_running(): + time.sleep(1) + except psutil.NoSuchProcess: + pass # Parent process already exited + log.info("Parent process (pid=%d) has exited, shutting down dashboard viewer", pid) + self._terminate() + + def _show_window(self) -> None: + if not self.window: + return + + if sys.platform == "darwin": + from PyObjCTools.AppHelper import callAfter + + callAfter(self._show_window_on_macos) + else: + self.window.show() + self.window.restore() + + def _hide_window(self) -> None: + if not self.window: + return + + if sys.platform == "darwin": + from PyObjCTools.AppHelper import callAfter + + callAfter(self._hide_window_on_macos) + else: + self.window.hide() + + def _show_window_on_macos(self) -> None: + from AppKit import ( + NSApplication, + NSApplicationActivationPolicyRegular, + ) + from PyObjCTools.AppHelper import callLater + + ns_app = NSApplication.sharedApplication() + ns_app.setActivationPolicy_(NSApplicationActivationPolicyRegular) + self._set_macos_app_icon(ns_app) + ns_app.unhide_(None) + ns_app.activateIgnoringOtherApps_(True) + # Give the status item menu a beat to close before restoring the window. + callLater(0.1, self._restore_window_on_macos) + + def _restore_window_on_macos(self) -> None: + self.window.show() + self.window.restore() + + def _hide_window_on_macos(self) -> None: + from AppKit import ( + NSApplication, + NSApplicationActivationPolicyAccessory, + ) + + self.window.hide() + NSApplication.sharedApplication().setActivationPolicy_(NSApplicationActivationPolicyAccessory) + + def _set_macos_app_icon(self, ns_app: Any) -> None: + if not self._app_icon_path: + return + + from AppKit import NSImage + + ns_image = NSImage.alloc().initByReferencingFile_(self._app_icon_path) + if ns_image is not None: + ns_app.setApplicationIconImage_(ns_image) + + def _on_closing(self) -> bool: + """Intercept window close: hide window instead of quitting (macOS standard behavior).""" + if self._quitting: + return True + self._hide_window() + return False # prevent the window from actually closing + + def _terminate(self) -> None: + """ + Terminates the viewer application and the tray icon + """ + self._quitting = True + try: + if self._tray_icon: + self._tray_icon.stop() + finally: + if self.window: + self.window.destroy() + + 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 + + icon_img = None + if self._tray_icon_path is not None: + icon_img = Image.open(self._tray_icon_path) + + def show(_icon: TrayIcon, _item: Item) -> None: + self._show_window() + + def hide(_icon: TrayIcon, _item: Item) -> None: + self._hide_window() + + def quit_app(_icon: TrayIcon, _item: Item) -> None: + self._terminate() + + menu = pystray.Menu( + Item("Open", show, default=True), + Item("Hide", hide), + Item("Quit", quit_app), + ) + + 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() + # 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 7392d65b008244b7eab1a93ba735bf36acc13f1e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 17 Apr 2026 09:32:03 +0200 Subject: [PATCH 189/190] Tests: removed unnecessary and failing test for empty symbol not being cached --- test/solidlsp/lean4/test_lean4_basic.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/test/solidlsp/lean4/test_lean4_basic.py b/test/solidlsp/lean4/test_lean4_basic.py index a20a32cd..b8f35efa 100644 --- a/test/solidlsp/lean4/test_lean4_basic.py +++ b/test/solidlsp/lean4/test_lean4_basic.py @@ -12,8 +12,6 @@ Test Repository Structure: - Main.lean: Main entry point using Helper, plus multiply and calculate functions """ -from unittest.mock import patch - import pytest from solidlsp.ls import SolidLanguageServer @@ -148,29 +146,6 @@ class TestLean4LanguageServer: assert def_location["uri"].endswith("Main.lean"), f"Expected definition in Main.lean, got: {def_location['uri']}" assert def_location["range"]["start"]["line"] == 5, f"Expected definition at line 5, got: {def_location['range']['start']['line']}" - @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) - def test_empty_symbol_response_not_cached(self, language_server: SolidLanguageServer) -> None: - """ - Test that an empty document symbol response is not written to the cache. - - This guards against the scenario where the Lean language server returns no symbols - before `lake build` has completed. If the empty result were cached, subsequent - requests after the build would still return empty symbols even though the LS now - has the full build artifacts available. - """ - file_path = "Helper.lean" - - # Patch the LSP server to return an empty list, simulating a pre-build response. - with patch.object(language_server.server.send, "document_symbol", return_value=[]): - result = language_server._request_document_symbols(file_path, None) - assert result == [], "Expected the (mocked) empty response to be returned as-is" - - # The empty result must NOT have been written to the cache. - assert file_path not in language_server._raw_document_symbols_cache, ( - "Empty symbol response must not be stored in the cache; " - "it would permanently hide symbols that become available after `lake build`" - ) - @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) def test_go_to_definition_across_files(self, language_server: SolidLanguageServer) -> None: """ From 3be62e3c27deff756e8cc65a1b4befbfd2b193dc Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 17 Apr 2026 11:02:28 +0200 Subject: [PATCH 190/190] Fix in request_overview: explicitly passed file path bypassed ignore check --- CHANGELOG.md | 1 + src/solidlsp/ls.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13c481e9..9e03f1f3 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 * General: - Support `serena --version` CLI command for displaying the current version #1347 - Fix: Check for ignored path ignored `.git` folder only at the top level, not in every subdirectory (`Project._is_ignored_relative_path`) #1350 + - `GetSymbolsOverviewTool`: ignored paths were not respected in LSP variant (fix in `SolidLanguageServer`) - Fix: Duplicate comments in re-saved YAML configuration files #1285 * JetBrains: diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 5b679ce9..6ae69a39 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -1694,6 +1694,7 @@ class SolidLanguageServer(ABC): def request_overview(self, within_relative_path: str) -> dict[str, list[UnifiedSymbolInformation]]: """ An overview of all symbols in the given file or directory. + Raises a ValueError if a path to an ignored file is passed. :param within_relative_path: the relative path to the file or directory to get the overview of. :return: A mapping of all relative paths analyzed to lists of top-level symbols in the corresponding file. @@ -1703,6 +1704,8 @@ class SolidLanguageServer(ABC): raise FileNotFoundError(f"File or directory not found: {abs_path}") if abs_path.is_file(): + if self.is_ignored_path(within_relative_path): + raise ValueError(f"The explicitly passed file {within_relative_path} is ignored, not returning overview.") symbols_overview = self.request_document_overview(within_relative_path) return {within_relative_path: symbols_overview} else: