Merge remote-tracking branch 'upstream/main' into fix/ts-coverage-dirname-ignore

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Therese Stirling committed 2026-06-03 15:49:53 -03:00
commit c34be023d0
18 files changed
+317 -115

No files matched your search

+15 -3
View File
@@ -7,14 +7,26 @@ Status of the `main` branch. Changes prior to the next official version change w
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.
- `SvelteLanguageServer`: Fix diagnostics requests for TypeScript/JavaScript files incorrectly being
processed by the Svelte LS instead of the TypeScript LS.
- Improve quoting of arguments in shell executions
- `typescript` / `typescript_vts`: No longer ignore directories named `coverage`. This was intended to skip
coverage-report output, but matched by bare dirname and so also hid legitimate source directories named
`coverage` (e.g. `src/routes/coverage/`) from symbol tools. Generated report dirs are already covered by
gitignore. Fixes #1523.
* Dashboard:
- Fix: Host validation required a local host regardless of the listen address (regression introduced in v1.5.2),
preventing remote connections
* JetBrains:
- Add configuration option `jetbrains_launch_command`, allowing Serena to spawn IDE instances automatically
upon project activation
* 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..."
- Tray manager: Add NixOS-support for AppIndicator-based trays (e.g., most Wayland-trays) to the package in flake.nix.
- Fix: Wait for the subprocess that opens the browser window, preventing zombie processes #1488
# v1.5.3 (2026-05-26)
+6 -2
View File
@@ -84,7 +84,7 @@ language_backend: JetBrains
Note that the file might not exist yet if you never executed Serena before.
**Per-Server Instance Configuration**.
**Per-Instance Configuration**.
The configuration setting in the global config file can be overridden on a
per-instance basis by providing the arguments `--language-backend JetBrains` when
launching the Serena MCP server.
@@ -121,8 +121,12 @@ You will also notice that your client will use the JetBrains-specific tools like
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.
Note that the project must be appropriately set up in your IDE, i.e. symbol lookups for all relevant programming languages and frameworks should work in the IDE.
Note that the project must be appropriately set up in your IDE, i.e. symbol lookups for all relevant programming languages and frameworks should work in the IDE.
You can optionally make Serena open an IDE instance for your project root folder automatically upon project activation, allowing you to skip this step for a project that was previously set up correctly.
To enable this, configure `jetbrains_launch_command` in [Serena's global configuration file](global-config) appropriately.
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's tools as usual.
+2 -2
View File
@@ -25,7 +25,7 @@ The dashboard is a web application, which is opened in one of three ways:
* via a tray manager that aggregates multiple instances (on supported platforms)
* via your default web browser (supported on all platforms)
Configure your preferred interface via the setting `web_dashoard_interface` in [Serena's global configuration file](global-config) (see below).
Configure your preferred interface via the setting `web_dashboard_interface` in [Serena's global configuration file](global-config) (see below).
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.
@@ -82,4 +82,4 @@ Via its menu, it furthermore allows you to
The tool is mainly supported on Windows, but it may work on some Linux systems as well (depending on your desktop environment).
To enable the tool, set `gui_log_window` to `true` in Serena's [global configuration file](global-config).
To enable the tool, set `gui_log_window` to `true` in Serena's [global configuration file](global-config).
+6 -22
View File
@@ -3,30 +3,14 @@
## Prompting Strategies
We found that it is often a good idea to spend some time conceptualizing and planning a task
before actually implementing it, especially for non-trivial task. This helps both in achieving
better results and in increasing the feeling of control and staying in the loop. You can
make a detailed plan in one session, where Serena may read a lot of your code to build up the context,
and then continue with the implementation in another (potentially after creating suitable memories).
## Running Out of Context
For long and complicated tasks, or tasks where Serena has read a lot of content, you
may come close to the limits of context tokens. In that case, it is often a good idea to continue
in a new conversation. Serena has a dedicated tool to create a summary of the current state
of the progress and all relevant info for continuing it. You can request to create this summary and
write it to a memory. Then, in a new conversation, you can just ask Serena to read the memory and
continue with the task. In our experience, this worked really well. On the up-side, since in a
single session there is no summarization involved, Serena does not usually get lost (unlike some
other agents that summarize under the hood), and it is also instructed to occasionally check whether
it's on the right track.
Serena instructs the LLM to be economical in general, so the problem of running out of context
should not occur too often, unless the task is very large or complicated.
before actually implementing it, especially for non-trivial tasks.
For very complex tasks, you can make a detailed plan in one session,
where Serena may read a lot of your code to build up the context,
and then continue with the implementation in another,
having persisted the plan in a memory or dedicated file.
## Serena and Git Worktrees
[git-worktree](https://git-scm.com/docs/git-worktree) can be an excellent way to parallelize your work. More on this in [Anthropic: Run parallel Claude Code sessions with Git worktrees](https://docs.claude.com/en/docs/claude-code/common-workflows#run-parallel-claude-code-sessions-with-git-worktrees).
When it comes to serena AND git-worktree AND larger projects (that take longer to index),
the recommended way is to COPY your `$ORIG_PROJECT/.serena/cache` to `$GIT_WORKTREE/.serena/cache`.
Perform [pre-indexing of your project](indexing) to avoid having to re-index per each worktree you create.
Be sure to add the `.serena` folder to version control, such that your project-specific settings and memories are available across worktrees.
+114 -34
View File
@@ -50,27 +50,79 @@
sourcePreference = "wheel"; # or sourcePreference = "sdist";
};
pyprojectOverrides = final: prev: {
# Add setuptools for packages that need it during build
ruamel-yaml-clib = prev.ruamel-yaml-clib.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or []) ++ [
final.setuptools
];
});
proxy-tools = prev.proxy-tools.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or []) ++ [
final.setuptools
];
});
pywebview = prev.pywebview.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or []) ++ [
final.setuptools
final.setuptools-scm
];
});
};
python = pkgs.python311;
pyprojectHacks = pkgs.callPackage pyproject-nix.build.hacks {};
pyprojectOverrides = final: prev:
{
# Add setuptools for packages that need it during build
ruamel-yaml-clib = prev.ruamel-yaml-clib.overrideAttrs (old: {
nativeBuildInputs =
(old.nativeBuildInputs or [])
++ [
final.setuptools
];
});
proxy-tools = prev.proxy-tools.overrideAttrs (old: {
nativeBuildInputs =
(old.nativeBuildInputs or [])
++ [
final.setuptools
];
});
pywebview = prev.pywebview.overrideAttrs (old: {
nativeBuildInputs =
(old.nativeBuildInputs or [])
++ [
final.setuptools
final.setuptools-scm
];
});
}
// lib.optionalAttrs pkgs.stdenv.isLinux {
# pyproject-nix's virtualenv resolver follows pyproject-style
# passthru.dependencies, while these GI bindings are already packaged
# by nixpkgs' Python infrastructure. nixpkgsPrebuilt is the upstream
# adapter for that case:
# https://pyproject-nix.github.io/pyproject.nix/builders/hacks.html#using-prebuilt-packages-from-nixpkgs
pycairo = pyprojectHacks.nixpkgsPrebuilt {
from = python.pkgs.pycairo;
prev = {
passthru = {
dependencies = {};
optional-dependencies = {};
dependency-groups = {};
};
};
};
pygobject3 = pyprojectHacks.nixpkgsPrebuilt {
from = python.pkgs.pygobject3;
prev = {
passthru = {
dependencies = {
pycairo = [];
};
optional-dependencies = {};
dependency-groups = {};
};
};
};
# pystray has an optional AppIndicator backend on Linux. Its uv lock
# metadata does not encode the GI dependency, so add the runtime edge
# here while keeping pystray itself lockfile-driven.
pystray = prev.pystray.overrideAttrs (old: {
passthru =
(old.passthru or {})
// {
dependencies =
(old.passthru.dependencies or {})
// {
pygobject3 = [];
};
};
});
};
pythonSet =
(pkgs.callPackage pyproject-nix.build.packages {
@@ -88,20 +140,48 @@
packages = {
serena-env = pythonSet.mkVirtualEnv "serena-env" workspace.deps.default;
serena = pkgs.runCommand "serena" {
meta = {
description = "A powerful coding agent toolkit providing semantic retrieval and editing capabilities (MCP server & Agno integration)";
homepage = "https://oraios.github.io/serena";
changelog = "https://github.com/oraios/serena/blob/main/CHANGELOG.md";
mainProgram = "serena";
license = pkgs.lib.licenses.mit;
platforms = lib.platforms.all;
};
} ''
mkdir -p $out/bin
ln -s ${packages.serena-env}/bin/serena $out/bin/serena
ln -s ${packages.serena-env}/bin/serena-hooks $out/bin/serena-hooks
'';
serena = pkgs.stdenv.mkDerivation {
name = "serena";
dontUnpack = true;
dontWrapGApps = true;
nativeBuildInputs =
[pkgs.makeWrapper]
++ lib.optionals pkgs.stdenv.isLinux [
pkgs.wrapGAppsHook3
pkgs.gobject-introspection
];
buildInputs = lib.optionals pkgs.stdenv.isLinux [
pkgs.gtk3
pkgs.libayatana-appindicator
];
installPhase = ''
runHook preInstall
mkdir -p $out/bin
${
lib.optionalString (!pkgs.stdenv.isLinux) ''
ln -s ${packages.serena-env}/bin/serena $out/bin/serena
''
}
ln -s ${packages.serena-env}/bin/serena-hooks $out/bin/serena-hooks
runHook postInstall
'';
# Run the GApps fixup phase so the wrapper gets GI_TYPELIB_PATH for
# Gtk and AyatanaAppIndicator3. This is the same pattern nixpkgs uses
# for pystray consumers such as plex-mpv-shim and jellyfin-mpv-shim.
preFixup = lib.optionalString pkgs.stdenv.isLinux ''
makeWrapper ${packages.serena-env}/bin/serena $out/bin/serena "''${gappsWrapperArgs[@]}"
'';
meta = {
description = "A powerful coding agent toolkit providing semantic retrieval and editing capabilities (MCP server & Agno integration)";
homepage = "https://oraios.github.io/serena";
changelog = "https://github.com/oraios/serena/blob/main/CHANGELOG.md";
mainProgram = "serena";
license = pkgs.lib.licenses.mit;
platforms = lib.platforms.all;
};
};
default = packages.serena;
};
+62 -6
View File
@@ -7,6 +7,7 @@ import multiprocessing
import os
import platform
import signal
import subprocess
import threading
from collections import defaultdict
from collections.abc import Callable, Iterator, Sequence
@@ -40,6 +41,7 @@ from serena.config.serena_config import (
ToolInclusionDefinition,
)
from serena.dashboard import SerenaDashboardAPI, SerenaDashboardTrayManager, SerenaDashboardViewer, open_url_in_browser
from serena.jetbrains import jetbrains_plugin_client
from serena.ls_manager import LanguageServerManager
from serena.memories.memory_manager import MemoryManager
from serena.project import Project
@@ -60,6 +62,7 @@ from serena.util.gui import system_has_usable_display
from serena.util.inspection import iter_subclasses
from serena.util.logging import MemoryLogHandler
from solidlsp.ls_config import Language
from solidlsp.util import subprocess_util
if TYPE_CHECKING:
from serena.gui_log_viewer import GuiLogViewer
@@ -544,6 +547,7 @@ class SerenaAgent:
self._project_prompt_status = ProjectPromptProvisionStatus()
self._session_mode_selection_definition = modes
self.version = serena_version()
self._config_changed_callbacks: list[Callable[[], None]] = []
# obtain serena configuration using the decoupled factory function
self.serena_config = serena_config or SerenaConfig.from_config_file()
@@ -633,7 +637,7 @@ class SerenaAgent:
# create executor for starting the language server and running tools in another thread
# This executor is used to achieve linear task execution
self._task_executor = TaskExecutor("SerenaAgentTaskExecutor")
self._task_executor = TaskExecutor("SerenaAgentTaskExecutor", self._task_completion_callback)
# Initialize the prompt factory
self.prompt_factory = SerenaPromptFactory()
@@ -659,17 +663,29 @@ class SerenaAgent:
self._active_tools: AvailableTools
self._update_active_tools()
# start the dashboard (web frontend), registering its log handler
# should be the last thing to happen in the initialization since the dashboard
# may access various parts of the agent
# create the dashboard backend (if enabled), which will register callback.
dashboard_api: SerenaDashboardAPI | None = None
if self.serena_config.web_dashboard:
self._dashboard_thread, port = SerenaDashboardAPI(
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,
).run_in_thread()
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 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()
self._dashboard_manager = DashboardManager(
port,
self.serena_config.web_dashboard_listen_address,
@@ -1076,6 +1092,27 @@ class SerenaAgent:
"""
return self._task_executor.execute_task(task, name=name, logged=logged, timeout=timeout)
def _task_completion_callback(self) -> None:
"""
Called after every task completion. Tasks potentially change the agent's state/configuration.
"""
self._on_config_changed()
def _on_config_changed(self) -> None:
for callback in self._config_changed_callbacks:
try:
callback()
except Exception as e:
log.error(f"Error in config changed callback {callback}: {e}", exc_info=e)
def register_config_changed_callback(self, callback: Callable[[], None]) -> None:
"""
Registers a callback to be called when the agent configuration has (potentially) changed.
:param callback: the callback function to register
"""
self._config_changed_callbacks.append(callback)
def is_using_language_server(self) -> bool:
"""
:return: whether this agent uses language server-based code analysis
@@ -1131,6 +1168,25 @@ class SerenaAgent:
if self.get_language_backend().is_lsp():
self.issue_task(init_language_server_manager)
def init_jetbrains_ide() -> None:
assert self.serena_config.jetbrains_launch_command is not None
try:
client = jetbrains_plugin_client.JetBrainsPluginClient.from_project(project, log_warning=False)
log.info("Found Serena JetBrains Plugin server: %s", client)
except jetbrains_plugin_client.ServerNotFoundError:
log.info("Serena JetBrains Plugin server not found for project %s", project.project_name)
if self.serena_config.jetbrains_launch_command:
cmd = subprocess_util.convert_shell_cmd([self.serena_config.jetbrains_launch_command, project.project_root])
log.info("Launching IDE with command: %s", cmd)
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout, stderr = p.communicate()
if p.returncode != 0:
log.error(f"Failed to launch JetBrains IDE: {stderr.decode('utf-8')}")
# for JetBrains mode, search for plugin server and spawn IDE if not found and enabled
if self.get_language_backend().is_jetbrains():
self.issue_task(init_jetbrains_ide)
if self._project_activation_callback is not None:
self._project_activation_callback()
+5
View File
@@ -720,7 +720,12 @@ 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"
jetbrains_launch_command: str | None = None
"""
JetBrains IDE launch command, which can be used to auto-start an IDE instance on demand.
"""
tool_timeout: float = DEFAULT_TOOL_TIMEOUT
token_count_estimator: str = RegisteredTokenCountEstimator.CHAR_COUNT.name
+19 -8
View File
@@ -198,21 +198,26 @@ class SerenaDashboardAPI:
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__)
local_hosts = ["127.0.0.1", "localhost"]
if self._host in local_hosts:
self._app.config["TRUSTED_HOSTS"] = local_hosts
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()
self._setup_routes()
self._read_news = ReadNews.load()
# Fetch remote news in background on startup (non-blocking)
# register callback for config changes
self._current_config_overview: dict[str, Any] | None = None
self._agent.register_config_changed_callback(self._on_agent_config_changed)
# fetch remote news in background on startup (non-blocking)
threading.Thread(target=self._fetch_news, daemon=True).start()
@property
@@ -277,8 +282,10 @@ class SerenaDashboardAPI:
@self._app.route("/get_config_overview", methods=["GET"])
def get_config_overview() -> dict[str, Any]:
result = self._agent.execute_task(self._get_config_overview, logged=False)
return result.model_dump()
result = self._current_config_overview
if result is None:
raise ValueError("Config overview not yet available")
return result
@self._app.route("/shutdown", methods=["PUT"])
def shutdown() -> dict[str, str]:
@@ -473,7 +480,7 @@ class SerenaDashboardAPI:
if self._tool_usage_stats is not None:
self._tool_usage_stats.clear()
def _get_config_overview(self) -> ResponseConfigOverview:
def _compute_config_overview(self) -> ResponseConfigOverview:
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
from serena.tools.tools_base import Tool
@@ -601,6 +608,9 @@ class SerenaDashboardAPI:
serena_version=self._agent.version,
)
def _on_agent_config_changed(self) -> None:
self._current_config_overview = self._compute_config_overview().model_dump()
def _get_available_languages(self) -> ResponseAvailableLanguages:
from solidlsp.ls_config import Language
@@ -803,13 +813,14 @@ def open_url_in_browser(url: str, use_subprocess: bool = False) -> None:
if use_subprocess:
# Use a subprocess to avoid any output from webbrowser.open being written to stdout
try:
subprocess.Popen(
p = subprocess.Popen(
[sys.executable, "-c", f"import webbrowser; webbrowser.open({url!r})"],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=False,
)
threading.Thread(target=p.wait, daemon=True).start()
except Exception as e:
# Subprocess creation can fail in rare cases (e.g. on some Linux systems; possibly subprocess/glibc bug)
# See #1363
@@ -105,7 +105,12 @@ class JetBrainsPluginClientManager:
futures.append(future)
return futures
def find_client(self, project_root: Path) -> "JetBrainsPluginClient":
def find_client(self, project_root: Path, log_warning: bool = True) -> "JetBrainsPluginClient":
"""
:param project_root: the project root path to find a plugin instance for
:param log_warning: whether to log a warning if no matching plugin instance is found
:return: the instance
"""
plugin_paths_found = []
for future in self._submit_scan():
client = future.result()
@@ -114,12 +119,13 @@ class JetBrainsPluginClientManager:
elif client.project_root is not None:
plugin_paths_found.append(client.project_root)
log.warning(
"Searched for Serena JetBrains plugin service for project at %s but found no matching service. "
"Found plugin instances for the following project paths: %s",
project_root,
plugin_paths_found,
)
if log_warning:
log.warning(
"Searched for Serena JetBrains plugin service for project at %s but found no matching service. "
"Found plugin instances for the following project paths: %s",
project_root,
plugin_paths_found,
)
raise ServerNotFoundError(
f"Found no Serena service in a JetBrains IDE instance for the project at {project_root}. "
"STOP. Do not attempt any other tools or workarounds. Ask the user to open this folder as a project in a JetBrains IDE "
@@ -219,7 +225,7 @@ class JetBrainsPluginClient(ToStringMixin):
return ["_port", "project_root", "_plugin_version"]
@classmethod
def from_project(cls, project: Project) -> Self:
def from_project(cls, project: Project, log_warning: bool = True) -> Self:
resolved_path = Path(project.project_root).resolve()
if cls._last_port is not None:
@@ -227,7 +233,7 @@ class JetBrainsPluginClient(ToStringMixin):
if client.matches(resolved_path):
return client
client = JetBrainsPluginClientManager().find_client(resolved_path)
client = JetBrainsPluginClientManager().find_client(resolved_path, log_warning=log_warning)
cls._last_port = client._port
return client
@@ -1012,6 +1012,11 @@ code, pre, kbd, samp,
font-style: italic;
}
.loaded {
text-align: center;
color: var(--text-muted);
}
.error-message {
color: var(--log-error);
text-align: center;
@@ -944,6 +944,7 @@ class Dashboard {
displayActiveExecutionsQueue(executions) {
if (!executions || executions.length === 0) {
this.$activeExecutionQueueDisplay.html('<div class="loaded">No executions queued.</div>');
return;
}
@@ -61,15 +61,32 @@ web_dashboard_open_on_launch: True
# opening the dashboard in browser tabs when selected from the tray menu.
# This is EXPERIMENTAL. It is tested on Windows only. We will establish macOS support, but it is yet untested.
# On Linux, this cannot be universally supported, but it may work in some desktop environments.
# On NixOS, when using the package from flake.nix, both modern AppIndicator trays as well as
# older Xorg-/XEmbed-based trays should be supported.
# See https://oraios.github.io/serena/02-usage/060_dashboard.html
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
# command used to launch a JetBrains IDE on demand (only relevant when using the JetBrains language backend)
# If this is non-empty and, at project activation, the Serena JetBrains Plugin server instance corresponding
# to the project is not found, use this command to spawn a new instance.
# Specifically, if this is <cmd>, then `<cmd> <projectDir>` is launched.
# Provide the full path to the executable/script (e.g., "/usr/bin/idea" or "C:/Users/bob/AppData/Local/JetBrains/Toolbox/scripts/idea.cmd").
jetbrains_launch_command:
# the minimum log level for the GUI log window and the dashboard (10 = debug, 20 = info, 30 = warning, 40 = error)
log_level: 20
+14 -1
View File
@@ -16,7 +16,12 @@ T = TypeVar("T")
class TaskExecutor:
def __init__(self, name: str):
def __init__(self, name: str, task_completion_callback: Callable[[], None] | None = None):
"""
:param name: the name of the task executor, used for logging purposes
:param task_completion_callback: an optional callback function that will be called after each task completion
(regardless of success, failure, or cancellation)
"""
self._task_executor_lock = threading.Lock()
self._task_executor_queue: list[TaskExecutor.Task] = []
self._task_executor_thread = Thread(target=self._process_task_queue, name=name, daemon=True)
@@ -24,6 +29,7 @@ class TaskExecutor:
self._task_executor_task_index = 1
self._task_executor_current_task: TaskExecutor.Task | None = None
self._task_executor_last_executed_task_info: TaskExecutor.TaskInfo | None = None
self._task_completion_callback = task_completion_callback
class Task(ToStringMixin, Generic[T]):
def __init__(self, function: Callable[[], T], name: str, logged: bool = True, timeout: float | None = None):
@@ -130,6 +136,13 @@ class TaskExecutor:
if task.logged:
self._task_executor_last_executed_task_info = self.TaskInfo.from_task(task, is_running=False)
# call the task completion callback if provided
if self._task_completion_callback is not None:
try:
self._task_completion_callback()
except Exception as e:
log.error(f"Error in task completion callback after executing {task.name}: {e}", exc_info=e)
@dataclass
class TaskInfo:
name: str
+3 -8
View File
@@ -10,7 +10,7 @@ from dataclasses import dataclass, replace
from typing import Any, cast
from solidlsp.ls_utils import FileUtils, PlatformUtils
from solidlsp.util.subprocess_util import subprocess_kwargs
from solidlsp.util import subprocess_util
log = logging.getLogger(__name__)
@@ -101,18 +101,13 @@ class RuntimeDependencyCollection:
@staticmethod
def _run_command(command: str | list[str], cwd: str) -> None:
kwargs = subprocess_kwargs()
kwargs = subprocess_util.subprocess_kwargs()
if not PlatformUtils.get_platform_id().is_windows():
import pwd
kwargs["user"] = pwd.getpwuid(os.getuid()).pw_name # type: ignore
is_windows = platform.system() == "Windows"
if not isinstance(command, str) and not is_windows:
# Since we are using the shell, we need to convert the command list to a single string
# on Linux/macOS
command = " ".join(command)
command = subprocess_util.convert_shell_cmd(command)
log.info("Running command %s in '%s'", f"'{command}'" if isinstance(command, str) else command, cwd)
completed_process = subprocess.run(
@@ -647,6 +647,19 @@ class SvelteLanguageServer(SolidLanguageServer):
return self._ts_server.request_definition(relative_file_path, line, column)
return super().request_definition(relative_file_path, line, column)
@override
def request_text_document_diagnostics(
self,
relative_file_path: str,
start_line: int = 0,
end_line: int = -1,
min_severity: int = 4,
) -> list[ls_types.Diagnostic]:
if _is_ts_file(relative_file_path) and self._ts_server is not None:
with self._ts_server.open_file(relative_file_path):
return self._ts_server.request_text_document_diagnostics(relative_file_path, start_line, end_line, min_severity)
return super().request_text_document_diagnostics(relative_file_path, start_line, end_line, min_severity)
@override
def _get_language_id_for_file(self, relative_file_path: str) -> str:
ext = os.path.splitext(relative_file_path)[1].lower()
+6 -10
View File
@@ -2,7 +2,6 @@ import asyncio
import json
import logging
import os
import platform
import socket
import subprocess
import threading
@@ -33,7 +32,7 @@ from solidlsp.lsp_protocol_handler.server import (
make_request,
make_response,
)
from solidlsp.util.subprocess_util import quote_arg, subprocess_kwargs, terminate_process_tree_with_kill_fallback
from solidlsp.util import subprocess_util
log = logging.getLogger(__name__)
@@ -459,14 +458,9 @@ class StdioLanguageServer(LanguageServerInterface):
child_proc_env = os.environ.copy()
child_proc_env.update(self.process_launch_info.env)
cmd = self.process_launch_info.cmd
is_windows = platform.system() == "Windows"
if not isinstance(cmd, str) and not is_windows:
# Since we are using the shell, we need to convert the command list to a single string
# on Linux/macOS
cmd = " ".join(map(quote_arg, cmd))
cmd = subprocess_util.convert_shell_cmd(self.process_launch_info.cmd)
log.info("Starting language server process via command: %s", self.process_launch_info.cmd)
kwargs = subprocess_kwargs()
kwargs = subprocess_util.subprocess_kwargs()
kwargs["start_new_session"] = self.start_independent_lsp_process
self.process = subprocess.Popen(
cmd,
@@ -513,7 +507,9 @@ class StdioLanguageServer(LanguageServerInterface):
log.debug(f"Exception during graceful shutdown: {e}")
# Ignore errors here, we are proceeding to terminate anyway.
# terminate the process
terminate_process_tree_with_kill_fallback(self.process, terminate_timeout=timeout, process_name=f"LS[{self.language.value}]")
subprocess_util.terminate_process_tree_with_kill_fallback(
self.process, terminate_timeout=timeout, process_name=f"LS[{self.language.value}]"
)
finally:
self.process = None
+10 -10
View File
@@ -19,19 +19,19 @@ def subprocess_kwargs() -> dict:
return kwargs
def quote_arg(arg: str) -> str:
def convert_shell_cmd(cmd: str | list[str]) -> str | list[str]:
"""
Quotes a shell argument to prevent interpretation of metacharacters.
Converts a command (specified as a list or string) to a format supported by subprocess calls with shell=True on the current platform.
List format must be converted to string format on POSIX systems, quoting arguments appropriately,
while it can be used as-is on Windows.
Uses :func:`shlex.quote` on POSIX systems for proper escaping of all
shell-special characters. On Windows, wraps arguments containing spaces
in double quotes (Windows shell does not interpret single-quoted strings).
:param cmd: the command to convert, specified as a list of arguments
:return: a suitable representation of the command for subprocess calls on the current platform
"""
if platform.system() == "Windows":
if " " not in arg:
return arg
return f'"{arg}"'
return shlex.quote(arg)
if isinstance(cmd, list) and platform.system() != "Windows":
return " ".join(shlex.quote(arg) for arg in cmd)
else:
return cmd
def _signal_process_tree(process: subprocess.Popen[bytes], terminate: bool = True) -> None:
+4
View File
@@ -1,3 +1,4 @@
from collections.abc import Callable
from types import SimpleNamespace
from serena.dashboard import SerenaDashboardAPI
@@ -16,6 +17,9 @@ class _DummyAgent:
def __init__(self, project: SimpleNamespace | None) -> None:
self._project = project
def register_config_changed_callback(self, callback: Callable[[], None]) -> None:
pass
def execute_task(self, func, *, logged: bool | None = None, name: str | None = None):
del logged, name
return func()