From 065df5ea310b6dc59095e0d68482e712e38d9592 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 21 May 2026 15:17:45 +0200 Subject: [PATCH 1/2] Introduce events for agent config/status updates decoupling dashboard config computation from the task queue Resolves #1064 --- CHANGELOG.md | 4 ++++ src/serena/agent.py | 45 +++++++++++++++++++++++++++++------ src/serena/dashboard.py | 18 ++++++++++---- src/serena/task_executor.py | 15 +++++++++++- test/serena/test_dashboard.py | 4 ++++ 5 files changed, 74 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5c99726..9c4b584a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* Dashboard: + - 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 + # v1.5.1 (2026-05-18) * General: diff --git a/src/serena/agent.py b/src/serena/agent.py index c8e0d1e5..7b174f87 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -544,6 +544,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 +634,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,13 +660,22 @@ 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( - get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats - ).run_in_thread(host=self.serena_config.web_dashboard_listen_address) + dashboard_api = SerenaDashboardAPI(get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats) + + # 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. + 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) self._dashboard_manager = DashboardManager( port, self.serena_config.web_dashboard_listen_address, @@ -1072,6 +1082,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 diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 66c93827..234245d3 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -207,7 +207,12 @@ class SerenaDashboardAPI: 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 @@ -272,8 +277,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]: @@ -468,7 +475,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 @@ -596,6 +603,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 diff --git a/src/serena/task_executor.py b/src/serena/task_executor.py index 5e624296..1c72f068 100644 --- a/src/serena/task_executor.py +++ b/src/serena/task_executor.py @@ -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 diff --git a/test/serena/test_dashboard.py b/test/serena/test_dashboard.py index 37e68e01..a4355e87 100644 --- a/test/serena/test_dashboard.py +++ b/test/serena/test_dashboard.py @@ -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() From 24c16b6fecc1ff38d341e2267c99b9c7692c405c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 21 May 2026 17:22:38 +0200 Subject: [PATCH 2/2] Fix empty executions queue displaying as 'Loading ...' --- CHANGELOG.md | 1 + src/serena/resources/dashboard/dashboard.css | 5 +++++ src/serena/resources/dashboard/dashboard.js | 1 + 3 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c4b584a..e56d4f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Status of the `main` branch. Changes prior to the next official version change w * Dashboard: - 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.1 (2026-05-18) diff --git a/src/serena/resources/dashboard/dashboard.css b/src/serena/resources/dashboard/dashboard.css index ad658a97..fb2d70da 100644 --- a/src/serena/resources/dashboard/dashboard.css +++ b/src/serena/resources/dashboard/dashboard.css @@ -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; diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index afa71865..43987b58 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -944,6 +944,7 @@ class Dashboard { displayActiveExecutionsQueue(executions) { if (!executions || executions.length === 0) { + this.$activeExecutionQueueDisplay.html('
No executions queued.
'); return; }