diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 3be3f3d5..6dcd0e5c 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -86,6 +86,14 @@ class RequestDeleteMemory(BaseModel): memory_name: str +class ResponseGetSerenaConfig(BaseModel): + content: str + + +class RequestSaveSerenaConfig(BaseModel): + content: str + + class RequestCancelTaskExecution(BaseModel): task_id: int @@ -248,6 +256,26 @@ class SerenaDashboardAPI: except Exception as e: return {"status": "error", "message": str(e)} + @self._app.route("/get_serena_config", methods=["GET"]) + def get_serena_config() -> dict[str, Any]: + try: + result = self._get_serena_config() + return result.model_dump() + except Exception as e: + return {"status": "error", "message": str(e)} + + @self._app.route("/save_serena_config", methods=["POST"]) + def save_serena_config() -> dict[str, str]: + request_data = request.get_json() + if not request_data: + return {"status": "error", "message": "No data provided"} + request_save_config = RequestSaveSerenaConfig.model_validate(request_data) + try: + self._save_serena_config(request_save_config) + return {"status": "success", "message": "Serena config saved successfully"} + except Exception as e: + return {"status": "error", "message": str(e)} + @self._app.route("/queued_task_executions", methods=["GET"]) def get_queued_executions() -> dict[str, Any]: try: @@ -473,6 +501,27 @@ class SerenaDashboardAPI: self._agent.execute_task(run, logged=True, name="DeleteMemory") + def _get_serena_config(self) -> ResponseGetSerenaConfig: + config_path = self._agent.serena_config.config_file_path + if config_path is None or not os.path.exists(config_path): + raise ValueError("Serena config file not found") + + with open(config_path, encoding="utf-8") as f: + content = f.read() + + return ResponseGetSerenaConfig(content=content) + + def _save_serena_config(self, request_save_config: RequestSaveSerenaConfig) -> None: + def run() -> None: + config_path = self._agent.serena_config.config_file_path + if config_path is None: + raise ValueError("Serena config file path not set") + + with open(config_path, "w", encoding="utf-8") as f: + f.write(request_save_config.content) + + self._agent.execute_task(run, logged=True, name="SaveSerenaConfig") + def _add_language(self, request_add_language: RequestAddLanguage) -> None: from solidlsp.ls_config import Language diff --git a/src/serena/resources/dashboard/dashboard.css b/src/serena/resources/dashboard/dashboard.css index 92f1184d..e751ef89 100644 --- a/src/serena/resources/dashboard/dashboard.css +++ b/src/serena/resources/dashboard/dashboard.css @@ -919,6 +919,24 @@ body { color: var(--btn-primary); } +.memory-add-btn { + padding: 6px 12px; + font-size: 13px; + font-weight: 500; + border-radius: 6px; + border: 1px dashed var(--border-color); + background: var(--bg-secondary); + color: var(--text-primary); + cursor: pointer; + transition: all 0.2s; +} + +.memory-add-btn:hover { + background: var(--border-color); + border-color: var(--btn-primary); + color: var(--btn-primary); +} + .language-spinner { display: inline-flex; align-items: center; diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index efc525e0..0a80680f 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -60,7 +60,10 @@ class Dashboard { this.memoryContentDirty = false; this.memoryToDelete = null; this.isAddingLanguage = false; - this.waitingForPollingResult = false; + this.waitingForConfigPollingResult = false; + this.waitingForExecutionsPollingResult = false; + this.originalSerenaConfigContent = null; + this.serenaConfigContentDirty = false; // Execution tracking this.cancelledExecutions = []; @@ -129,6 +132,11 @@ class Dashboard { this.$cancelExecutionOkBtn = $('#cancel-execution-ok-btn'); this.$cancelExecutionCancelBtn = $('#cancel-execution-cancel-btn'); this.$modalCloseCancelExecution = $('.modal-close-cancel-execution'); + this.$editSerenaConfigModal = $('#edit-serena-config-modal'); + this.$editSerenaConfigContent = $('#edit-serena-config-content'); + this.$editSerenaConfigSaveBtn = $('#edit-serena-config-save-btn'); + this.$editSerenaConfigCancelBtn = $('#edit-serena-config-cancel-btn'); + this.$modalCloseEditSerenaConfig = $('.modal-close-edit-serena-config'); // Chart references this.countChart = null; @@ -171,6 +179,9 @@ class Dashboard { this.$cancelExecutionOkBtn.click(this.confirmCancelExecutionOk.bind(this)); this.$cancelExecutionCancelBtn.click(this.closeCancelExecutionModal.bind(this)); this.$modalCloseCancelExecution.click(this.closeCancelExecutionModal.bind(this)); + this.$editSerenaConfigSaveBtn.click(this.saveSerenaConfigFromModal.bind(this)); + this.$editSerenaConfigCancelBtn.click(this.closeEditSerenaConfigModal.bind(this)); + this.$modalCloseEditSerenaConfig.click(this.closeEditSerenaConfigModal.bind(this)); // Page navigation $('[data-page]').click(function(e) { @@ -217,6 +228,12 @@ class Dashboard { } }); + this.$editSerenaConfigModal.click(function(e) { + if ($(e.target).hasClass('modal')) { + self.closeEditSerenaConfigModal(); + } + }); + // Collapsible sections $('.collapsible-header').click(function() { const $header = $(this); @@ -310,11 +327,12 @@ class Dashboard { // ===== Config Overview Methods ===== loadConfigOverview() { - if (this.waitingForPollingResult) { + if (this.waitingForConfigPollingResult) { console.log('Still waiting for previous config poll result, skipping this poll'); return; } - this.waitingForPollingResult = true; + this.waitingForConfigPollingResult = true; + console.log('Polling for config overview...'); let self = this; $.ajax({ url: '/get_config_overview', @@ -346,23 +364,21 @@ class Dashboard { self.$availableContextsDisplay.html('
Error loading contexts
'); }, complete: function() { - self.waitingForPollingResult = false; + self.waitingForConfigPollingResult = false; } }); } startConfigPolling() { - // Poll every 2 seconds for config updates - this.configPollInterval = setInterval(this.loadConfigOverview.bind(this), 2000); + this.configPollInterval = setInterval(this.loadConfigOverview.bind(this), 100000); } startExecutionsPolling() { // Poll every 1 second for executions (independent of config polling) // This ensures stuck executions can still be cancelled even if config polling is blocked - this.loadExecutions(); - this.loadLastExecution(); + this.loadExecutions() this.executionsPollInterval = setInterval(() => { - this.loadExecutions(); + this.loadQueuedExecutions(); this.loadLastExecution(); }, 1000); } @@ -475,22 +491,28 @@ class Dashboard { }); } // Add Create Memory button - html += ''; + html += ''; html += ''; html += ''; } - // Configuration help link - html += '
'; + // Configuration help link and edit config button + html += '
'; + html += '
'; html += '📖 '; html += 'View Configuration Guide'; html += '
'; + html += ''; + html += '
'; this.$configDisplay.html(html); // Attach event handlers for the dynamically created add language button $('#add-language-btn').click(this.openLanguageModal.bind(this)); + // Attach event handler for edit serena config button + $('#edit-serena-config-btn').click(this.openEditSerenaConfigModal.bind(this)); + // Attach event handlers for language remove buttons const self = this; $('.language-remove').click(function(e) { @@ -637,7 +659,7 @@ class Dashboard { // ===== Executions Methods ===== - loadExecutions() { + loadQueuedExecutions() { let self = this; $.ajax({ url: '/queued_task_executions', @@ -677,6 +699,17 @@ class Dashboard { }); } + loadExecutions() { + if (this.waitingForExecutionsPollingResult) { + console.log('Still waiting for previous executions poll result, skipping this poll'); + } else { + this.waitingForExecutionsPollingResult = true; + console.log('Polling for executions...'); + this.loadQueuedExecutions(); + this.loadLastExecution(); + } + } + displayActiveExecutionsQueue(executions) { if (!executions || executions.length === 0) { return; @@ -863,7 +896,7 @@ class Dashboard { console.log('Task ' + executionData.task_id + ' could not be cancelled (may have already completed). ' + response.message ); } // Refresh display regardless - self.loadExecutions(); + self.loadQueuedExecutions(); } else { console.error('Unexpected response status:', response.status); alert('Unexpected response from server'); @@ -1832,6 +1865,89 @@ class Dashboard { }); } + // ===== Serena Config Editing Methods ===== + + openEditSerenaConfigModal() { + const self = this; + this.serenaConfigContentDirty = false; + + // Load serena config content + $.ajax({ + url: '/get_serena_config', + type: 'GET', + success: function(response) { + if (response.status === 'error') { + alert('Error: ' + response.message); + return; + } + self.originalSerenaConfigContent = response.content; + self.$editSerenaConfigContent.val(response.content); + self.serenaConfigContentDirty = false; + self.$editSerenaConfigModal.fadeIn(200); + }, + error: function(xhr, status, error) { + console.error('Error loading serena config:', error); + alert('Error loading serena config: ' + (xhr.responseJSON ? xhr.responseJSON.message : error)); + } + }); + + // Track changes to config content + this.$editSerenaConfigContent.off('input').on('input', function() { + const currentContent = self.$editSerenaConfigContent.val(); + self.serenaConfigContentDirty = (currentContent !== self.originalSerenaConfigContent); + }); + } + + closeEditSerenaConfigModal() { + // Check if there are unsaved changes + if (this.serenaConfigContentDirty) { + if (!confirm('You have unsaved changes. Are you sure you want to close?')) { + return; + } + } + + this.$editSerenaConfigModal.fadeOut(200); + this.originalSerenaConfigContent = null; + this.serenaConfigContentDirty = false; + } + + saveSerenaConfigFromModal() { + const self = this; + const content = this.$editSerenaConfigContent.val(); + + // Disable button during request + self.$editSerenaConfigSaveBtn.prop('disabled', true).text('Saving...'); + + $.ajax({ + url: '/save_serena_config', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ + content: content + }), + success: function(response) { + if (response.status === 'success') { + // Update original content and reset dirty flag + self.originalSerenaConfigContent = content; + self.serenaConfigContentDirty = false; + // Close modal + self.$editSerenaConfigModal.fadeOut(200); + alert('Configuration saved successfully. Please restart Serena for changes to take effect.'); + } else { + alert('Error: ' + response.message); + } + }, + error: function(xhr, status, error) { + console.error('Error saving serena config:', error); + alert('Error saving serena config: ' + (xhr.responseJSON ? xhr.responseJSON.message : error)); + }, + complete: function() { + // Re-enable button + self.$editSerenaConfigSaveBtn.prop('disabled', false).text('Save'); + } + }); + } + // ===== Shutdown Method ===== shutdown() { @@ -1846,7 +1962,7 @@ class Dashboard { self.$errorContainer.html('
Shutting down ...
') setTimeout(function() { window.close(); - }, 2000); + }, 1000); } // ask for confirmation using a dialog diff --git a/src/serena/resources/dashboard/index.html b/src/serena/resources/dashboard/index.html index 82e1239c..d9220cba 100644 --- a/src/serena/resources/dashboard/index.html +++ b/src/serena/resources/dashboard/index.html @@ -293,6 +293,25 @@
+ + +