diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index fdff3019..4e303ee9 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -61,11 +61,25 @@ class ResponseToolStats(BaseModel): stats: dict[str, dict[str, int]] +class ResponseFacadeMethod(BaseModel): + name: str + is_enabled: bool + + +class ResponseFacade(BaseModel): + name: str + is_enabled: bool + methods: list[ResponseFacadeMethod] + + class ResponseConfigOverview(BaseModel): active_project: dict[str, str | None] context: dict[str, str] modes: list[dict[str, str]] active_tools: list[str] + agent_interface: str + language_backend: str + facades: list[ResponseFacade] | None tool_stats_summary: dict[str, dict[str, int]] registered_projects: list[dict[str, str | bool]] available_tools: list[dict[str, str | bool]] @@ -608,6 +622,19 @@ class SerenaDashboardAPI: if self._agent.is_tool_function_available(ReadMemoryTool) and project is not None: available_memories = project.memory_manager.list_memories().get_full_list() + # Get the availability of the REPL's facades and their methods (REPL interface only) + facades = None + if self._agent.get_agent_interface().is_repl(): + availability_info = self._agent.get_repl().entrypoint.get_facade_availability_info() + facades = [ + ResponseFacade( + name=facade_info.name, + is_enabled=facade_info.is_enabled, + methods=[ResponseFacadeMethod(name=m.name, is_enabled=m.is_enabled) for m in facade_info.methods], + ) + for facade_info in availability_info.facades + ] + # Get list of languages for the active project ls_ids = [] if project is not None: @@ -623,6 +650,9 @@ class SerenaDashboardAPI: context=context_info, modes=modes_info, active_tools=active_tools, + agent_interface=self._agent.get_agent_interface().value, + language_backend=self._agent.get_language_backend().value, + facades=facades, tool_stats_summary=tool_stats_summary, registered_projects=registered_projects, available_tools=available_tools, diff --git a/src/serena/repl/facade.py b/src/serena/repl/facade.py index 45a2538c..cb66a012 100644 --- a/src/serena/repl/facade.py +++ b/src/serena/repl/facade.py @@ -643,6 +643,12 @@ class Facade: """ return [m for m in self._methods.values() if m.enabled] + def get_methods(self) -> list[FacadeMethod]: + """ + :return: the list of all methods, regardless of whether they are enabled (e.g. for changing their enabled state) + """ + return list(self._methods.values()) + def get_types(self) -> list[ReferencedType]: """ :return: the types referenced by the facade's methods diff --git a/src/serena/repl/repl.py b/src/serena/repl/repl.py index 59cdfaf1..1e697738 100644 --- a/src/serena/repl/repl.py +++ b/src/serena/repl/repl.py @@ -8,6 +8,7 @@ import ast import logging import re import traceback +from dataclasses import dataclass from typing import TYPE_CHECKING, Any from ..session import SerenaSession @@ -21,6 +22,35 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) +class FacadeAvailabilityInfo: + """ + Represents information on the availability of facades and the methods therein + """ + + @dataclass + class FacadeInfo: + name: str + is_enabled: bool + methods: list["FacadeAvailabilityInfo.MethodInfo"] + + @dataclass + class MethodInfo: + name: str + is_enabled: bool + + def __init__(self): + self.facades: list[FacadeAvailabilityInfo.FacadeInfo] = [] + + def add_facade(self, facade: Facade, is_enabled: bool): + def is_method_enabled(m: FacadeMethod) -> bool: + return is_enabled and m.enabled + + methods_info = [ + FacadeAvailabilityInfo.MethodInfo(name=method.name, is_enabled=is_method_enabled(method)) for method in facade.get_methods() + ] + self.facades.append(FacadeAvailabilityInfo.FacadeInfo(name=facade.name, is_enabled=is_enabled, methods=methods_info)) + + class SerenaReplEntrypoint: """ Represents the entrypoint object for the REPL. It holds the configured facades as attributes @@ -35,13 +65,25 @@ class SerenaReplEntrypoint: self._facades: dict[str, Facade] = {} self._current_session: SerenaSession | None = None self._current_namespace: dict[str, Any] | None = None + self._facade_availability_info = FacadeAvailabilityInfo() registered_facade_names = [] for facade in facades: - if api_scope.is_facade_enabled(facade.name): - self._register(facade) + is_facade_enabled = api_scope.is_facade_enabled(facade.name) + self._facade_availability_info.add_facade(facade, is_facade_enabled) + if is_facade_enabled: + if facade.name in self._facades: + raise ValueError(f"Duplicate facade name: {facade.name}") + self._facades[facade.name] = facade + setattr(self, facade.name, facade) registered_facade_names.append(facade.name) log.info("Registered %d/%d facades: %s", len(registered_facade_names), len(facades), registered_facade_names) + def get_facade_availability_info(self) -> FacadeAvailabilityInfo: + """ + :return: the availability of all facades and their methods (enabled or disabled) + """ + return self._facade_availability_info + def get_enabled_methods(self) -> list[FacadeMethod]: """ :return: the list of all enabled methods across all facades @@ -117,12 +159,6 @@ class SerenaReplEntrypoint: del self._current_namespace[name] return f"Removed {len(items)} persisted item(s)." - def _register(self, facade: Facade) -> None: - if facade.name in self._facades: - raise ValueError(f"Duplicate facade name: {facade.name}") - self._facades[facade.name] = facade - setattr(self, facade.name, facade) - def _get_facade(self, name: str) -> Facade: if name not in self._facades: raise ValueError(f"Unknown facade '{name}'. Available facades: {list(self._facades)}") diff --git a/src/serena/resources/dashboard/dashboard.css b/src/serena/resources/dashboard/dashboard.css index f8e4e1af..08514e77 100644 --- a/src/serena/resources/dashboard/dashboard.css +++ b/src/serena/resources/dashboard/dashboard.css @@ -603,6 +603,23 @@ code, pre, kbd, samp, cursor: default; } +.facade-block { + margin-bottom: 12px; +} + +.facade-name { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 4px; +} + +.facade-name.disabled, +.tool-item.disabled { + color: var(--text-secondary); + opacity: 0.5; +} + /* Projects List */ .project-item { padding: 10px 12px; diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index 74308c93..45330466 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -614,6 +614,7 @@ class Dashboard { const $existingToolsContent = $('#tools-content'); const $existingMemoriesContent = $('#memories-content'); const wasToolsExpanded = $existingToolsContent.is(':visible'); + const wasFunctionsExpanded = $('#functions-content').is(':visible'); const wasMemoriesExpanded = $existingMemoriesContent.is(':visible'); let html = '
'; @@ -634,10 +635,14 @@ class Dashboard { html += '
' + (config.active_project.name || 'None') + '
'; } - html += '
Languages:
'; - if (this.jetbrainsMode) { - html += '
Using JetBrains backend
'; - } else { + html += '
Interface:
'; + html += '
' + config.agent_interface + '
'; + + html += '
Backend:
'; + html += '
' + config.language_backend + '
'; + + if (!this.jetbrainsMode) { + html += '
Languages:
'; html += '
'; if (config.languages && config.languages.length > 0) { html += '
'; @@ -705,6 +710,32 @@ class Dashboard { html += '
'; html += '
'; + // Active functions of the REPL's facades - collapsible (REPL interface only) + if (config.facades) { + const enabledMethodCount = config.facades.reduce(function (count, facade) { + return count + facade.methods.filter(function (method) { return method.is_enabled; }).length; + }, 0); + html += '
'; + html += '

'; + html += 'Active Functions (' + enabledMethodCount + ')'; + html += '▼'; + html += '

'; + html += '
'; + config.facades.forEach(function (facade) { + html += '
'; + html += '
s.' + facade.name + '
'; + html += '
'; + facade.methods.forEach(function (method) { + const title = facade.name + '.' + method.name + (method.is_enabled ? '' : ' (disabled)'); + html += '
' + method.name + '
'; + }); + html += '
'; + html += '
'; + }); + html += '
'; + html += '
'; + } + // Available memories - collapsible (show if memories exist or if project exists) if (config.active_project && config.active_project.name) { html += '
'; @@ -773,6 +804,15 @@ class Dashboard { $('#create-memory-btn').click(this.openCreateMemoryModal.bind(this)); // Re-attach collapsible handler for the newly created tools header + $('#functions-header').click(function () { + const $header = $(this); + const $content = $('#functions-content'); + const $icon = $header.find('.toggle-icon'); + + $content.slideToggle(300); + $icon.toggleClass('expanded'); + }); + $('#tools-header').click(function () { const $header = $(this); const $content = $('#tools-content');