Skip to content

Commit 48ce591

Browse files
authored
Merge pull request #1340 from oraios/dev-dj
Improvements for next patch release
2 parents adad335 + de2f756 commit 48ce591

7 files changed

Lines changed: 116 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ Status of the `main` branch. Changes prior to the next official version change w
55
* General:
66
- Support environment variable `SERENA_USAGE_REPORTING` (set to `false` to disable usage reporting)
77
- Extended the list of always ignored directories (by language servers) with common cases.
8+
- Improve exposed toolset: With mode switching no longer being a feature, we now fully apply tool exclusions
9+
defined by modes when in a single-project context (limiting exposed tools to a minimum)
810
- Fix: When scanning for `.gitignore` files, the presence of files that could not be made relative
911
to the project root would cause the scan to fail. #1317
1012

13+
Dashboard:
14+
- Fix handling of read news, saving each read news entry separately #1338
15+
1116
JetBrains:
1217
- Improve handling of `relative_path` parameter
1318
- Improve its documentation to avoid usage errors
@@ -18,7 +23,7 @@ JetBrains:
1823
- Add mSL (mIRC Scripting Language) support (custom pygls-based language server; symbols, references, definitions)
1924
- Fix initialisation issues in Vue language server #1333
2025

21-
# 1.1.1
26+
# v1.1.1 (2026-04-12)
2227

2328
* General:
2429
- Enable cert verification for HTTPS request to oraios-software.de #1320
@@ -30,7 +35,7 @@ JetBrains:
3035
- Fix Dart LSP returning only symbol name as body instead of full method body.
3136

3237

33-
# 1.1.0
38+
# v1.1.0 (2026-04-11)
3439

3540
* General:
3641
- **Major**: Add commands for hooks and documentation of recommended setup. Consider setting up the [recommended hooks](https://oraios.github.io/serena/02-usage/030_clients.html) !
@@ -53,7 +58,7 @@ JetBrains:
5358
Some clients would terminate the MCP server in a way that did not ensure proper termination.
5459
- Fix: Manual server shutdown triggered by GUI tool/dashboard not cleaning everything up.
5560

56-
# 1.0.0
61+
# v1.0.0 (2026-04-03)
5762

5863
* General:
5964
* Add monorepo/multi-language support
@@ -134,7 +139,7 @@ JetBrains:
134139
* **C/C++ alternate LS (ccls)**: Add experimental, opt-in support for ccls as an alternative backend to clangd. Enable via `cpp_ccls` in project configuration. Requires `ccls` installed and ideally a `compile_commands.json` at repo root.
135140
* **Add support for Solidity** via the Nomic Foundation `@nomicfoundation/solidity-language-server` (automatically installed via npm)
136141

137-
# 0.1.4
142+
# v0.1.4 (2025-08-15)
138143

139144
## Summary
140145

@@ -178,7 +183,7 @@ Fixes:
178183
default shell reconfiguration imposed by Claude Code)
179184
* Additional wait for initialization in C# language server before requesting references, allowing cross-file references to be found.
180185

181-
# 0.1.3
186+
# v0.1.3 (2025-07-22)
182187

183188
## Summary
184189

news/20260414.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<div class="news-item">
2+
<h3>Agents Evaluating Serena for Themselves & Maintenance Update</h3>
3+
<p class="date">April 14, 2026</p>
4+
<p>
5+
<b>Evaluation.</b>
6+
We had our actual "end users", i.e. coding agents, evaluate the added value of Serena's tools based on their own empirical insights.
7+
Read what the agents had to say in our <a target="_blank" href="https://oraios.github.io/serena/04-evaluation/000_evaluation-intro.html">evaluation report</a>.<br>
8+
This represents the first systematic evaluation we ever conducted.
9+
</p>
10+
<p>
11+
<b>Maintenance Update.</b> The new v1.1.2 release adds several improvements and fixes.<br>
12+
If you have already switched to the uv tool installation of Serena, upgrade as follows:<br>
13+
uv tool upgrade serena-agent --prerelease=allow
14+
</p>
15+
</div>

scripts/bump_version.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
import os
55
import re
6+
from datetime import datetime
67
from pathlib import Path
78
from typing import Literal
89

@@ -185,7 +186,9 @@ def update_changelog(changelog_text: str, new_version: str) -> str:
185186
unreleased_body = unreleased_section[len(_UNRELEASED_HEADER) :]
186187
intro, unreleased_entries = split_unreleased_body(unreleased_body)
187188

188-
updated_section = _UNRELEASED_HEADER + intro + f"# {new_version}\n"
189+
date_str = datetime.now().strftime("%Y-%m-%d")
190+
updated_section = _UNRELEASED_HEADER + intro + f"# v{new_version} ({date_str})\n"
191+
189192
if unreleased_entries.strip():
190193
updated_section += "\n" + unreleased_entries.lstrip("\n")
191194
else:

