Dashboard: possibility to add a language.

Also specialize dashboard to jetbrains mode when it is active
This commit is contained in:
MischaPanch committed 2025-10-29 22:44:55 +01:00
1 parent 2c2fcc6a07
commit 31dae80218
7 files changed
+508 -14

No files matched your search

+9
View File
@@ -557,6 +557,15 @@ class SerenaAgent:
else:
self.issue_task(task, name=task_name)
def remove_language(self, language: Language) -> None:
"""
Removes a language from the active project, shutting down the respective language server and updating the project configuration.
The removal is scheduled via the agent's task executor and executed asynchronously.
:param language: the language to remove
"""
self.issue_task(lambda: self.get_active_project_or_raise().remove_language(language), name=f"RemoveLanguage:{language.value}")
def get_tool(self, tool_class: type[TTool]) -> TTool:
return self._all_tools[tool_class] # type: ignore
+90
View File
@@ -50,6 +50,20 @@ class ResponseConfigOverview(BaseModel):
available_modes: list[dict[str, str | bool]]
available_contexts: list[dict[str, str | bool]]
available_memories: list[str] | None
jetbrains_mode: bool
languages: list[str]
class ResponseAvailableLanguages(BaseModel):
languages: list[str]
class RequestAddLanguage(BaseModel):
language: str
class RequestRemoveLanguage(BaseModel):
language: str
class SerenaDashboardAPI:
@@ -127,6 +141,35 @@ class SerenaDashboardAPI:
self._shutdown()
return {"status": "shutting down"}
@self._app.route("/get_available_languages", methods=["GET"])
def get_available_languages() -> dict[str, Any]:
result = self._get_available_languages()
return result.model_dump()
@self._app.route("/add_language", methods=["POST"])
def add_language() -> dict[str, str]:
request_data = request.get_json()
if not request_data:
return {"status": "error", "message": "No data provided"}
request_add_language = RequestAddLanguage.model_validate(request_data)
try:
self._add_language(request_add_language)
return {"status": "success", "message": f"Language {request_add_language.language} added successfully"}
except Exception as e:
return {"status": "error", "message": str(e)}
@self._app.route("/remove_language", methods=["POST"])
def remove_language() -> dict[str, str]:
request_data = request.get_json()
if not request_data:
return {"status": "error", "message": "No data provided"}
request_remove_language = RequestRemoveLanguage.model_validate(request_data)
try:
self._remove_language(request_remove_language)
return {"status": "success", "message": f"Language {request_remove_language.language} removed successfully"}
except Exception as e:
return {"status": "error", "message": str(e)}
def _get_log_messages(self, request_log: RequestLog) -> ResponseLog:
all_messages = self._memory_log_handler.get_log_messages()
requested_messages = all_messages[request_log.start_idx :] if request_log.start_idx <= len(all_messages) else []
@@ -233,6 +276,11 @@ class SerenaDashboardAPI:
if self._agent.tool_is_active("read_memory") and project is not None:
available_memories = project.memories_manager.list_memories()
# Get list of languages for the active project
languages = []
if project is not None:
languages = [lang.value for lang in project.project_config.languages]
return ResponseConfigOverview(
active_project=project_info,
context=context_info,
@@ -244,6 +292,8 @@ class SerenaDashboardAPI:
available_modes=available_modes,
available_contexts=available_contexts,
available_memories=available_memories,
jetbrains_mode=self._agent.serena_config.jetbrains,
languages=languages,
)
def _shutdown(self) -> None:
@@ -255,6 +305,46 @@ class SerenaDashboardAPI:
# noinspection PyUnresolvedReferences
os._exit(0)
def _get_available_languages(self) -> ResponseAvailableLanguages:
from solidlsp.ls_config import Language
# Get all non-experimental languages
all_languages = [lang.value for lang in Language.iter_all(include_experimental=False)]
# Filter out already added languages for the active project
project = self._agent.get_active_project()
if project:
current_languages = [lang.value for lang in project.project_config.languages]
available_languages = [lang for lang in all_languages if lang not in current_languages]
else:
available_languages = all_languages
return ResponseAvailableLanguages(languages=sorted(available_languages))
def _add_language(self, request_add_language: RequestAddLanguage) -> None:
from solidlsp.ls_config import Language
# Convert string to Language enum
try:
language = Language(request_add_language.language)
except ValueError:
raise ValueError(f"Invalid language: {request_add_language.language}")
# Add the language to the active project
self._agent.add_language(language, wait_for_ls_startup=False)
def _remove_language(self, request_remove_language: RequestRemoveLanguage) -> None:
from solidlsp.ls_config import Language
# Convert string to Language enum
try:
language = Language(request_remove_language.language)
except ValueError:
raise ValueError(f"Invalid language: {request_remove_language.language}")
# Remove the language from the active project
self._agent.remove_language(language)
@staticmethod
def _find_first_free_port(start_port: int) -> int:
port = start_port
+20 -5
View File
@@ -175,6 +175,25 @@ class LanguageServerManager:
raise ValueError(f"Language server for language {language.value} already present")
return self._create_and_start_language_server(language)
def remove_language_server(self, language: Language, save_cache: bool = False) -> None:
"""
Removes the language server for the given language, stopping it if it is running.
:param language: the language
"""
if language not in self._language_servers:
raise ValueError(f"No language server for language {language.value} present; cannot remove")
ls = self._language_servers.pop(language)
self._stop_language_server(ls, save_cache=save_cache)
@staticmethod
def _stop_language_server(ls: SolidLanguageServer, save_cache: bool = False) -> None:
if ls.is_running():
if save_cache:
ls.save_cache()
log.info(f"Stopping language server for language {ls.language} ...")
ls.stop()
def iter_language_servers(self) -> Iterator[SolidLanguageServer]:
for ls in self._language_servers.values():
yield self._ensure_functional_ls(ls)
@@ -186,11 +205,7 @@ class LanguageServerManager:
:param save_cache: whether to save the cache before stopping
"""
for ls in self.iter_language_servers():
if ls.is_running():
if save_cache:
ls.save_cache()
log.info(f"Stopping language server for language {ls.language} ...")
ls.stop()
self._stop_language_server(ls, save_cache=save_cache)
def save_all_caches(self) -> None:
"""
+22
View File
@@ -416,6 +416,28 @@ class Project(ToStringMixin):
log.info("Adding and starting the language server for new language %s ...", language.value)
self.language_server_manager.add_language_server(language)
def remove_language(self, language: Language) -> None:
"""
Removes a programming language from the project configuration, stopping the corresponding
language server instance if the LS manager is active.
The project configuration is saved to disk after removing the language.
:param language: the programming language to remove
"""
if language not in self.project_config.languages:
log.info(f"Language {language.value} is not present in the project configuration.")
return
# update the project configuration
self.project_config.languages.remove(language)
self.save_config()
# stop the language server (if the LS manager is active)
if self.language_server_manager is None:
log.info("Language server manager is not active; skipping language server shutdown for the removed language.")
else:
log.info("Removing and stopping the language server for language %s ...", language.value)
self.language_server_manager.remove_language_server(language)
def shutdown(self) -> None:
if self.language_server_manager is not None:
self.language_server_manager.stop_all(save_cache=True)
@@ -618,3 +618,96 @@ body {
min-width: 100%;
}
}
/* Modal Styles */
.modal {
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
background-color: var(--bg-primary);
margin: 10% auto;
padding: 25px;
border: 1px solid var(--border-color);
border-radius: 8px;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
position: relative;
}
.modal-close {
color: var(--text-muted);
float: right;
font-size: 28px;
font-weight: bold;
line-height: 20px;
cursor: pointer;
transition: color 0.2s;
}
.modal-close:hover,
.modal-close:focus {
color: var(--text-primary);
}
.modal h3 {
margin-top: 0;
margin-bottom: 15px;
color: var(--text-primary);
}
/* Language Badge Styles */
.languages-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.language-badge {
position: relative;
display: inline-flex;
align-items: center;
padding: 6px 12px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 13px;
font-weight: 500;
}
.language-badge.removable {
padding-right: 28px;
}
.language-remove {
position: absolute;
top: 2px;
right: 2px;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 68, 68, 0.1);
border-radius: 3px;
cursor: pointer;
color: #ff4444;
font-size: 14px;
font-weight: bold;
line-height: 1;
transition: all 0.2s;
}
.language-remove:hover {
background: rgba(255, 68, 68, 0.2);
transform: scale(1.1);
}
+232 -8
View File
@@ -52,6 +52,9 @@ class Dashboard {
// Page state
this.currentPage = 'overview';
this.configData = null;
this.jetbrainsMode = false;
this.activeProjectName = null;
this.languageToRemove = null;
// Tool names and stats
this.toolNames = [];
@@ -80,6 +83,18 @@ class Dashboard {
this.$availableToolsDisplay = $('#available-tools-display');
this.$availableModesDisplay = $('#available-modes-display');
this.$availableContextsDisplay = $('#available-contexts-display');
this.$addLanguageBtn = $('#add-language-btn');
this.$addLanguageModal = $('#add-language-modal');
this.$modalLanguageSelect = $('#modal-language-select');
this.$modalProjectName = $('#modal-project-name');
this.$modalAddBtn = $('#modal-add-btn');
this.$modalCancelBtn = $('#modal-cancel-btn');
this.$modalClose = $('.modal-close');
this.$removeLanguageModal = $('#remove-language-modal');
this.$removeLanguageName = $('#remove-language-name');
this.$removeModalOkBtn = $('#remove-modal-ok-btn');
this.$removeModalCancelBtn = $('#remove-modal-cancel-btn');
this.$modalCloseRemove = $('.modal-close-remove');
// Chart references
this.countChart = null;
@@ -97,6 +112,13 @@ class Dashboard {
this.$themeToggle.click(this.toggleTheme.bind(this));
this.$refreshStats.click(this.loadStats.bind(this));
this.$clearStats.click(this.clearStats.bind(this));
this.$addLanguageBtn.click(this.openLanguageModal.bind(this));
this.$modalAddBtn.click(this.addLanguageFromModal.bind(this));
this.$modalCancelBtn.click(this.closeLanguageModal.bind(this));
this.$modalClose.click(this.closeLanguageModal.bind(this));
this.$removeModalOkBtn.click(this.confirmRemoveLanguageOk.bind(this));
this.$removeModalCancelBtn.click(this.closeRemoveLanguageModal.bind(this));
this.$modalCloseRemove.click(this.closeRemoveLanguageModal.bind(this));
// Page navigation
$('[data-page]').click(function(e) {
@@ -112,6 +134,19 @@ class Dashboard {
}
});
// Close modals when clicking outside
this.$addLanguageModal.click(function(e) {
if ($(e.target).hasClass('modal')) {
self.closeLanguageModal();
}
});
this.$removeLanguageModal.click(function(e) {
if ($(e.target).hasClass('modal')) {
self.closeRemoveLanguageModal();
}
});
// Collapsible sections
$('.collapsible-header').click(function() {
const $header = $(this);
@@ -125,6 +160,17 @@ class Dashboard {
// Initialize theme
this.initializeTheme();
// Add ESC key handler for closing modals
$(document).keydown(function(e) {
if (e.key === 'Escape' || e.keyCode === 27) {
if (self.$addLanguageModal.is(':visible')) {
self.closeLanguageModal();
} else if (self.$removeLanguageModal.is(':visible')) {
self.closeRemoveLanguageModal();
}
}
});
// Initialize the application
this.loadToolNames().then(function() {
// Start on overview page
@@ -187,8 +233,11 @@ class Dashboard {
url: '/get_config_overview',
type: 'GET',
success: function(response) {
console.log('Config overview response:', response);
self.failureCount = 0;
self.configData = response;
self.jetbrainsMode = response.jetbrains_mode;
self.activeProjectName = response.active_project.name;
self.displayConfig(response);
self.displayBasicStats(response.tool_stats_summary);
self.displayProjects(response.registered_projects);
@@ -219,13 +268,14 @@ class Dashboard {
}
displayConfig(config) {
// Check if tools and memories sections are currently expanded
const $existingToolsContent = $('#tools-content');
const $existingMemoriesContent = $('#memories-content');
const wasToolsExpanded = $existingToolsContent.is(':visible');
const wasMemoriesExpanded = $existingMemoriesContent.is(':visible');
try {
// Check if tools and memories sections are currently expanded
const $existingToolsContent = $('#tools-content');
const $existingMemoriesContent = $('#memories-content');
const wasToolsExpanded = $existingToolsContent.is(':visible');
const wasMemoriesExpanded = $existingMemoriesContent.is(':visible');
let html = '<div class="config-grid">';
let html = '<div class="config-grid">';
// Project info
html += '<div class="config-label">Active Project:</div>';
@@ -236,8 +286,28 @@ class Dashboard {
html += '<div class="config-value">' + (config.active_project.name || 'None') + '</div>';
}
html += '<div class="config-label">Language:</div>';
html += '<div class="config-value">' + (config.active_project.language || 'N/A') + '</div>';
html += '<div class="config-label">Languages:</div>';
if (this.jetbrainsMode) {
html += '<div class="config-value">Using JetBrains backend</div>';
} else {
html += '<div class="config-value">';
if (config.languages && config.languages.length > 0) {
html += '<div class="languages-container">';
config.languages.forEach(function(language, index) {
const isRemovable = config.languages.length > 1;
html += '<div class="language-badge' + (isRemovable ? ' removable' : '') + '">';
html += language;
if (isRemovable) {
html += '<span class="language-remove" data-language="' + language + '">&times;</span>';
}
html += '</div>';
});
html += '</div>';
} else {
html += 'N/A';
}
html += '</div>';
}
// Context info
html += '<div class="config-label">Context:</div>';
@@ -294,6 +364,22 @@ class Dashboard {
this.$configDisplay.html(html);
// Show/hide add language button based on jetbrains mode
if (this.jetbrainsMode) {
this.$addLanguageBtn.hide();
} else {
this.$addLanguageBtn.show();
}
// Attach event handlers for language remove buttons
const self = this;
$('.language-remove').click(function(e) {
e.preventDefault();
e.stopPropagation();
const language = $(this).data('language');
self.confirmRemoveLanguage(language);
});
// Re-attach collapsible handler for the newly created tools header
$('#tools-header').click(function() {
const $header = $(this);
@@ -313,6 +399,10 @@ class Dashboard {
$content.slideToggle(300);
$icon.toggleClass('expanded');
});
} catch (error) {
console.error('Error in displayConfig:', error);
this.$configDisplay.html('<div class="error-message">Error displaying configuration: ' + error.message + '</div>');
}
}
displayBasicStats(stats) {
@@ -952,6 +1042,140 @@ class Dashboard {
}
}
// ===== Language Management Methods =====
confirmRemoveLanguage(language) {
// Store the language to remove
this.languageToRemove = language;
// Set language name in modal
this.$removeLanguageName.text(language);
// Show modal
this.$removeLanguageModal.fadeIn(200);
}
closeRemoveLanguageModal() {
this.$removeLanguageModal.fadeOut(200);
this.languageToRemove = null;
}
confirmRemoveLanguageOk() {
if (this.languageToRemove) {
this.removeLanguage(this.languageToRemove);
this.closeRemoveLanguageModal();
}
}
removeLanguage(language) {
const self = this;
$.ajax({
url: '/remove_language',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
language: language
}),
success: function(response) {
if (response.status === 'success') {
// Reload config to show updated language list
self.loadConfigOverview();
} else {
alert('Error: ' + response.message);
}
},
error: function(xhr, status, error) {
console.error('Error removing language:', error);
alert('Error removing language: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
}
});
}
openLanguageModal() {
// Set project name in modal
this.$modalProjectName.text(this.activeProjectName || 'Unknown');
// Load available languages into modal dropdown
this.loadAvailableLanguages();
// Show modal
this.$addLanguageModal.fadeIn(200);
}
closeLanguageModal() {
this.$addLanguageModal.fadeOut(200);
this.$modalLanguageSelect.empty();
this.$modalAddBtn.prop('disabled', false).text('Add Language');
}
loadAvailableLanguages() {
let self = this;
$.ajax({
url: '/get_available_languages',
type: 'GET',
success: function(response) {
const languages = response.languages || [];
// Clear all existing options
self.$modalLanguageSelect.empty();
if (languages.length === 0) {
// Show message if no languages available
self.$modalLanguageSelect.append($('<option>').val('').text('No languages available to add'));
self.$modalAddBtn.prop('disabled', true);
} else {
// Add language options
languages.forEach(function(language) {
self.$modalLanguageSelect.append($('<option>').val(language).text(language));
});
self.$modalAddBtn.prop('disabled', false);
}
},
error: function(xhr, status, error) {
console.error('Error loading available languages:', error);
}
});
}
addLanguageFromModal() {
const selectedLanguage = this.$modalLanguageSelect.val();
if (!selectedLanguage) {
alert('No language selected or no languages available to add');
return;
}
const self = this;
// Disable button during request
self.$modalAddBtn.prop('disabled', true).text('Adding...');
$.ajax({
url: '/add_language',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
language: selectedLanguage
}),
success: function(response) {
if (response.status === 'success') {
// Close modal
self.closeLanguageModal();
// Reload config to show updated language
self.loadConfigOverview();
} else {
alert('Error: ' + response.message);
}
},
error: function(xhr, status, error) {
console.error('Error adding language:', error);
alert('Error adding language: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
},
complete: function() {
// Re-enable button
self.$modalAddBtn.prop('disabled', false).text('Add Language');
}
});
}
// ===== Shutdown Method =====
shutdown() {
+42 -1
View File
@@ -44,7 +44,10 @@
<div class="overview-container">
<div class="overview-left">
<section class="config-section">
<h2>Current Configuration</h2>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h2 style="margin: 0;">Current Configuration</h2>
<button id="add-language-btn" class="btn" style="padding: 6px 12px; font-size: 14px;">Add Language</button>
</div>
<div id="config-display">
<div class="loading">Loading configuration...</div>
</div>
@@ -144,6 +147,44 @@
</div>
</div>
<!-- Add Language Modal -->
<div id="add-language-modal" class="modal" style="display:none">
<div class="modal-content">
<span class="modal-close">&times;</span>
<h3>Add Language</h3>
<p id="modal-project-info" style="margin: 15px 0; color: var(--text-primary); line-height: 1.5;">
Adding a language to serena config of project <strong id="modal-project-name"></strong>.
</p>
<p style="margin: 15px 0; color: var(--text-muted); font-size: 13px; line-height: 1.5;">
Note that this may download some dependencies needed for the language server and then start it,
it may take a few seconds before the LS is responsive.
</p>
<div style="margin: 20px 0;">
<label for="modal-language-select" style="display: block; margin-bottom: 8px; font-weight: 500;">Select Language:</label>
<select id="modal-language-select" class="language-select" style="width: 100%; padding: 8px 10px; border-radius: 4px; border: 1px solid var(--border-color); background: var(--bg-primary); color: var(--text-primary); font-size: 14px;">
</select>
</div>
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
<button id="modal-cancel-btn" class="btn" style="background: var(--bg-secondary); color: var(--text-primary); padding: 8px 16px;">Cancel</button>
<button id="modal-add-btn" class="btn" style="padding: 8px 16px;">Add Language</button>
</div>
</div>
</div>
<!-- Remove Language Confirmation Modal -->
<div id="remove-language-modal" class="modal" style="display:none">
<div class="modal-content">
<span class="modal-close modal-close-remove">&times;</span>
<p style="margin: 20px 0; color: var(--text-primary); line-height: 1.5; font-size: 15px;">
Remove language <strong id="remove-language-name"></strong> from configuration?
</p>
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
<button id="remove-modal-cancel-btn" class="btn" style="background: var(--bg-secondary); color: var(--text-primary); padding: 8px 16px;">Cancel</button>
<button id="remove-modal-ok-btn" class="btn" style="padding: 8px 16px;">OK</button>
</div>
</div>
</div>
<script>
$(document).ready(function () {
const dashboard = new Dashboard();