From 85af1a3fd621ecbf8cb3f7aba8f3a9de44b3f6a6 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Thu, 30 Oct 2025 17:08:44 +0100 Subject: [PATCH] Support for adding memories and copying logs through dashboard --- src/serena/dashboard.py | 2 - src/serena/resources/dashboard/dashboard.css | 39 +++++ src/serena/resources/dashboard/dashboard.js | 155 +++++++++++++++++-- src/serena/resources/dashboard/index.html | 35 ++++- 4 files changed, 208 insertions(+), 23 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 4628d6b6..f6c65db5 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -425,7 +425,6 @@ class SerenaDashboardAPI: # add_language is already thread-safe self._agent.add_language(language) - def _remove_language(self, request_remove_language: RequestRemoveLanguage) -> None: from solidlsp.ls_config import Language @@ -436,7 +435,6 @@ class SerenaDashboardAPI: # remove_language is already thread-safe self._agent.remove_language(language) - @staticmethod def _find_first_free_port(start_port: int) -> int: port = start_port diff --git a/src/serena/resources/dashboard/dashboard.css b/src/serena/resources/dashboard/dashboard.css index da2e17af..8fd4a71f 100644 --- a/src/serena/resources/dashboard/dashboard.css +++ b/src/serena/resources/dashboard/dashboard.css @@ -832,3 +832,42 @@ body { flex-wrap: wrap; gap: 8px; } + +/* Copy Logs Button */ +.copy-logs-btn { + position: absolute; + top: 15px; + right: 15px; + z-index: 10; + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + background-color: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--text-primary); + cursor: pointer; + opacity: 0.8; + transition: opacity 0.2s ease, background-color 0.2s ease; + font-size: 13px; + font-weight: 500; +} + +.copy-logs-btn:hover { + opacity: 1; + background-color: var(--border-color); +} + +.copy-logs-btn svg { + flex-shrink: 0; +} + +.copy-logs-text { + display: none; + white-space: nowrap; +} + +.copy-logs-btn:hover .copy-logs-text { + display: inline; +} diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index f7a84415..84165cd5 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -72,7 +72,7 @@ class Dashboard { // jQuery elements this.$logContainer = $('#log-container'); this.$errorContainer = $('#error-container'); - this.$loadButton = $('#load-logs'); + this.$copyLogsBtn = $('#copy-logs-btn'); this.$menuToggle = $('#menu-toggle'); this.$menuDropdown = $('#menu-dropdown'); this.$menuShutdown = $('#menu-shutdown'); @@ -111,6 +111,12 @@ class Dashboard { this.$deleteMemoryOkBtn = $('#delete-memory-ok-btn'); this.$deleteMemoryCancelBtn = $('#delete-memory-cancel-btn'); this.$modalCloseDeleteMemory = $('.modal-close-delete-memory'); + this.$createMemoryModal = $('#create-memory-modal'); + this.$createMemoryProjectName = $('#create-memory-project-name'); + this.$createMemoryNameInput = $('#create-memory-name-input'); + this.$createMemoryCreateBtn = $('#create-memory-create-btn'); + this.$createMemoryCancelBtn = $('#create-memory-cancel-btn'); + this.$modalCloseCreateMemory = $('.modal-close-create-memory'); // Chart references this.countChart = null; @@ -119,7 +125,7 @@ class Dashboard { this.outputChart = null; // Register event handlers - this.$loadButton.click(this.loadLogs.bind(this)); + this.$copyLogsBtn.click(this.copyLogs.bind(this)); this.$menuShutdown.click(function(e) { e.preventDefault(); self.shutdown(); @@ -141,6 +147,15 @@ class Dashboard { this.$deleteMemoryOkBtn.click(this.confirmDeleteMemoryOk.bind(this)); this.$deleteMemoryCancelBtn.click(this.closeDeleteMemoryModal.bind(this)); this.$modalCloseDeleteMemory.click(this.closeDeleteMemoryModal.bind(this)); + this.$createMemoryCreateBtn.click(this.createMemoryFromModal.bind(this)); + this.$createMemoryCancelBtn.click(this.closeCreateMemoryModal.bind(this)); + this.$modalCloseCreateMemory.click(this.closeCreateMemoryModal.bind(this)); + this.$createMemoryNameInput.keypress(function(e) { + if (e.which === 13) { // Enter key + e.preventDefault(); + self.createMemoryFromModal(); + } + }); // Page navigation $('[data-page]').click(function(e) { @@ -181,6 +196,12 @@ class Dashboard { } }); + this.$createMemoryModal.click(function(e) { + if ($(e.target).hasClass('modal')) { + self.closeCreateMemoryModal(); + } + }); + // Collapsible sections $('.collapsible-header').click(function() { const $header = $(this); @@ -205,6 +226,8 @@ class Dashboard { self.closeEditMemoryModal(); } else if (self.$deleteMemoryModal.is(':visible')) { self.closeDeleteMemoryModal(); + } else if (self.$createMemoryModal.is(':visible')) { + self.closeCreateMemoryModal(); } } }); @@ -403,20 +426,25 @@ class Dashboard { html += ''; html += ''; - // Available memories - collapsible (only show if memories exist) - if (config.available_memories && config.available_memories.length > 0) { + // Available memories - collapsible (show if memories exist or if project exists) + if (config.active_project && config.active_project.name) { html += '
'; html += '

