From c6eba9046024e0e413f441639b0377f1e207c74c Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Thu, 30 Oct 2025 12:54:36 +0100 Subject: [PATCH] Added possibility to edit and delete memories from dashboard Also hide Add Language button when no active project is present --- src/serena/dashboard.py | 76 +++++++ src/serena/resources/dashboard/dashboard.css | 76 +++++++ src/serena/resources/dashboard/dashboard.js | 208 ++++++++++++++++++- src/serena/resources/dashboard/index.html | 27 +++ 4 files changed, 384 insertions(+), 3 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 2325af90..c7a8ad69 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -66,6 +66,24 @@ class RequestRemoveLanguage(BaseModel): language: str +class RequestGetMemory(BaseModel): + memory_name: str + + +class ResponseGetMemory(BaseModel): + content: str + memory_name: str + + +class RequestSaveMemory(BaseModel): + memory_name: str + content: str + + +class RequestDeleteMemory(BaseModel): + memory_name: str + + class SerenaDashboardAPI: log = logging.getLogger(__qualname__) @@ -170,6 +188,42 @@ class SerenaDashboardAPI: except Exception as e: return {"status": "error", "message": str(e)} + @self._app.route("/get_memory", methods=["POST"]) + def get_memory() -> dict[str, Any]: + request_data = request.get_json() + if not request_data: + return {"status": "error", "message": "No data provided"} + request_get_memory = RequestGetMemory.model_validate(request_data) + try: + result = self._get_memory(request_get_memory) + return result.model_dump() + except Exception as e: + return {"status": "error", "message": str(e)} + + @self._app.route("/save_memory", methods=["POST"]) + def save_memory() -> dict[str, str]: + request_data = request.get_json() + if not request_data: + return {"status": "error", "message": "No data provided"} + request_save_memory = RequestSaveMemory.model_validate(request_data) + try: + self._save_memory(request_save_memory) + return {"status": "success", "message": f"Memory {request_save_memory.memory_name} saved successfully"} + except Exception as e: + return {"status": "error", "message": str(e)} + + @self._app.route("/delete_memory", methods=["POST"]) + def delete_memory() -> dict[str, str]: + request_data = request.get_json() + if not request_data: + return {"status": "error", "message": "No data provided"} + request_delete_memory = RequestDeleteMemory.model_validate(request_data) + try: + self._delete_memory(request_delete_memory) + return {"status": "success", "message": f"Memory {request_delete_memory.memory_name} deleted 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 [] @@ -321,6 +375,28 @@ class SerenaDashboardAPI: return ResponseAvailableLanguages(languages=sorted(available_languages)) + def _get_memory(self, request_get_memory: RequestGetMemory) -> ResponseGetMemory: + project = self._agent.get_active_project() + if project is None: + raise ValueError("No active project") + + content = project.memories_manager.load_memory(request_get_memory.memory_name) + return ResponseGetMemory(content=content, memory_name=request_get_memory.memory_name) + + def _save_memory(self, request_save_memory: RequestSaveMemory) -> None: + project = self._agent.get_active_project() + if project is None: + raise ValueError("No active project") + + project.memories_manager.save_memory(request_save_memory.memory_name, request_save_memory.content) + + def _delete_memory(self, request_delete_memory: RequestDeleteMemory) -> None: + project = self._agent.get_active_project() + if project is None: + raise ValueError("No active project") + + project.memories_manager.delete_memory(request_delete_memory.memory_name) + 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 d320f24c..57d78cd9 100644 --- a/src/serena/resources/dashboard/dashboard.css +++ b/src/serena/resources/dashboard/dashboard.css @@ -711,3 +711,79 @@ body { background: rgba(255, 68, 68, 0.2); transform: scale(1.1); } + +/* Memory Editor Styles */ +.modal-content-large { + max-width: 800px; + width: 90%; +} + +.memory-editor { + font-family: 'Courier New', monospace; + font-size: 13px; + line-height: 1.5; + tab-size: 4; + -moz-tab-size: 4; +} + +.memory-editor:focus { + outline: 2px solid var(--btn-primary); + outline-offset: -1px; +} + +/* Memory Item Styles */ +.memory-item { + position: relative; + display: inline-flex; + align-items: center; + padding: 8px 12px; + margin: 5px; + border-radius: 4px; + background-color: var(--bg-primary); + border: 1px solid var(--border-color); + transition: background-color 0.2s ease; + cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.memory-item:hover { + background-color: var(--border-color); + text-decoration: underline; +} + +.memory-item.removable { + padding-right: 28px; +} + +.memory-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; +} + +.memory-remove:hover { + background: rgba(255, 68, 68, 0.2); + transform: scale(1.1); + text-decoration: none; +} + +.memories-container { + display: flex; + flex-wrap: wrap; + gap: 8px; +} diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index fbb306b7..736afaf6 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -55,6 +55,10 @@ class Dashboard { this.jetbrainsMode = false; this.activeProjectName = null; this.languageToRemove = null; + this.currentMemoryName = null; + this.originalMemoryContent = null; + this.memoryContentDirty = false; + this.memoryToDelete = null; // Tool names and stats this.toolNames = []; @@ -95,6 +99,17 @@ class Dashboard { this.$removeModalOkBtn = $('#remove-modal-ok-btn'); this.$removeModalCancelBtn = $('#remove-modal-cancel-btn'); this.$modalCloseRemove = $('.modal-close-remove'); + this.$editMemoryModal = $('#edit-memory-modal'); + this.$editMemoryName = $('#edit-memory-name'); + this.$editMemoryContent = $('#edit-memory-content'); + this.$editMemorySaveBtn = $('#edit-memory-save-btn'); + this.$editMemoryCancelBtn = $('#edit-memory-cancel-btn'); + this.$modalCloseEditMemory = $('.modal-close-edit-memory'); + this.$deleteMemoryModal = $('#delete-memory-modal'); + this.$deleteMemoryName = $('#delete-memory-name'); + this.$deleteMemoryOkBtn = $('#delete-memory-ok-btn'); + this.$deleteMemoryCancelBtn = $('#delete-memory-cancel-btn'); + this.$modalCloseDeleteMemory = $('.modal-close-delete-memory'); // Chart references this.countChart = null; @@ -119,6 +134,13 @@ class Dashboard { this.$removeModalOkBtn.click(this.confirmRemoveLanguageOk.bind(this)); this.$removeModalCancelBtn.click(this.closeRemoveLanguageModal.bind(this)); this.$modalCloseRemove.click(this.closeRemoveLanguageModal.bind(this)); + this.$editMemorySaveBtn.click(this.saveMemoryFromModal.bind(this)); + this.$editMemoryCancelBtn.click(this.closeEditMemoryModal.bind(this)); + this.$modalCloseEditMemory.click(this.closeEditMemoryModal.bind(this)); + this.$editMemoryContent.on('input', this.trackMemoryChanges.bind(this)); + this.$deleteMemoryOkBtn.click(this.confirmDeleteMemoryOk.bind(this)); + this.$deleteMemoryCancelBtn.click(this.closeDeleteMemoryModal.bind(this)); + this.$modalCloseDeleteMemory.click(this.closeDeleteMemoryModal.bind(this)); // Page navigation $('[data-page]').click(function(e) { @@ -147,6 +169,18 @@ class Dashboard { } }); + this.$editMemoryModal.click(function(e) { + if ($(e.target).hasClass('modal')) { + self.closeEditMemoryModal(); + } + }); + + this.$deleteMemoryModal.click(function(e) { + if ($(e.target).hasClass('modal')) { + self.closeDeleteMemoryModal(); + } + }); + // Collapsible sections $('.collapsible-header').click(function() { const $header = $(this); @@ -167,6 +201,10 @@ class Dashboard { self.closeLanguageModal(); } else if (self.$removeLanguageModal.is(':visible')) { self.closeRemoveLanguageModal(); + } else if (self.$editMemoryModal.is(':visible')) { + self.closeEditMemoryModal(); + } else if (self.$deleteMemoryModal.is(':visible')) { + self.closeDeleteMemoryModal(); } } }); @@ -354,9 +392,12 @@ class Dashboard { html += 'Available Memories (' + config.available_memories.length + ')'; html += '▼'; html += ''; - html += '
'; + html += '
'; config.available_memories.forEach(function(memory) { - html += '
' + memory + '
'; + html += '
'; + html += memory; + html += '×'; + html += '
'; }); html += '
'; html += '
'; @@ -365,7 +406,7 @@ class Dashboard { this.$configDisplay.html(html); // Show/hide add language button based on jetbrains mode - if (this.jetbrainsMode) { + if (this.jetbrainsMode || this.activeProjectName === null) { this.$addLanguageBtn.hide(); } else { this.$addLanguageBtn.show(); @@ -380,6 +421,21 @@ class Dashboard { self.confirmRemoveLanguage(language); }); + // Attach event handlers for memory items + $('.memory-item').click(function(e) { + e.preventDefault(); + const memoryName = $(this).data('memory'); + self.openEditMemoryModal(memoryName); + }); + + // Attach event handlers for memory remove buttons + $('.memory-remove').click(function(e) { + e.preventDefault(); + e.stopPropagation(); + const memoryName = $(this).data('memory'); + self.confirmDeleteMemory(memoryName); + }); + // Re-attach collapsible handler for the newly created tools header $('#tools-header').click(function() { const $header = $(this); @@ -1176,6 +1232,152 @@ class Dashboard { }); } + // ===== Memory Editing Methods ===== + + openEditMemoryModal(memoryName) { + const self = this; + this.currentMemoryName = memoryName; + this.memoryContentDirty = false; + + // Set memory name in modal + this.$editMemoryName.text(memoryName); + + // Load memory content + $.ajax({ + url: '/get_memory', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ + memory_name: memoryName + }), + success: function(response) { + if (response.status === 'error') { + alert('Error: ' + response.message); + return; + } + self.originalMemoryContent = response.content; + self.$editMemoryContent.val(response.content); + self.memoryContentDirty = false; + self.$editMemoryModal.fadeIn(200); + }, + error: function(xhr, status, error) { + console.error('Error loading memory:', error); + alert('Error loading memory: ' + (xhr.responseJSON ? xhr.responseJSON.message : error)); + } + }); + } + + closeEditMemoryModal() { + // Check if there are unsaved changes + if (this.memoryContentDirty) { + if (!confirm('You have unsaved changes. Are you sure you want to close?')) { + return; + } + } + + this.$editMemoryModal.fadeOut(200); + this.currentMemoryName = null; + this.originalMemoryContent = null; + this.memoryContentDirty = false; + } + + trackMemoryChanges() { + const currentContent = this.$editMemoryContent.val(); + this.memoryContentDirty = (currentContent !== this.originalMemoryContent); + } + + saveMemoryFromModal() { + const self = this; + const memoryName = this.currentMemoryName; + const content = this.$editMemoryContent.val(); + + if (!memoryName) { + alert('No memory selected'); + return; + } + + // Disable button during request + self.$editMemorySaveBtn.prop('disabled', true).text('Saving...'); + + $.ajax({ + url: '/save_memory', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ + memory_name: memoryName, + content: content + }), + success: function(response) { + if (response.status === 'success') { + // Update original content and reset dirty flag + self.originalMemoryContent = content; + self.memoryContentDirty = false; + // Close modal + self.$editMemoryModal.fadeOut(200); + self.currentMemoryName = null; + } else { + alert('Error: ' + response.message); + } + }, + error: function(xhr, status, error) { + console.error('Error saving memory:', error); + alert('Error saving memory: ' + (xhr.responseJSON ? xhr.responseJSON.message : error)); + }, + complete: function() { + // Re-enable button + self.$editMemorySaveBtn.prop('disabled', false).text('Save'); + } + }); + } + + confirmDeleteMemory(memoryName) { + // Set memory name to delete + this.memoryToDelete = memoryName; + + // Set memory name in modal + this.$deleteMemoryName.text(memoryName); + + // Show modal + this.$deleteMemoryModal.fadeIn(200); + } + + closeDeleteMemoryModal() { + this.$deleteMemoryModal.fadeOut(200); + this.memoryToDelete = null; + } + + confirmDeleteMemoryOk() { + if (this.memoryToDelete) { + this.deleteMemory(this.memoryToDelete); + this.closeDeleteMemoryModal(); + } + } + + deleteMemory(memoryName) { + const self = this; + + $.ajax({ + url: '/delete_memory', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ + memory_name: memoryName + }), + success: function(response) { + if (response.status === 'success') { + // Reload config to show updated memory list + self.loadConfigOverview(); + } else { + alert('Error: ' + response.message); + } + }, + error: function(xhr, status, error) { + console.error('Error deleting memory:', error); + alert('Error deleting memory: ' + (xhr.responseJSON ? xhr.responseJSON.message : error)); + } + }); + } + // ===== Shutdown Method ===== shutdown() { diff --git a/src/serena/resources/dashboard/index.html b/src/serena/resources/dashboard/index.html index ccbf972d..e8cee24b 100644 --- a/src/serena/resources/dashboard/index.html +++ b/src/serena/resources/dashboard/index.html @@ -185,6 +185,33 @@ + + + + + +