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('
'); }, 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 += '