Merge branch 'main' into new_evals

This commit is contained in:
Michael Panchenko authored and GitHub committed 2026-04-20 09:32:50 +02:00
commit e0cf89ff6a
13 files changed
+583 -254

No files matched your search

+103
View File
@@ -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}}"
+7
View File
@@ -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:
@@ -16,6 +17,12 @@ Status of the `main` branch. Changes prior to the next official version change w
* Evaluations:
- Added new evaluations for Junie Plugin with Opus 4.6 and GLM 5.1 in Claude Code.
* 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)
* General:
+9 -26
View File
@@ -28,6 +28,12 @@ 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.
## Quick Demo
https://github.com/user-attachments/assets/8d11646e-b80e-4723-b9d7-32d6101b5f58
:tv: Longer video: [Introduction to Serena in 5 Minutes (YouTube)](https://www.youtube.com/watch?v=5QN7gN1KYLA)
## What Our "End Users" Say
While it is humans who download and set up Serena, our end users are essentially AI agents.
@@ -72,16 +78,12 @@ Most modern AI chat clients directly support MCP, including
<img src="resources/serena-block-diagram.svg">
:tv: See also: [Introduction to Serena in 5 Minutes (YouTube)](https://www.youtube.com/watch?v=5QN7gN1KYLA)
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
@@ -97,7 +99,7 @@ There are two alternative technologies powering these capabilities:
* **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
* **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.
@@ -203,25 +205,6 @@ To achieve this, Serena offers multiple levels of (composable) configuration:
</details>
## Serena in Action
<details>
<summary>Demonstrations</summary>
#### 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.
https://github.com/user-attachments/assets/ab78ebe0-f77d-43cc-879a-cc399efefd87
#### Demonstration 2: Serena in Claude Desktop
A demonstration of Serena implementing a small feature for itself (a better log GUI) with Claude Desktop.
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
</details>
## Quick Start
**Prerequisites**. Serena is managed by *uv*, and [installing uv](https://docs.astral.sh/uv/getting-started/installation/) is the only required prerequisite.
+14 -13
View File
@@ -2,7 +2,7 @@
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.
This page explains how to install the plugin and how to configure Serena appropriately.
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.
@@ -18,20 +18,20 @@ LLM-based client as described in [client setup](030_clients.md) in addition to f
We recommend the JetBrains plugin as the preferred way of using Serena,
especially for users of JetBrains IDEs.
```{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))
4. Start coding via your MCP client as usual
```{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 CLI) or internal (like Copilot or 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!
```
**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.
@@ -82,7 +82,7 @@ language_backend: JetBrains
Note that the file might not exist yet if you never executed Serena before.
**Per-Instance Configuration**.
**Per-Server 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.
@@ -119,9 +119,10 @@ 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
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
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.
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.
Note that the project folder that is open in your IDE and the Serena project root folder must match.
+1 -1
View File
@@ -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",
+25 -198
View File
@@ -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()
+233
View File
@@ -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()
@@ -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
@@ -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
+9 -3
View File
@@ -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
@@ -1691,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.
@@ -1700,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:
@@ -2456,8 +2456,8 @@ class DocumentSymbol(TypedDict):
selectionRange: "Range"
""" The range that should be selected and revealed when this symbol is being picked, e.g the name of a function.
Must be contained by the `range`. """
# TODO: I think this type is missing the 'children' field - DJ
children: NotRequired[list["DocumentSymbol"]]
""" Children of this symbol, e.g. properties of a class. """
class DocumentSymbolRegistrationOptions(TypedDict):
+71
View File
@@ -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)
Generated
+4 -4
View File
@@ -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" },