Merge remote-tracking branch 'origin/main' into agent-config-update-events

Conflicts:
	CHANGELOG.md
	src/serena/agent.py
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-06-01 11:25:23 +02:00
commit 4aeef5ab85
21 files changed
+366 -145

No files matched your search

+26 -1
View File
@@ -2,11 +2,36 @@
Status of the `main` branch. Changes prior to the next official version change will appear here.
* Language Servers:
- `typescript_vts`: Add `initialization_options` setting in `ls_specific_settings.typescript_vts`.
The dict is forwarded to vtsls via `initializationOptions`, `workspace/didChangeConfiguration`,
and `workspace/configuration` pulls. Enables Yarn PnP setups with `typescript.tsdk` pointing
at the Yarn-generated SDK.
* Dashboard:
- Make list of trusted hosts configurable, fixing host validation introduced in v1.5.2 allowing only
default local hostnames, effectively preventing remote connections
- Decouple configuration computation from the agent's task queue by introducing events for agent config/status updates.
This allows the dashboard to display the configuration while the project provided at startup is still initialising. #1064
- Fix empty executions queue displaying "Loading..."
# v1.5.3 (2026-05-26)
Add meta-data for the GitHub MCP registry
# v1.5.2 (2026-05-26)
* General:
- Not existing paths return `False` on is ignored checks (instead of raising an error)
- Add `serena-agent` CLI command so that `uvx serena-agent` can be used as entrypoint.
- Fortls and pyright are now installed on the fly instead of being bundled in the serena-agent package.
* Dashboard:
- Add host validation
* Hooks:
- Extend list of extensions that are considered code files (affects the reminder hook counter).
# v1.5.1 (2026-05-18)
* General:
+3 -1
View File
@@ -226,7 +226,7 @@ To achieve this, Serena offers multiple levels of (composable) configuration:
**Install Serena**. Serena is installed via uv as follows:
```bash
uv tool install -p 3.13 serena-agent@latest --prerelease=allow
uv tool install -p 3.13 serena-agent
```
After successful installation, the command `serena` should be available in your shell.
@@ -261,3 +261,5 @@ 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.
<!-- mcp-name: io.github.oraios/serena -->
+2 -2
View File
@@ -20,7 +20,7 @@ to be provided manually, this is mentioned in the notes below the respective lan
With `uv` installed and on your PATH, install Serena with this command:
uv tool install -p 3.13 serena-agent@latest --prerelease=allow
uv tool install -p 3.13 serena-agent
Upon completion, the command `serena` should be available in your terminal.
@@ -37,7 +37,7 @@ Note that you can switch backends at any time via Serena's [configuration](050_c
To update Serena to the latest version, run:
uv tool upgrade serena-agent --prerelease=allow
uv tool upgrade serena-agent
:::{tip}
To keep informed about updates, make sure you regularly open [Serena's Dashboard](060_dashboard),
+24
View File
@@ -1022,6 +1022,30 @@ Supported settings:
|---|---|---|
| `vtsls_version` | `0.2.9` | Override the `@vtsls/language-server` npm package version Serena installs. |
| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. |
| `initialization_options` | `null` | Dict forwarded to vtsls on three LSP channels: the `initializationOptions` field of the `initialize` request, a `workspace/didChangeConfiguration` notification sent right after initialize, and as the response to `workspace/configuration` pull requests (section-scoped). Typical use is Yarn PnP: point `typescript.tsdk` at the Yarn-generated SDK and enable `vtsls.autoUseWorkspaceTsdk`. |
Example (Yarn PnP project with TypeScript in a subdirectory; run `yarn dlx @yarnpkg/sdks vscode` in the project once to generate the SDK):
```yaml
ls_specific_settings:
typescript_vts:
initialization_options:
typescript:
tsdk: "project/.yarn/sdks/typescript/lib"
vtsls:
autoUseWorkspaceTsdk: true
```
vtsls reads `typescript.tsdk` through the `workspace/configuration` pull, not through `initializationOptions`, so Serena answers those pulls from the same dict (and also pushes it on `workspace/didChangeConfiguration` for compatibility with servers that expect the notification). Without `autoUseWorkspaceTsdk: true`, vtsls falls back to its bundled TypeScript and ignores `tsdk` (there is no UI prompt to confirm the switch in a headless LSP).
The dict is forwarded to vtsls verbatim — Serena does not validate its structure. For the list of supported keys and their expected types, refer to the vtsls [configuration schema](https://github.com/yioneko/vtsls/blob/main/packages/service/configuration.schema.json) and the underlying [VS Code TypeScript settings](https://code.visualstudio.com/docs/languages/typescript). `null` (the default) and `{}` are both treated as "unset": no `initializationOptions` are sent and no `workspace/didChangeConfiguration` notification is pushed. A non-dict value (e.g. a string or list) raises an error at server start.
**Troubleshooting:**
- *vtsls keeps using its bundled TypeScript and ignores `tsdk`* — ensure `vtsls.autoUseWorkspaceTsdk: true` is set alongside `typescript.tsdk`. Without it vtsls does not auto-switch to the workspace TS in a headless LSP.
- *tsserver fails to start after pointing at a custom `tsdk`* — verify the path resolves to a directory containing `tsserver.js` (e.g. `.yarn/sdks/typescript/lib`, not `.yarn/sdks/typescript`). Relative paths are interpreted relative to the project root.
- *Setting appears in `solidlsp` logs but vtsls does not react* — cross-check the key against the vtsls configuration schema linked above. The dict is forwarded as-is, so an unknown or wrong-typed key is silently ignored by vtsls.
- *Need to inspect what Serena is actually forwarding* — the dict is logged at INFO level via the `Forwarding user-provided initializationOptions to vtsls: …` line at language server startup.
#### Vue
+3 -4
View File
@@ -4,7 +4,7 @@ requires = ["hatchling"]
[project]
name = "serena-agent"
version = "1.5.2.dev0"
version = "1.5.4.dev0"
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"
@@ -18,8 +18,6 @@ classifiers = [
]
dependencies = [
"requests==2.33.0",
"pyright==1.1.403",
"fortls==3.2.2",
"overrides==7.7.0",
"python-dotenv==1.2.2",
"mcp==1.27.0",
@@ -52,7 +50,7 @@ dependencies = [
"filelock==3.25.2",
"cryptography==46.0.7",
"regex==2026.2.28",
"pythonnet==3.1.0-rc0 ; sys_platform == 'win32'",
"pythonnet==3.1.0 ; sys_platform == 'win32'",
]
@@ -64,6 +62,7 @@ explicit = true
[project.scripts]
serena = "serena.cli:top_level"
serena-agent = "serena.cli:top_level"
serena-hooks = "serena.hooks:hook_commands"
[project.license]
+46
View File
@@ -0,0 +1,46 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.oraios/serena",
"title": "Serena MCP: the IDE for your agent",
"description": "A powerful toolkit for coding, providing semantic retrieval and editing capabilities.",
"version": "1.5.3",
"repository": {
"source": "github",
"url": "https://github.com/oraios/serena"
},
"packages": [
{
"registryType": "pypi",
"identifier": "serena-agent",
"version": "1.5.3",
"transport": {
"type": "stdio"
},
"runtimeHint": "uvx",
"runtimeArguments": [
{
"description": "Select the python version",
"name": "-p",
"value": "3.13",
"type": "named"
},
{
"type": "positional",
"value": "start-mcp-server"
},
{
"type": "positional",
"value": "--project-from-cwd",
"isRequired": false
},
{
"type": "named",
"name": "--context",
"isRequired": false,
"default": "desktop-app",
"description": "The name of a markdown file representing Serena's execution context, typically related to the executing client. Allowed values are: agent, antigravity, chatgpt, claude-code, codex, context.template, copilot-cli, desktop-app, ide, jb-ai-assistant, jb-copilot-plugin, junie, oaicompat-agent, vscode "
}
]
}
]
}
+1 -1
View File
@@ -1,4 +1,4 @@
__version__ = "1.5.2.dev0"
__version__ = "1.5.4.dev0"
import logging
+10 -3
View File
@@ -663,19 +663,26 @@ class SerenaAgent:
# create the dashboard backend (if enabled), which will register callback.
dashboard_api: SerenaDashboardAPI | None = None
if self.serena_config.web_dashboard:
dashboard_api = SerenaDashboardAPI(get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats)
dashboard_api = SerenaDashboardAPI(
get_memory_log_handler(),
tool_names,
agent=self,
tool_usage_stats=self._tool_usage_stats,
host=self.serena_config.web_dashboard_listen_address,
trusted_hosts=self.serena_config.web_dashboard_trusted_hosts,
)
# propagate the initial config/state to listeners, particularly to the dashboard API.
# Note: The only ongoing task can be the LS initialisation, which does not change the config
# This is executed before starting the dashboard thread to ensure that the dashboard
# has the correct initial state before can requests come in.
# has the correct initial state before requests can come in.
self._on_config_changed()
# start the dashboard backend thread and the dashboard frontend manager (if enabled).
# This should be the last thing to happen in the initialization since the dashboard
# may access various parts of the agent
if dashboard_api:
dashboard_thread, port = dashboard_api.run_in_thread(host=self.serena_config.web_dashboard_listen_address)
dashboard_thread, port = dashboard_api.run_in_thread()
self._dashboard_manager = DashboardManager(
port,
self.serena_config.web_dashboard_listen_address,
+1
View File
@@ -720,6 +720,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
web_dashboard_open_on_launch: bool = True
web_dashboard_interface: str | None = None
web_dashboard_listen_address: str = "127.0.0.1"
web_dashboard_trusted_hosts: list[str] = field(default_factory=lambda: ["127.0.0.1", "localhost"])
jetbrains_plugin_server_address: str = "127.0.0.1"
tool_timeout: float = DEFAULT_TOOL_TIMEOUT
+11 -7
View File
@@ -197,11 +197,16 @@ class SerenaDashboardAPI:
tool_names: list[str],
agent: "SerenaAgent",
tool_usage_stats: ToolUsageStats | None = None,
host: str = "127.0.0.1",
trusted_hosts: list[str] | None = None,
) -> None:
self._memory_log_handler = memory_log_handler
self._tool_names = tool_names
self._agent = agent
self._host = host
self._app = Flask(__name__)
if trusted_hosts:
self._app.config["TRUSTED_HOSTS"] = trusted_hosts
self._tool_usage_stats = tool_usage_stats
self._loaded_news: dict[str, str] = {}
self._news_ready = threading.Event()
@@ -777,7 +782,7 @@ class SerenaDashboardAPI:
raise RuntimeError(f"No free ports found starting from {start_port}")
def run(self, host: str, port: int) -> int:
def run(self, port: int) -> int:
"""
Runs the dashboard on the given host and port and returns the port number.
"""
@@ -785,14 +790,13 @@ class SerenaDashboardAPI:
from flask import cli
cli.show_server_banner = lambda *args, **kwargs: None
self._app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True)
self._app.run(host=self._host, port=port, debug=False, use_reloader=False, threaded=True)
return port
def run_in_thread(self, host: str) -> tuple[threading.Thread, int]:
port = self._find_first_free_port(self.BASE_PORT, host)
log.info("Starting dashboard (listen_address=%s, port=%d)", host, port)
thread = threading.Thread(target=lambda: self.run(host=host, port=port), daemon=True)
def run_in_thread(self) -> tuple[threading.Thread, int]:
port = self._find_first_free_port(self.BASE_PORT, self._host)
log.info("Starting dashboard (listen_address=%s, port=%d)", self._host, port)
thread = threading.Thread(target=lambda: self.run(port=port), daemon=True)
thread.start()
return thread, port
+14
View File
@@ -272,6 +272,8 @@ class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook):
#: file suffixes for source-like files where symbolic tools are usually more
#: appropriate than repeated raw reads. Lowercase and extension-only.
#: Note: ``search_for_pattern`` is always available regardless of extension and
#: is a better alternative to repeated raw reads for any structured file type.
_CODE_FILE_EXTENSIONS: frozenset[str] = frozenset(
(
".al",
@@ -289,14 +291,19 @@ class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook):
".fs",
".fsx",
".go",
".graphql",
".gql",
".groovy",
".h",
".hcl",
".hpp",
".hs",
".html",
".java",
".jl",
".js",
".json",
".jsonc",
".jsx",
".kt",
".kts",
@@ -305,6 +312,7 @@ class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook):
".m",
".matlab",
".php",
".proto",
".ps1",
".py",
".r",
@@ -313,11 +321,17 @@ class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook):
".scala",
".sh",
".sol",
".sql",
".svelte",
".swift",
".tf",
".tfvars",
".toml",
".ts",
".tsx",
".vue",
".yaml",
".yml",
".zig",
)
)
+8 -6
View File
@@ -191,8 +191,8 @@ class Project(ToStringMixin):
def _is_ignored_relative_path(self, relative_path: str | Path, ignore_non_source_files: bool = True) -> bool:
"""
Determine whether an existing path should be ignored based on file type and ignore patterns.
Raises `FileNotFoundError` if the path does not exist.
Determine whether a path should be ignored based on file type and ignore patterns.
Returns False for non-existent paths since they cannot be matched by ignore patterns.
:param relative_path: Relative path to check
:param ignore_non_source_files: whether files that are not source files (according to the file masks
@@ -208,7 +208,8 @@ class Project(ToStringMixin):
abs_path = os.path.join(self.project_root, relative_path)
if not os.path.exists(abs_path):
raise FileNotFoundError(f"File {abs_path} not found, the ignore check cannot be performed")
log.debug(f"Path {abs_path} does not exist, skipping ignore check")
return False
# Check file extension if it's a file
is_file = os.path.isfile(abs_path)
@@ -284,10 +285,11 @@ class Project(ToStringMixin):
def validate_relative_path(self, relative_path: str, require_not_ignored: bool = False) -> None:
"""
Validates that the given relative path to an existing file/dir is safe to read or edit,
meaning it's inside the project directory.
Validates that the given relative path is safe to read or edit,
meaning it's inside the project directory and not ignored.
Passing a path to a non-existing file will lead to a `FileNotFoundError`.
Non-existent paths are allowed (not considered ignored) to support
editing tools that create new files.
:param relative_path: the path to validate, relative to the project root
:param require_not_ignored: if True, the path must not be ignored according to the project's ignore settings
@@ -67,6 +67,14 @@ web_dashboard_interface:
# the address the web dashboard will listen on (bind address).
web_dashboard_listen_address: 127.0.0.1
# trusted hosts used to access the web dashboard.
# By default, only allow access via local addresses for security reasons.
# If you want to allow access from remote machines, add the hostname used to access the dashboard to this list.
# If the list is empty/undefined, all hosts are trusted.
web_dashboard_trusted_hosts:
- 127.0.0.1
- localhost
# address where JetBrains plugin servers are running (only relevant when using the JetBrains language backend)
jetbrains_plugin_server_address: 127.0.0.1
+37
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import logging
import os
import platform
import shutil
import subprocess
from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass, replace
@@ -154,6 +155,42 @@ class RuntimeDependencyCollection:
)
DEFAULT_UVX_PYTHON_VERSION = "3.13"
def build_uvx_launch_command(
package: str,
version: str,
entrypoint: str,
extra_args: Sequence[str] = (),
python_version: str = DEFAULT_UVX_PYTHON_VERSION,
) -> list[str]:
"""Build a command that runs a pinned PyPI package's console script on demand via ``uvx`` / ``uv x``.
Resolution order:
1. Prefer ``uvx`` (env var ``UVX`` or PATH lookup).
2. Fall back to ``uv x`` if only ``uv`` is on PATH.
3. Raise ``RuntimeError`` if neither is available.
:param package: PyPI package name (e.g. ``"pyright"``).
:param version: Pinned package version.
:param entrypoint: Console script provided by the package (e.g. ``"pyright-langserver"``).
:param extra_args: Arguments appended after the entrypoint (e.g. ``("--stdio",)``).
:param python_version: Python interpreter version passed via ``-p`` (uv will fetch it if missing).
"""
base_args = ["-p", python_version, "--from", f"{package}=={version}", entrypoint, *extra_args]
uvx_path = os.environ.get("UVX") or shutil.which("uvx")
if uvx_path is not None:
return [uvx_path, *base_args]
uv_path = shutil.which("uv")
if uv_path is not None:
return [uv_path, "x", *base_args]
raise RuntimeError("Could not find 'uvx' or 'uv' in PATH. Install uv (https://docs.astral.sh/uv/).")
def build_npm_install_command(package_name: str, version: str, registry: str | None = None) -> list[str]:
"""Build a pinned npm install command for a package in a Serena-managed install directory."""
command = ["npm", "install", "--prefix", "./"]
@@ -6,19 +6,26 @@ import logging
import os
import pathlib
import re
import shutil
from overrides import override
from solidlsp import ls_types
from solidlsp.ls import DocumentSymbols, LSPConstants, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls import (
DocumentSymbols,
LanguageServerDependencyProvider,
LanguageServerDependencyProviderUvx,
LSPConstants,
LSPFileBuffer,
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__)
FORTLS_VERSION = "3.2.2"
class FortranLanguageServer(SolidLanguageServer):
"""Fortran Language Server implementation using fortls."""
@@ -170,24 +177,17 @@ class FortranLanguageServer(SolidLanguageServer):
return DocumentSymbols(fixed_root_symbols)
@staticmethod
def _check_fortls_installation() -> str:
"""Check if fortls is available."""
fortls_path = shutil.which("fortls")
if fortls_path is None:
raise RuntimeError("fortls is not installed or not in PATH.\nInstall it with: pip install fortls")
return fortls_path
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
# Check fortls installation
fortls_path = self._check_fortls_installation()
super().__init__(config, repository_root_path, None, "fortran", solidlsp_settings)
# Command to start fortls language server
# fortls uses stdio for LSP communication by default
fortls_cmd = f"{fortls_path}"
super().__init__(
config, repository_root_path, ProcessLaunchInfo(cmd=fortls_cmd, cwd=repository_root_path), "fortran", solidlsp_settings
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return LanguageServerDependencyProviderUvx(
self._custom_settings,
self._ls_resources_dir,
package="fortls",
entrypoint="fortls",
default_version=FORTLS_VERSION,
version_setting_key="fortls_version",
)
@staticmethod
+12 -10
View File
@@ -6,19 +6,20 @@ import logging
import os
import pathlib
import re
import sys
import threading
from typing import cast
from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderUvx, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
PYRIGHT_VERSION = "1.1.403"
class PyrightServer(SolidLanguageServer):
"""
@@ -44,14 +45,15 @@ class PyrightServer(SolidLanguageServer):
self.found_source_files = False
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
def _get_or_install_core_dependency(self) -> str:
return sys.executable
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "-m", "pyright.langserver", "--stdio"]
return LanguageServerDependencyProviderUvx(
self._custom_settings,
self._ls_resources_dir,
package="pyright",
entrypoint="pyright-langserver",
default_version=PYRIGHT_VERSION,
version_setting_key="pyright_version",
extra_args=("--stdio",),
)
@override
def is_ignored_dirname(self, dirname: str) -> bool:
+10 -24
View File
@@ -10,12 +10,11 @@ You can pass the following entries in ``ls_specific_settings["python_ty"]``:
import logging
import os
import pathlib
import shutil
from typing import cast
from typing_extensions import override
from solidlsp.ls import LanguageServerDependencyProvider, SolidLanguageServer
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderUvx, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
@@ -44,28 +43,15 @@ class TyLanguageServer(SolidLanguageServer):
)
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
class DependencyProvider(LanguageServerDependencyProvider):
def create_launch_command(self) -> list[str]:
# respecting an explicit override
ls_path = self._custom_settings.get("ls_path")
if ls_path is not None:
return [ls_path, "server"]
ty_version = self._custom_settings.get("ty_version", TY_VERSION)
# preferring uvx for on-demand execution
uvx_path = os.environ.get("UVX") or shutil.which("uvx")
if uvx_path is not None:
return [uvx_path, "--from", f"ty=={ty_version}", "ty", "server"]
# falling back to uv's uvx-compatible subcommand when only `uv` is available
uv_path = shutil.which("uv")
if uv_path is not None:
return [uv_path, "x", "--from", f"ty=={ty_version}", "ty", "server"]
raise RuntimeError("Could not find 'uvx' or 'uv' in PATH.\nInstall uv or provide ls_specific_settings.python_ty.ls_path.")
return LanguageServerDependencyProviderUvx(
self._custom_settings,
self._ls_resources_dir,
package="ty",
entrypoint="ty",
default_version=TY_VERSION,
version_setting_key="ty_version",
extra_args=("server",),
)
@override
def is_ignored_dirname(self, dirname: str) -> bool:
@@ -34,7 +34,20 @@ DEFAULT_VTSLS_VERSION = "0.2.9"
class VtsLanguageServer(SolidLanguageServer):
"""
Provides TypeScript specific instantiation of the LanguageServer class using vtsls.
Contains various configurations and settings specific to TypeScript via vtsls wrapper.
Supported entries in ``ls_specific_settings["typescript_vts"]``:
- ``vtsls_version``: version of ``@vtsls/language-server`` to install (default: ``"0.2.9"``).
- ``npm_registry``: custom npm registry for the managed install.
- ``initialization_options``: optional dict forwarded verbatim as LSP
``initializationOptions``. Useful for Yarn PnP projects, e.g.::
initialization_options:
typescript:
tsdk: "project/.yarn/sdks/typescript/lib"
vtsls:
autoUseWorkspaceTsdk: true
See https://github.com/yioneko/vtsls/issues/169 for the PnP recipe.
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
@@ -113,13 +126,30 @@ class VtsLanguageServer(SolidLanguageServer):
assert os.path.exists(vts_executable_path), "vtsls executable not found. Please install @vtsls/language-server and try again."
return f"{vts_executable_path} --stdio"
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
@property
def _initialization_options(self) -> dict:
"""
Validated user-provided ``initializationOptions``.
:raises ValueError: if ``ls_specific_settings.typescript_vts.initialization_options``
is set to a value that is not a dict.
"""
opts = self._custom_settings.get("initialization_options")
if opts is None:
return {}
if not isinstance(opts, dict):
raise ValueError(f"ls_specific_settings.typescript_vts.initialization_options must be a dict, got {type(opts).__name__}")
return opts
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the VTS Language Server.
If ``initialization_options`` is set in ``ls_specific_settings["typescript_vts"]``,
it is forwarded verbatim as LSP ``initializationOptions``.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
initialize_params: dict = {
"locale": "en",
"capabilities": {
"textDocument": {
@@ -152,6 +182,11 @@ class VtsLanguageServer(SolidLanguageServer):
}
],
}
if self._initialization_options:
log.info("Forwarding user-provided initializationOptions to vtsls: %s", self._initialization_options)
initialize_params["initializationOptions"] = self._initialization_options
return cast(InitializeParams, initialize_params)
def _start_server(self) -> None:
@@ -179,12 +214,12 @@ class VtsLanguageServer(SolidLanguageServer):
def execute_client_command_handler(params: dict) -> list:
return []
def workspace_configuration_handler(params: dict) -> list[dict] | dict:
# VTS may request workspace configuration
# Return empty configuration for each requested item
if "items" in params:
return [{}] * len(params["items"])
return {}
init_options = self._initialization_options
def workspace_configuration_handler(params: dict) -> list[object]:
# vtsls pulls settings for sections like "typescript", "vtsls", "javascript".
# Return the matching sub-dicts from the user-provided initialization_options.
return [init_options.get(item.get("section", ""), {}) for item in params["items"]]
def do_nothing(params: dict) -> None:
return
@@ -227,6 +262,14 @@ class VtsLanguageServer(SolidLanguageServer):
log.debug(f"completionProvider: {init_response['capabilities']['completionProvider']}")
self.server.notify.initialized({})
# vtsls also reads settings via workspace/didChangeConfiguration (in addition
# to initializationOptions and workspace/configuration pulls). Push the same
# user-provided settings on all three channels for maximum compatibility,
# e.g. so that `typescript.tsdk` is honoured for Yarn PnP projects.
if init_options:
self.server.notify.workspace_did_change_configuration({"settings": init_options})
if self.server_ready.wait(timeout=1.0):
log.info("VTS server is ready")
else:
+44 -1
View File
@@ -8,7 +8,7 @@ import shutil
import threading
from abc import ABC, abstractmethod
from collections import defaultdict
from collections.abc import Callable, Hashable, Iterator
from collections.abc import Callable, Hashable, Iterator, Sequence
from contextlib import contextmanager
from copy import copy
from pathlib import Path, PurePath
@@ -22,6 +22,7 @@ from sensai.util.string import ToStringMixin
from serena.util.file_system import match_path
from serena.util.text_utils import MatchedConsecutiveLines
from solidlsp import ls_types
from solidlsp.language_servers.common import build_uvx_launch_command
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_process import LanguageServerInterface, StdioLanguageServer
@@ -333,6 +334,48 @@ class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProvide
"""
class LanguageServerDependencyProviderUvx(LanguageServerDependencyProvider):
"""
Dependency provider for language servers distributed as a PyPI package, run on demand via ``uvx`` / ``uv x``.
The pinned package version can be overridden by the user via the LS-specific setting given by
``version_setting_key``. Alternatively, the LS-specific setting "ls_path" can be set to the path of an
already-installed language server executable, in which case it is launched directly, bypassing uv entirely.
"""
def __init__(
self,
custom_settings: "SolidLSPSettings.CustomLSSettings",
ls_resources_dir: str,
*,
package: str,
entrypoint: str,
default_version: str,
version_setting_key: str,
extra_args: Sequence[str] = (),
):
"""
:param package: the PyPI package name (e.g. ``"pyright"``)
:param entrypoint: the console script provided by the package (e.g. ``"pyright-langserver"``)
:param default_version: the package version to pin unless overridden
:param version_setting_key: the LS-specific setting key through which the user can override the version
:param extra_args: arguments appended after the entrypoint (e.g. ``("--stdio",)``)
"""
super().__init__(custom_settings, ls_resources_dir)
self._package = package
self._entrypoint = entrypoint
self._default_version = default_version
self._version_setting_key = version_setting_key
self._extra_args = tuple(extra_args)
def create_launch_command(self) -> list[str]:
ls_path = self._custom_settings.get("ls_path")
if ls_path is not None:
return [ls_path, *self._extra_args]
version = self._custom_settings.get(self._version_setting_key, self._default_version)
return build_uvx_launch_command(self._package, version, self._entrypoint, self._extra_args)
class SolidLanguageServer(ABC):
"""
High-level abstraction for language server interaction, which wraps the underlying low-level LSP interface
@@ -102,6 +102,31 @@ class TestGlobalIgnoredPaths:
assert project.is_ignored_path(str(self.project_path / "debug.log"))
assert not project.is_ignored_path(str(self.project_path / "main.py"))
def test_nonexistent_path_not_ignored(self) -> None:
"""Non-existent paths should return False (not ignored), not raise FileNotFoundError."""
project = _create_test_project(self.project_path)
nonexistent = str(self.project_path / "src" / "new_file.py")
assert not os.path.exists(nonexistent)
assert not project.is_ignored_path(nonexistent)
def test_nonexistent_path_with_ignore_patterns(self) -> None:
"""Non-existent paths matching ignore patterns still return False (cannot check)."""
project = _create_test_project(
self.project_path,
global_ignored_paths=["*.log"],
)
nonexistent = str(self.project_path / "nonexistent.log")
assert not os.path.exists(nonexistent)
# Should not raise, should not match the *.log pattern
assert not project.is_ignored_path(nonexistent)
def test_validate_relative_path_allows_nonexistent(self) -> None:
"""validate_relative_path with require_not_ignored should not raise for non-existent paths."""
project = _create_test_project(self.project_path)
nonexistent = "src/new_file.py"
# Should not raise
project.validate_relative_path(nonexistent, require_not_ignored=True)
class TestRegisteredProjectGlobalIgnoredPaths:
"""RegisteredProject.get_project_instance() correctly passes global patterns to Project."""
Generated
+9 -56
View File
@@ -421,14 +421,14 @@ wheels = [
[[package]]
name = "clr-loader"
version = "0.2.10"
version = "0.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/46/7eea92b6aa2d68af78e049cbecec5f757f1aad44ecdecdc16bbad7eead51/clr_loader-0.3.1.tar.gz", hash = "sha256:2e073e9aaf49d1ae2f56ecba27987ad5fb68be4bcd9dd34a5bed8f0e4e128366", size = 86805, upload-time = "2026-04-18T17:49:44.287Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/61/cf819f8e8bb4d4c74661acf2498ba8d4a296714be3478d21eaabf64f5b9b/clr_loader-0.2.10-py3-none-any.whl", hash = "sha256:ebbbf9d511a7fe95fa28a95a4e04cd195b097881dfe66158dc2c281d3536f282", size = 56483, upload-time = "2026-01-03T23:13:05.439Z" },
{ url = "https://files.pythonhosted.org/packages/5e/da/ec1a6e36624000b6df0dd61183c42342ee5814c073315e802cadaad04d2f/clr_loader-0.3.1-py3-none-any.whl", hash = "sha256:cbad189de20d202a7d621956b0fc38049e13c9bf7ca2923441eff725cd121aa1", size = 55730, upload-time = "2026-04-18T17:49:42.99Z" },
]
[[package]]
@@ -671,19 +671,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
]
[[package]]
name = "fortls"
version = "3.2.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "json5" },
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c1/2b/db1e5cd07fc9e74a2e4fb8f65946f8fd79ef72211001af00982a04d977d5/fortls-3.2.2.tar.gz", hash = "sha256:b43b2b8cbd447ae848c63b8f008c2df96fd48c3a967b33f6ed64b3421496883b", size = 7085222, upload-time = "2025-03-16T18:03:11.377Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/7e/41a55a49e764243fd72b1ed202a95d9a9193586d8665bdc94e4df92050e4/fortls-3.2.2-py3-none-any.whl", hash = "sha256:b7ae8378ce9741064b8340d3f1fbbbf7ab697285e174a0208fed98485c4edfe3", size = 278359, upload-time = "2025-03-16T18:03:09.969Z" },
]
[[package]]
name = "gitdb"
version = "4.0.12"
@@ -1135,15 +1122,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" },
]
[[package]]
name = "json5"
version = "0.13.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/77/e8/a3f261a66e4663f22700bc8a17c08cb83e91fbf086726e7a228398968981/json5-0.13.0.tar.gz", hash = "sha256:b1edf8d487721c0bf64d83c28e91280781f6e21f4a797d3261c7c828d4c165bf", size = 52441, upload-time = "2026-01-01T19:42:14.99Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl", hash = "sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc", size = 36163, upload-time = "2026-01-01T19:42:13.962Z" },
]
[[package]]
name = "jsonschema"
version = "4.26.0"
@@ -1616,15 +1594,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
]
[[package]]
name = "nodeenv"
version = "1.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
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 = "overrides"
version = "7.7.0"
@@ -2235,19 +2204,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/86/637cda4983dc0936b73a385f3906256953ac434537b812814cb0b6d231a2/pyobjc_framework_webkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1aaa3bf12c7b68e1a36c0b294d2728e06f2cc220775e6dc4541d5046290e4dc8", size = 50680, upload-time = "2025-11-14T10:07:23.331Z" },
]
[[package]]
name = "pyright"
version = "1.1.403"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodeenv" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526, upload-time = "2025-07-09T07:15:52.882Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504, upload-time = "2025-07-09T07:15:50.958Z" },
]
[[package]]
name = "pystray"
version = "0.19.5"
@@ -2347,14 +2303,15 @@ wheels = [
[[package]]
name = "pythonnet"
version = "3.1.0rc0"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "clr-loader" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a2/59/4295652a9a93a2bd6755ecce2dc32e3d3c704e7b8ef301946ee65c818a59/pythonnet-3.1.0rc0.tar.gz", hash = "sha256:5f0f505e100233d70864b00dc5f5dd760603cfd94ac64da5c8b0e15ed95af939", size = 241025, upload-time = "2026-02-25T10:46:53.632Z" }
sdist = { url = "https://files.pythonhosted.org/packages/05/57/da1992e44663b71365c6e842c8d7fa453d4ec45fb99a68cfee5b7e944d3c/pythonnet-3.1.0.tar.gz", hash = "sha256:7b34c382905d10a371509ffafd64cae0416305c28817738a9cd138336f4e9991", size = 250599, upload-time = "2026-05-23T20:30:21.578Z" }
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" },
{ url = "https://files.pythonhosted.org/packages/ac/4b/52414f442624d2589f5374a48c08d5ae94f24bea67fc13a20a752884e5b7/pythonnet-3.1.0-cp310.cp311.cp312.cp313.cp314-none-any.whl", hash = "sha256:698dd88edc198819ad63b624a6ebe76208c7b46e4fe13626f65e484f0358d6ba", size = 217578, upload-time = "2026-05-23T20:30:19.527Z" },
{ url = "https://files.pythonhosted.org/packages/db/67/031124fdcb937c266a3265118525bbf6dc13b8c79786d6a7290aecb6e7bb/pythonnet-3.1.0-cp310.cp311.cp312.cp313.cp314-none-win32.win_amd64.whl", hash = "sha256:7bdd4de03df3547a48122a3989265c8b31d5be0d19dadffa009eec7df8085e0b", size = 1644898, upload-time = "2026-05-23T20:30:16.213Z" },
]
[[package]]
@@ -2856,7 +2813,7 @@ wheels = [
[[package]]
name = "serena-agent"
version = "1.5.2.dev0"
version = "1.5.4.dev0"
source = { editable = "." }
dependencies = [
{ name = "anthropic" },
@@ -2866,7 +2823,6 @@ dependencies = [
{ name = "dotenv" },
{ name = "filelock" },
{ name = "flask" },
{ name = "fortls" },
{ name = "jinja2" },
{ name = "joblib" },
{ name = "lsprotocol" },
@@ -2876,7 +2832,6 @@ dependencies = [
{ name = "psutil" },
{ name = "pydantic" },
{ name = "pygls" },
{ name = "pyright" },
{ name = "pystray" },
{ name = "python-dotenv" },
{ name = "python-multipart" },
@@ -2937,7 +2892,6 @@ requires-dist = [
{ name = "dotenv", specifier = "==0.9.9" },
{ name = "filelock", specifier = "==3.25.2" },
{ name = "flask", specifier = "==3.1.3" },
{ name = "fortls", specifier = "==3.2.2" },
{ name = "google-genai", marker = "extra == 'google'", specifier = "==1.27.0" },
{ name = "jinja2", specifier = "==3.1.6" },
{ name = "jinja2", marker = "extra == 'dev'", specifier = "==3.1.6" },
@@ -2955,14 +2909,13 @@ requires-dist = [
{ 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" },
{ name = "pystray", specifier = "==0.19.5" },
{ name = "pytest", marker = "extra == 'dev'", specifier = "==8.4.1" },
{ 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.2" },
{ name = "python-multipart", specifier = "==0.0.27" },
{ name = "pythonnet", marker = "sys_platform == 'win32'", specifier = "==3.1.0rc0" },
{ name = "pythonnet", marker = "sys_platform == 'win32'", specifier = "==3.1.0" },
{ name = "pywebview", specifier = "==6.2" },
{ name = "pyyaml", specifier = "==6.0.2" },
{ name = "regex", specifier = "==2026.2.28" },