mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 16:15:33 +00:00
feat: add save and clear log buttons to dashboard log page (#1050)
This commit is contained in:
1 parent
3791c1b050
commit
db9980f0ed
6 files changed
+124
-24
No files matched your search
+2
-1
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user