src/serena/agent.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -453,31 +453,35 @@ def _create_base_toolset(
453453
tool_inclusion_definitions.append(serena_config)
454454
tool_inclusion_definitions.append(context)
455455

456+
# determine whether we are operating in a single-project context
457+
# (i.e. the project that is activated at startup is the only project that will be worked with throughout the session)
458+
is_single_project = context.single_project and project is not None
459+
456460
# consider modes
457-
# Since modes can be dynamically turned on and off, we don't include their definitions directly,
458-
# For the initially active dynamic modes, we make sure that the tools they enable are included.
459-
for mode in modes.get_default_modes():
460-
tool_inclusion_definitions.append(
461-
NamedToolInclusionDefinition(
462-
name=f"InitialDynamicModeInclusions[{mode.name}]", included_optional_tools=mode.included_optional_tools
463-
)
464-
)
465-
# For the base modes, we also apply the tool exclusions, since they apply throughout the entire session
461+
# * base modes: These cannot be changed, so they are fully applied
466462
for base_mode in modes.get_base_modes():
467-
tool_inclusion_definitions.append(
468-
NamedToolInclusionDefinition(
469-
name=f"BaseMode[{base_mode.name}]",
470-
included_optional_tools=base_mode.included_optional_tools,
471-
excluded_tools=base_mode.excluded_tools,
463+
tool_inclusion_definitions.append(base_mode)
464+
# * default modes: When not in a single-project context, these modes are dynamic (can later be turned off),
465+
# so we consider only their inclusions (but not their exclusions, because these must not be hard)
466+
for mode in modes.get_default_modes():
467+
if is_single_project:
468+
tool_inclusion_definitions.append(mode)
469+
else:
470+
# Since modes can be dynamically turned on and off, we don't include their definitions directly,
471+
# For the initially active dynamic modes, we make sure that the tools they enable are included.
472+
tool_inclusion_definitions.append(
473+
NamedToolInclusionDefinition(
474+
name=f"InitialDynamicModeInclusions[{mode.name}]", included_optional_tools=mode.included_optional_tools
475+
)
472476
)
473-
)
474477

475478
# When in a single-project context, the agent is assumed to work on a single project, and we thus
476479
# want to apply that project's tool exclusions/inclusions from the get-go, limiting the set
477480
# of tools that will be exposed to the client.
478481
# Furthermore, we disable tools that are only relevant for project activation.
479482
# So if the project exists, we apply all the aforementioned exclusions.
480-
if context.single_project and project is not None:
483+
if is_single_project:
484+
assert project is not None
481485
log.info(
482486
"Applying tool inclusion/exclusion definitions for single-project context based on project '%s'",
483487
project.project_name,

src/serena/config/context_mode.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,13 @@ def print_overview(self) -> None:
5353
"""Print an overview of the mode."""
5454
print(f"{self.name}:\n {self.description}")
5555
if self.excluded_tools:
56-
print(" excluded tools:\n " + ", ".join(sorted(self.excluded_tools)))
56+
print(" excluded tools:\n " + ", ".join(sorted(self.excluded_tools)))
57+
if self.included_optional_tools:
58+
print(" included optional tools:\n " + ", ".join(sorted(self.included_optional_tools)))
59+
if self.fixed_tools:
60+
print(" fixed tools:\n " + ", ".join(sorted(self.fixed_tools)))
61+
if self.prompt:
62+
print(" defines initial prompt")
5763

5864
@classmethod
5965
def from_yaml(cls, yaml_path: str | Path) -> Self:

src/serena/config/serena_config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,11 @@ def __init__(self) -> None:
8383
If a name of a mode matches a name of a mode in SERENAS_OWN_MODES_YAML_DIR,
8484
the user mode will override the default mode definition.
8585
"""
86-
self.news_snippet_id_file: str = os.path.join(self.serena_user_home_dir, "last_read_news_snippet_id.txt")
86+
self.news_legacy_last_read_id_file: str = os.path.join(self.serena_user_home_dir, "last_read_news_snippet_id.txt")
87+
"""
88+
file containing the ID of the last read news snippet
89+
"""
90+
self.news_read_items_file: str = os.path.join(self.serena_user_home_dir, "news_read.pkl")
8791
"""
8892
file containing the ID of the last read news snippet
8993
"""

src/serena/dashboard.py

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from PIL import Image
1616
from pydantic import BaseModel
1717
from sensai.util import logging
18+
from sensai.util.pickle import dump_pickle, load_pickle
1819

1920
from serena.analytics import ToolUsageStats
2021
from serena.config.serena_config import SerenaConfig, SerenaPaths
@@ -132,6 +133,54 @@ def from_task_info(cls, task_info: TaskExecutor.TaskInfo) -> Self:
132133
)
133134

134135

136+
class ReadNews:
137+
def __init__(self, read_ids: list[str], legacy_last_read_id: str | None = None):
138+
self._read_ids = set(read_ids)
139+
self._legacy_last_read_id = legacy_last_read_id
140+
141+
@staticmethod
142+
def load() -> "ReadNews":
143+
read_news_path = SerenaPaths().news_read_items_file
144+
legacy_last_read_id_path = SerenaPaths().news_legacy_last_read_id_file
145+
146+
def load_legacy_last_read_id() -> str | None:
147+
if not os.path.exists(legacy_last_read_id_path):
148+
return None
149+
with open(legacy_last_read_id_path, encoding="utf-8") as f:
150+
last_read_news_id = f.read().strip()
151+
if last_read_news_id == "20262103":
152+
last_read_news_id = "20260321" # fix originally misnamed news id
153+
return last_read_news_id
154+
155+
if os.path.exists(read_news_path):
156+
return load_pickle(read_news_path)
157+
else:
158+
instance = ReadNews(read_ids=[], legacy_last_read_id=load_legacy_last_read_id())
159+
instance._save()
160+
try:
161+
os.unlink(legacy_last_read_id_path)
162+
except:
163+
pass
164+
return instance
165+
166+
def _save(self) -> None:
167+
dump_pickle(self, SerenaPaths().news_read_items_file)
168+
169+
def is_read(self, identifier: str) -> bool:
170+
if identifier in self._read_ids:
171+
return True
172+
if self._legacy_last_read_id is not None and identifier <= self._legacy_last_read_id:
173+
return True
174+
return False
175+
176+
def mark_read(self, identifier: str) -> None:
177+
"""
178+
Marks the given news snippet as read, saving the new state to disk
179+
"""
180+
self._read_ids.add(identifier)
181+
self._save()
182+
183+
135184
class SerenaDashboardAPI:
136185
log = logging.getLogger(__qualname__)
137186

@@ -150,6 +199,7 @@ def __init__(
150199
self._loaded_news: dict[str, str] = {}
151200
self._news_ready = threading.Event()
152201
self._setup_routes()
202+
self._read_news = ReadNews.load()
153203
# Fetch remote news in background on startup (non-blocking)
154204
threading.Thread(target=self._fetch_news, daemon=True).start()
155205

@@ -362,25 +412,19 @@ def _fetch_unread_news() -> dict[str, str]:
362412
self._news_ready.wait()
363413
all_news = self._loaded_news
364414

365-
# Filter news items by installation date
366415
serena_config_creation_date = SerenaConfig.get_config_file_creation_date()
367416
if serena_config_creation_date is None:
368417
# should not normally happen, since config file should exist when the dashboard is started
369418
# We assume a fresh installation in this case
370419
log.error("Serena config file not found when starting the dashboard")
371420
return {}
372421
serena_config_creation_date = serena_config_creation_date.strftime("%Y%m%d")
373-
# Only include news items published on or after the installation date
422+
423+
# filter for news after the installation date
374424
post_installation_news = {k: v for k, v in all_news.items() if k >= serena_config_creation_date}
375425

376-
news_snippet_id_file = SerenaPaths().news_snippet_id_file
377-
if not os.path.exists(news_snippet_id_file):
378-
return post_installation_news
379-
with open(news_snippet_id_file, encoding="utf-8") as f:
380-
last_read_news_id = f.read().strip()
381-
if last_read_news_id == "20262103":
382-
last_read_news_id = "20260321" # fix originally misnamed news id
383-
return {k: v for k, v in post_installation_news.items() if k > last_read_news_id}
426+
# read unread news
427+
return {k: v for k, v in post_installation_news.items() if not self._read_news.is_read(k)}
384428

385429
try:
386430
unread_news = _fetch_unread_news()
@@ -393,9 +437,7 @@ def mark_news_snippet_as_read() -> dict[str, str]:
393437
try:
394438
request_data = request.get_json()
395439
news_snippet_id = str(request_data.get("news_snippet_id"))
396-
news_snippet_id_file = SerenaPaths().news_snippet_id_file
397-
with open(news_snippet_id_file, "w", encoding="utf-8") as f:
398-
f.write(news_snippet_id)
440+
self._read_news.mark_read(news_snippet_id)
399441
return {"status": "success", "message": f"Marked news snippet {news_snippet_id} as read"}
400442
except Exception as e:
401443
return {"status": "error", "message": str(e)}

0 commit comments

Comments
 (0)