diff --git a/CHANGELOG.md b/CHANGELOG.md index ef7afa61..7a2fe6dc 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 solving several issues related to retrieval problems in Java projects * Major extensions to the dashboard, which now serves as a central web interface for Serena * View current configuration + * View news which can be marked as read * View the executions, with the possibility to cancel running/scheduled executions * View tool usage statistics * View and create memories and edit the serena configuration file @@ -29,6 +30,8 @@ Status of the `main` branch. Changes prior to the next official version change w * New mode `oaicompat-agent` and extensions enhancing OpenAI tool compatibility, permitting Serena to work with llama.cpp * Tools: + * *New tool*: `jet_brains_type_hierarchy` + * Symbol information (hover, docstring, quick-info) is now provided as part of `find_symbol` and related tool responses. * 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) @@ -57,6 +60,7 @@ Status of the `main` branch. Changes prior to the next official version change w * **Ruby dual language server support**: Added ruby-lsp as the modern primary Ruby language server. Solargraph remains available as an experimental legacy option. ruby-lsp supports both .rb and .erb files, while Solargraph supports .rb files only. * **Add support for PowerShell** via PowerShell Editor Services (PSES). Requires `pwsh` (PowerShell Core) to be installed and available in PATH. Supports symbol navigation, go-to-definition, and within-file references for .ps1 files. * **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. # 0.1.4 diff --git a/docs/01-about/035_tools.md b/docs/01-about/035_tools.md index 23662750..293651e6 100644 --- a/docs/01-about/035_tools.md +++ b/docs/01-about/035_tools.md @@ -3,41 +3,42 @@ Find the full list of Serena's tools below (output of ` tools list --all`). Note that in most configurations, only a subset of these tools will be enabled simultaneously (see the section on [configuration](../02-usage/050_configuration) for details). - * `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. +* `edit_memory`: * `execute_shell_command`: Executes a shell command. * `find_file`: Finds files in the given relative paths -* `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 filter - ed by type). -* `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, - contexts, and modes. +* `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`: 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! * `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. -* `jet_brains_find_referencing_symbols`: Finds symbols that reference the given symbol -* `jet_brains_find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (option - ally filtered by type). -* `jet_brains_get_symbols_overview`: Retrieves an overview of the top-level symbols within a specified file +* `jet_brains_find_referencing_symbols`: Finds symbols that reference the given symbol using the JetBrains backend +* `jet_brains_find_symbol`: Performs a global (or local) search for symbols using the JetBrains backend +* `jet_brains_get_symbols_overview`: Retrieves an overview of the top-level symbols within a specified file using the JetBrains backend +* `jet_brains_type_hierarchy`: Retrieves the type hierarchy (supertypes and/or subtypes) of a symbol using the JetBrains backend * `list_dir`: Lists files and directories in the given directory (optionally with recursion). * `list_memories`: Lists memories in Serena's project-specific memory store. * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). * `open_dashboard`: Opens the Serena web dashboard in the default web browser. -* `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with th - e necessary context). + The dashboard provides logs, session information, and tool usage statistics. +* `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. * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities. -* `replace_lines`: Replaces a range of lines within a file with new content. * `replace_content`: Replaces content in a file (optionally using regular expressions). -* `replace_symbol_body`: Replaces the full definition of a symbol. +* `replace_lines`: Replaces a range of lines within a file with new content. +* `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend. * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. * `search_for_pattern`: Performs a search for a pattern in the project. * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. @@ -45,4 +46,4 @@ Note that in most configurations, only a subset of these tools will be enabled s * `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`: Writes a named memory (for future reference) to Serena's project-specific memory store. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 375bbda5..64a3d14d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ dependencies = [ "tqdm>=4.67.1", "tiktoken>=0.9.0", "anthropic>=0.54.0", + "beautifulsoup4>=4.14.2", ] [[tool.uv.index]] @@ -315,4 +316,4 @@ markers = [ skip = '.git*,*.svg,*.lock,*.min.*' check-hidden = true # ignore-regex = '' -ignore-words-list = 'paket' \ No newline at end of file +ignore-words-list = 'paket' diff --git a/scripts/demo_run_tools.py b/scripts/demo_run_tools.py index 8cbaae79..bad7ed60 100644 --- a/scripts/demo_run_tools.py +++ b/scripts/demo_run_tools.py @@ -7,21 +7,30 @@ import json from pprint import pprint from serena.agent import SerenaAgent +from serena.config.serena_config import SerenaConfig from serena.constants import REPO_ROOT -from serena.tools import FindFileTool, FindReferencingSymbolsTool, GetSymbolsOverviewTool, JetBrainsFindSymbolTool, SearchForPatternTool +from serena.tools import ( + FindFileTool, + FindReferencingSymbolsTool, + JetBrainsFindSymbolTool, + JetBrainsGetSymbolsOverviewTool, + SearchForPatternTool, +) if __name__ == "__main__": - agent = SerenaAgent(project=REPO_ROOT) + serena_config = SerenaConfig.from_config_file() + serena_config.web_dashboard = False + agent = SerenaAgent(project=REPO_ROOT, serena_config=serena_config) # apply a tool find_symbol_tool = agent.get_tool(JetBrainsFindSymbolTool) find_refs_tool = agent.get_tool(FindReferencingSymbolsTool) find_file_tool = agent.get_tool(FindFileTool) search_pattern_tool = agent.get_tool(SearchForPatternTool) - overview_tool = agent.get_tool(GetSymbolsOverviewTool) + overview_tool = agent.get_tool(JetBrainsGetSymbolsOverviewTool) result = agent.execute_task( - lambda: find_symbol_tool.apply("displayBasicStats"), + lambda: find_symbol_tool.apply("SerenaAgent/get_tool_description_override"), ) pprint(json.loads(result)) # input("Press Enter to continue...") diff --git a/src/serena/agent.py b/src/serena/agent.py index ae39e8cd..c16bf55c 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -743,3 +743,9 @@ class SerenaAgent: def get_tool_by_name(self, tool_name: str) -> Tool: tool_class = ToolRegistry().get_tool_class_by_name(tool_name) return self.get_tool(tool_class) + + def get_active_lsp_languages(self) -> list[Language]: + ls_manager = self.get_language_server_manager() + if ls_manager is None: + return [] + return ls_manager.get_active_languages() diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 7933c214..62fc1b29 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -80,6 +80,10 @@ 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") + """ + file containing the ID of the last read news snippet + """ def get_next_log_file_path(self, prefix: str) -> str: """ diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 7d1f6268..0165d1c4 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -2,6 +2,7 @@ import os import socket import threading from collections.abc import Callable +from pathlib import Path from typing import TYPE_CHECKING, Any, Self from flask import Flask, Response, request, send_from_directory @@ -9,7 +10,7 @@ from pydantic import BaseModel from sensai.util import logging from serena.analytics import ToolUsageStats -from serena.config.serena_config import LanguageBackend +from serena.config.serena_config import LanguageBackend, SerenaPaths from serena.constants import SERENA_DASHBOARD_DIR from serena.task_executor import TaskExecutor from serena.util.logging import MemoryLogHandler @@ -317,6 +318,36 @@ 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_snippet_id_file = SerenaPaths().news_snippet_id_file + if not os.path.exists(news_snippet_id_file): + return all_news_ids + with open(news_snippet_id_file, encoding="utf-8") as f: + last_read_news_id = int(f.read().strip()) + return [news_id for news_id in all_news_ids if news_id > last_read_news_id] + + try: + unread_news_ids = _get_unread_news_ids() + return {"news_snippet_ids": unread_news_ids, "status": "success"} + except Exception as e: + return {"status": "error", "message": str(e)} + + @self._app.route("/mark_news_snippet_as_read", methods=["POST"]) + 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_file = SerenaPaths().news_snippet_id_file + with open(news_snippet_id_file, "w", encoding="utf-8") as f: + f.write(str(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)} + def _get_log_messages(self, request_log: RequestLog) -> ResponseLog: all_messages = self._memory_log_handler.get_log_messages() requested_messages = all_messages[request_log.start_idx :] if request_log.start_idx <= len(all_messages) else [] diff --git a/src/serena/ls_manager.py b/src/serena/ls_manager.py index 8f3681bf..6fdf0c24 100644 --- a/src/serena/ls_manager.py +++ b/src/serena/ls_manager.py @@ -182,6 +182,14 @@ class LanguageServerManager: ls = self._language_servers.pop(language) self._stop_language_server(ls, save_cache=save_cache) + def get_active_languages(self) -> list[Language]: + """ + Returns the list of languages for which language servers are currently managed. + + :return: list of languages + """ + return list(self._language_servers.keys()) + @staticmethod def _stop_language_server(ls: SolidLanguageServer, save_cache: bool = False, timeout: float = 2.0) -> None: if ls.is_running(): diff --git a/src/serena/resources/config/internal_modes/jetbrains.yml b/src/serena/resources/config/internal_modes/jetbrains.yml index 7af49efe..863f3beb 100644 --- a/src/serena/resources/config/internal_modes/jetbrains.yml +++ b/src/serena/resources/config/internal_modes/jetbrains.yml @@ -13,3 +13,4 @@ included_optional_tools: - jet_brains_find_symbol - jet_brains_find_referencing_symbols - jet_brains_get_symbols_overview + - jet_brains_type_hierarchy diff --git a/src/serena/resources/dashboard/dashboard.css b/src/serena/resources/dashboard/dashboard.css index 1bcc7a23..08ea8519 100644 --- a/src/serena/resources/dashboard/dashboard.css +++ b/src/serena/resources/dashboard/dashboard.css @@ -49,6 +49,100 @@ html { --stats-header: #3a3a3a; } +.news-section { + background: var(--bg-secondary); + padding: 20px; + border-radius: var(--border-radius); + box-shadow: var(--shadow); + margin-bottom: 25px; +} + +.news-section h2 { + margin: 0 0 20px 0; + font-size: 18px; + color: var(--text-primary); +} + +.news-item { + padding: 20px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + margin-bottom: 15px; + background: var(--bg-primary); + position: relative; +} + +.news-item:last-child { + margin-bottom: 0; +} + +.news-item h3 { + margin: 0 0 10px 0; + font-size: 16px; + color: var(--text-primary); +} + +.news-item .date { + color: var(--text-muted); + font-size: 13px; + margin: 0 0 15px 0; +} + +.news-item p { + margin: 10px 0; + line-height: 1.6; + color: var(--text-secondary); +} + +.news-item ul { + margin: 10px 0; + padding-left: 25px; + color: var(--text-secondary); +} + +.news-item ul li { + margin: 8px 0; + line-height: 1.5; +} + +.news-item strong { + color: var(--text-primary); +} + +.news-mark-read { + position: absolute; + top: 20px; + right: 20px; +} + +.news-mark-read-btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + transition: background-color 0.3s ease; + white-space: nowrap; +} + +.news-mark-read-btn:hover { + background-color: var(--btn-hover); +} + +.news-mark-read-btn:disabled { + background-color: var(--btn-disabled); + cursor: not-allowed; +} + +.news-no-items { + color: var(--text-muted); + font-style: italic; + text-align: center; + padding: 20px; +} + body { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; margin: 0; diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index 63a98419..b7cd3c56 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -268,6 +268,8 @@ class Dashboard { this.$editSerenaConfigSaveBtn = $('#edit-serena-config-save-btn'); this.$editSerenaConfigCancelBtn = $('#edit-serena-config-cancel-btn'); this.$modalCloseEditSerenaConfig = $('.modal-close-edit-serena-config'); + this.$newsSection = $('#news-section'); + this.$newsDisplay = $('#news-display'); // Chart references this.countChart = null; @@ -401,6 +403,7 @@ class Dashboard { // Initialize the application this.loadToolNames().then(function () { // Start on overview page + self.loadNews(); self.loadConfigOverview(); self.startConfigPolling(); self.startExecutionsPolling(); @@ -454,6 +457,7 @@ class Dashboard { // Start appropriate polling for the page if (page === 'overview') { + this.loadNews(); this.loadConfigOverview(); this.startConfigPolling(); this.startExecutionsPolling(); @@ -1877,6 +1881,104 @@ class Dashboard { }); } + // ===== News Methods ===== + + loadNews() { + let self = this; + console.log('Loading news...'); + $.ajax({ + url: '/news_snippet_ids', + 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); + } else { + console.log('No unread news, hiding section'); + self.$newsSection.hide(); + } + }, + error: function(xhr, status, error) { + console.error('Error loading news snippet IDs:', error); + self.$newsSection.hide(); + } + }); + } + + displayNews(newsIds) { + 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(); + return; + } + 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 = $('