feat: add save and clear log buttons to dashboard log page (#1050)

This commit is contained in:
aleksbeloushkin-srb authored and GitHub committed 2026-02-19 18:52:30 +01:00
1 parent 3791c1b050
commit db9980f0ed
6 files changed
+124 -24

No files matched your search

+2 -1
View File
@@ -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
+5
View File
@@ -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"
+25 -8
View File
@@ -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);
}
+54 -7
View File
@@ -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 = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#888" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg><span class="log-action-btn-text">save logs</span>';
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 = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#888" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg><span class="copy-logs-text">copy logs</span>';
const checkmarkSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#888" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg><span class="log-action-btn-text">copy logs</span>';
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 = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#888" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg><span class="log-action-btn-text">clear logs</span>';
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('<div class="loading">No log messages found.</div>');
}
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;
+30 -8
View File
@@ -156,14 +156,36 @@
<div id="page-logs" class="page-view" style="display:none">
<div id="error-container"></div>
<div style="position: relative;">
<button id="copy-logs-btn" class="copy-logs-btn" title="Copy logs">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<span class="copy-logs-text">copy logs</span>
</button>
<div class="log-action-buttons">
<button id="copy-logs-btn" class="log-action-btn" title="Copy logs" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<span class="log-action-btn-text">copy logs</span>
</button>
<button id="save-logs-btn" class="log-action-btn" title="Save logs to file" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
<span class="log-action-btn-text">save logs</span>
</button>
<button id="clear-logs-btn" class="log-action-btn log-action-btn-danger" title="Clear logs" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path>
<path d="M10 11v6"></path>
<path d="M14 11v6"></path>
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"></path>
</svg>
<span class="log-action-btn-text">clear logs</span>
</button>
</div>
<div id="log-container" class="log-container"></div>
</div>
</div>
+8
View File
@@ -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.