diff --git a/CHANGELOG.md b/CHANGELOG.md index 49475679..e0d94210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,10 @@ Status of the `main` branch. Changes prior to the next official version change w * Major extensions to the dashboard, which now serves as a central web interface for Serena * View current configuration * View news which can be marked as read - * View the executions, with the possibility to cancel running/scheduled executions + * View the executions, with the possibility to cancel running/scheduled executions * View tool usage statistics * View and create memories and edit the serena configuration file + * Log page now has save (downloads a snapshot) and clear (resets log view) buttons alongside the existing copy button * New two-tier caching of language server document symbols and considerable performance improvements surrounding symbol retrieval/indexing * Various fixes related to indexing, special paths and determination of ignored paths * Decreased `TOOL_DEFAULT_MAX_ANSWER_LENGTH` to be in accordance with (below) typical max-tokens configurations diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index c324ae98..db7ab4a7 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -184,6 +184,11 @@ class SerenaDashboardAPI: self._clear_tool_stats() return {"status": "cleared"} + @self._app.route("/clear_logs", methods=["POST"]) + def clear_logs() -> dict[str, str]: + self._memory_log_handler.clear_log_messages() + return {"status": "cleared"} + @self._app.route("/get_token_count_estimator_name", methods=["GET"]) def get_token_count_estimator_name() -> dict[str, str]: estimator_name = self._tool_usage_stats.token_estimator_name if self._tool_usage_stats else "unknown" diff --git a/src/serena/resources/dashboard/dashboard.css b/src/serena/resources/dashboard/dashboard.css index e47fc2c2..1c0a8a47 100644 --- a/src/serena/resources/dashboard/dashboard.css +++ b/src/serena/resources/dashboard/dashboard.css @@ -1277,12 +1277,18 @@ body { gap: 8px; } -/* Copy Logs Button */ -.copy-logs-btn { +/* Log Action Buttons (Save, Copy, Clear) */ +.log-action-buttons { position: absolute; top: 15px; - right: 15px; + right: 20px; z-index: 10; + display: flex; + gap: 8px; + align-items: center; +} + +.log-action-btn { display: flex; align-items: center; gap: 6px; @@ -1293,25 +1299,36 @@ body { color: var(--text-primary); cursor: pointer; opacity: 0.8; - transition: opacity 0.2s ease, background-color 0.2s ease; + transition: opacity 0.2s ease, background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease; font-size: 13px; font-weight: 500; } -.copy-logs-btn:hover { +.log-action-btn:hover { opacity: 1; background-color: var(--border-color); } -.copy-logs-btn svg { +.log-action-btn svg { flex-shrink: 0; } -.copy-logs-text { +.log-action-btn-text { display: none; white-space: nowrap; } -.copy-logs-btn:hover .copy-logs-text { +.log-action-btn:hover .log-action-btn-text { display: inline; } + +.log-action-btn:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.log-action-btn-danger:hover { + background-color: var(--border-color); + border-color: rgba(220, 53, 53, 0.9); + color: var(--log-error); +} diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index 934fd8b9..2b4af7fa 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -211,7 +211,9 @@ class Dashboard { // jQuery elements this.$logContainer = $('#log-container'); this.$errorContainer = $('#error-container'); + this.$saveLogsBtn = $('#save-logs-btn'); this.$copyLogsBtn = $('#copy-logs-btn'); + this.$clearLogsBtn = $('#clear-logs-btn'); this.$menuToggle = $('#menu-toggle'); this.$menuDropdown = $('#menu-dropdown'); this.$menuShutdown = $('#menu-shutdown'); @@ -278,7 +280,9 @@ class Dashboard { this.outputChart = null; // Register event handlers + this.$saveLogsBtn.click(this.saveLogs.bind(this)); this.$copyLogsBtn.click(this.copyLogs.bind(this)); + this.$clearLogsBtn.click(this.clearLogs.bind(this)); this.$menuShutdown.click(function (e) { e.preventDefault(); self.shutdown(); @@ -1112,19 +1116,39 @@ class Dashboard { document.title = activeProject ? `${activeProject} – Serena Dashboard` : 'Serena Dashboard'; } + updateLogButtons(hasLogs) { + this.$saveLogsBtn.prop('disabled', !hasLogs); + this.$copyLogsBtn.prop('disabled', !hasLogs); + this.$clearLogsBtn.prop('disabled', !hasLogs); + } + + saveLogs() { + const logText = this.$logContainer.text(); + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + const blob = new Blob([logText], {type: 'text/plain'}); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `serena-logs-${timestamp}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + const originalHtml = this.$saveLogsBtn.html(); + const checkmarkSvg = 'save logs'; + this.$saveLogsBtn.html(checkmarkSvg); + setTimeout(() => { this.$saveLogsBtn.html(originalHtml); }, 1500); + } + 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'; + const checkmarkSvg = 'copy logs'; this.$copyLogsBtn.html(checkmarkSvg); setTimeout(() => { @@ -1132,7 +1156,27 @@ class Dashboard { }, 1500); }).catch(err => { console.error('Failed to copy logs:', err); - alert('Failed to copy logs to clipboard'); + }); + } + + clearLogs() { + let self = this; + $.ajax({ + url: '/clear_logs', + type: 'POST', + success: function () { + self.$logContainer.empty(); + self.currentMaxIdx = -1; + self.updateLogButtons(false); + + const originalHtml = self.$clearLogsBtn.html(); + const checkmarkSvg = 'clear logs'; + self.$clearLogsBtn.html(checkmarkSvg); + setTimeout(() => { self.$clearLogsBtn.html(originalHtml); }, 1500); + }, + error: function (xhr, status, error) { + console.error('Failed to clear logs:', error); + } }); } @@ -1166,6 +1210,7 @@ class Dashboard { $('#log-container').html('
No log messages found.
'); } + self.updateLogButtons(response.messages && response.messages.length > 0); self.updateTitle(response.active_project); // Start periodic polling for new logs @@ -1206,6 +1251,8 @@ class Dashboard { // Update max_idx self.currentMaxIdx = response.max_idx || self.currentMaxIdx; + self.updateLogButtons(true); + // Auto-scroll to bottom if user was already at bottom if (wasAtBottom) { logContainer.scrollTop = logContainer.scrollHeight; diff --git a/src/serena/resources/dashboard/index.html b/src/serena/resources/dashboard/index.html index cd5fb4b7..1fbf0a45 100644 --- a/src/serena/resources/dashboard/index.html +++ b/src/serena/resources/dashboard/index.html @@ -156,14 +156,36 @@ diff --git a/src/serena/util/logging.py b/src/serena/util/logging.py index 4ca6e770..0d4f0970 100644 --- a/src/serena/util/logging.py +++ b/src/serena/util/logging.py @@ -64,6 +64,9 @@ class MemoryLogHandler(logging.Handler): def get_log_messages(self, from_idx: int = 0) -> LogMessages: return self._log_buffer.get_log_messages(from_idx=from_idx) + def clear_log_messages(self) -> None: + self._log_buffer.clear() + class LogBuffer: """ @@ -87,6 +90,11 @@ class LogBuffer: excess = len(self._log_messages) - self._max_messages self._log_messages = self._log_messages[excess:] + def clear(self) -> None: + with self._lock: + self._log_messages = [] + self._max_idx = -1 + def get_log_messages(self, from_idx: int = 0) -> LogMessages: """ :param from_idx: the 0-based index of the first log message to return.