'; - html += 'Available Memories (' + config.available_memories.length + ')'; + const memoryCount = (config.available_memories && config.available_memories.length) || 0; + html += 'Available Memories (' + memoryCount + ')'; html += '▼'; html += '

'; html += '
'; - config.available_memories.forEach(function(memory) { - html += '
'; - html += memory; - html += '×'; - html += '
'; - }); + if (config.available_memories && config.available_memories.length > 0) { + config.available_memories.forEach(function(memory) { + html += '
'; + html += memory; + html += '×'; + html += '
'; + }); + } + // Add Create Memory button + html += ''; html += '
'; html += '
'; } @@ -456,6 +484,9 @@ class Dashboard { self.confirmDeleteMemory(memoryName); }); + // Attach event handler for create memory button + $('#create-memory-btn').click(this.openCreateMemoryModal.bind(this)); + // Re-attach collapsible handler for the newly created tools header $('#tools-header').click(function() { const $header = $(this); @@ -598,12 +629,34 @@ class Dashboard { document.title = activeProject ? `${activeProject} – Serena Dashboard` : 'Serena Dashboard'; } + copyLogs() { + const logText = this.$logContainer.text(); + + if (!logText) { + alert('No logs to copy'); + return; + } + + // Use the Clipboard API to copy text + navigator.clipboard.writeText(logText).then(() => { + // Visual feedback - temporarily change icon to grey checkmark + const originalHtml = this.$copyLogsBtn.html(); + const checkmarkSvg = 'copy logs'; + this.$copyLogsBtn.html(checkmarkSvg); + + setTimeout(() => { + this.$copyLogsBtn.html(originalHtml); + }, 1500); + }).catch(err => { + console.error('Failed to copy logs:', err); + alert('Failed to copy logs to clipboard'); + }); + } + loadLogs() { console.log("Loading logs"); let self = this; - // Disable button and show loading state - self.$loadButton.prop('disabled', true).text('Loading...'); self.$errorContainer.empty(); // Make API call @@ -643,10 +696,6 @@ class Dashboard { console.error('Error loading logs:', error); self.$errorContainer.html('
Error loading logs: ' + (xhr.responseJSON ? xhr.responseJSON.detail : error) + '
'); - }, - complete: function() { - // Re-enable button - self.$loadButton.prop('disabled', false).text('Reload Log'); } }); } @@ -1407,6 +1456,78 @@ class Dashboard { }); } + openCreateMemoryModal() { + // Set project name in modal + this.$createMemoryProjectName.text(this.activeProjectName || 'Unknown'); + + // Clear the input field + this.$createMemoryNameInput.val(''); + + // Show modal + this.$createMemoryModal.fadeIn(200); + + // Focus on the input field + setTimeout(() => { + this.$createMemoryNameInput.focus(); + }, 250); + } + + closeCreateMemoryModal() { + this.$createMemoryModal.fadeOut(200); + this.$createMemoryNameInput.val(''); + this.$createMemoryCreateBtn.prop('disabled', false).text('Create'); + } + + createMemoryFromModal() { + const memoryName = this.$createMemoryNameInput.val().trim(); + + if (!memoryName) { + alert('Please enter a memory name'); + return; + } + + // Validate memory name (alphanumeric and underscores only) + if (!/^[a-zA-Z0-9_]+$/.test(memoryName)) { + alert('Memory name can only contain letters, numbers, and underscores'); + return; + } + + const self = this; + + // Disable button during request + self.$createMemoryCreateBtn.prop('disabled', true).text('Creating...'); + + $.ajax({ + url: '/save_memory', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ + memory_name: memoryName, + content: '' + }), + success: function(response) { + if (response.status === 'success') { + // Close the create modal + self.closeCreateMemoryModal(); + // Reload config to show the new memory + self.loadConfigOverview(); + // Open the edit modal for the newly created memory + setTimeout(() => { + self.openEditMemoryModal(memoryName); + }, 500); + } else { + alert('Error: ' + response.message); + self.$createMemoryCreateBtn.prop('disabled', false).text('Create'); + } + }, + error: function(xhr, status, error) { + console.error('Error creating memory:', error); + alert('Error creating memory: ' + (xhr.responseJSON ? xhr.responseJSON.message : error)); + self.$createMemoryCreateBtn.prop('disabled', false).text('Create'); + } + }); + } + // ===== Shutdown Method ===== shutdown() { diff --git a/src/serena/resources/dashboard/index.html b/src/serena/resources/dashboard/index.html index 022e9f29..c5dfb407 100644 --- a/src/serena/resources/dashboard/index.html +++ b/src/serena/resources/dashboard/index.html @@ -106,11 +106,17 @@ @@ -209,6 +215,27 @@ + + + +