Adding news support in dashboard, WIP

This commit is contained in:
Michael Panchenko committed 2026-01-10 00:52:31 +07:00
1 parent eb54e834b6
commit 8f89bc32b1
6 files changed
+251 -2

No files matched your search

+4
View File
@@ -80,6 +80,10 @@ class SerenaPaths:
If a name of a mode matches a name of a mode in SERENAS_OWN_MODES_YAML_DIR,
the user mode will override the default mode definition.
"""
self.news_snippet_id_file: str = os.path.join(self.serena_user_home_dir, "last_read_news_snippet_id.txt")
"""
file containing the ID of the last read news snippet
"""
def get_next_log_file_path(self, prefix: str) -> str:
"""
+33 -2
View File
@@ -2,6 +2,7 @@ import os
import socket
import threading
from collections.abc import Callable
from pathlib import Path
from typing import TYPE_CHECKING, Any, Self
from flask import Flask, Response, request, send_from_directory
@@ -9,8 +10,8 @@ from pydantic import BaseModel
from sensai.util import logging
from serena.analytics import ToolUsageStats
from serena.config.serena_config import LanguageBackend
from serena.constants import SERENA_DASHBOARD_DIR
from serena.config.serena_config import LanguageBackend, SerenaPaths
from serena.constants import REPO_ROOT, SERENA_DASHBOARD_DIR
from serena.task_executor import TaskExecutor
from serena.util.logging import MemoryLogHandler
@@ -317,6 +318,36 @@ class SerenaDashboardAPI:
except Exception as e:
return {"status": "error", "message": str(e)}
@self._app.route("/news_snippet_ids", methods=["GET"])
def get_news_snippet_ids() -> dict[str, str | list[int]]:
def _get_unread_news_ids() -> list[int]:
all_news_files = (Path(REPO_ROOT) / "src" / "serena" / "resources" / "dashboard" / "news").glob("*.html")
all_news_ids = sorted([int(f.stem) for f in all_news_files])
news_snippet_id_file = SerenaPaths().news_snippet_id_file
if not os.path.exists(news_snippet_id_file):
return all_news_ids
with open(news_snippet_id_file, encoding="utf-8") as f:
last_read_news_id = int(f.read().strip())
return [news_id for news_id in all_news_ids if news_id > last_read_news_id]
try:
unread_news_ids = _get_unread_news_ids()
return {"news_snippet_ids": unread_news_ids, "status": "success"}
except Exception as e:
return {"status": "error", "message": str(e)}
@self._app.route("/mark_news_snippet_as_read", methods=["POST"])
def mark_news_snippet_as_read() -> dict[str, str]:
try:
request_data = request.get_json()
news_snippet_id = int(request_data.get("news_snippet_id"))
news_snippet_id_file = SerenaPaths().news_snippet_id_file
with open(news_snippet_id_file, "w", encoding="utf-8") as f:
f.write(str(news_snippet_id))
return {"status": "success", "message": f"Marked news snippet {news_snippet_id} as read"}
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 []
@@ -49,6 +49,96 @@ html {
--stats-header: #3a3a3a;
}
.news-section {
background: var(--bg-secondary);
padding: 20px;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
margin-bottom: 25px;
}
.news-section h2 {
margin: 0 0 20px 0;
font-size: 18px;
color: var(--text-primary);
}
.news-item {
padding: 20px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
margin-bottom: 15px;
background: var(--bg-primary);
position: relative;
}
.news-item:last-child {
margin-bottom: 0;
}
.news-item h3 {
margin: 0 0 10px 0;
font-size: 16px;
color: var(--text-primary);
}
.news-item .date {
color: var(--text-muted);
font-size: 13px;
margin: 0 0 15px 0;
}
.news-item p {
margin: 10px 0;
line-height: 1.6;
color: var(--text-secondary);
}
.news-item ul {
margin: 10px 0;
padding-left: 25px;
color: var(--text-secondary);
}
.news-item ul li {
margin: 8px 0;
line-height: 1.5;
}
.news-item strong {
color: var(--text-primary);
}
.news-mark-read {
position: absolute;
top: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 13px;
color: var(--text-muted);
user-select: none;
}
.news-mark-read:hover {
color: var(--text-primary);
}
.news-mark-read input[type="checkbox"] {
cursor: pointer;
width: 18px;
height: 18px;
}
.news-no-items {
color: var(--text-muted);
font-style: italic;
text-align: center;
padding: 20px;
}
body {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
margin: 0;
+103
View File
@@ -268,6 +268,8 @@ class Dashboard {
this.$editSerenaConfigSaveBtn = $('#edit-serena-config-save-btn');
this.$editSerenaConfigCancelBtn = $('#edit-serena-config-cancel-btn');
this.$modalCloseEditSerenaConfig = $('.modal-close-edit-serena-config');
this.$newsSection = $('#news-section');
this.$newsDisplay = $('#news-display');
// Chart references
this.countChart = null;
@@ -401,6 +403,7 @@ class Dashboard {
// Initialize the application
this.loadToolNames().then(function () {
// Start on overview page
self.loadNews();
self.loadConfigOverview();
self.startConfigPolling();
self.startExecutionsPolling();
@@ -454,6 +457,7 @@ class Dashboard {
// Start appropriate polling for the page
if (page === 'overview') {
this.loadNews();
this.loadConfigOverview();
this.startConfigPolling();
this.startExecutionsPolling();
@@ -1877,6 +1881,105 @@ class Dashboard {
});
}
// ===== News Methods =====
loadNews() {
let self = this;
console.log('Loading news...');
$.ajax({
url: '/news_snippet_ids',
type: 'GET',
success: function(response) {
console.log('News snippet IDs response:', response);
if (response.status === 'success' && response.news_snippet_ids && response.news_snippet_ids.length > 0) {
console.log('Displaying news with IDs:', response.news_snippet_ids);
self.displayNews(response.news_snippet_ids);
} else {
console.log('No unread news, hiding section');
self.$newsSection.hide();
}
},
error: function(xhr, status, error) {
console.error('Error loading news snippet IDs:', error);
self.$newsSection.hide();
}
});
}
displayNews(newsIds) {
let self = this;
console.log('displayNews called with:', newsIds);
// Sort newest first (descending order)
newsIds.sort((a, b) => b - a);
if (newsIds.length === 0) {
console.log('No news items to display.');
self.$newsSection.hide();
return;
}
self.$newsSection.show();
self.$newsDisplay.empty();
console.log('Displaying ' + newsIds.length + ' news items.');
// Load each news snippet HTML
let loadedCount = 0;
newsIds.forEach(function(newsId) {
$.ajax({
url: '/dashboard/news/' + newsId + '.html',
type: 'GET',
success: function(html) {
// Wrap the HTML in a container with a checkbox
let $newsContainer = $('<div class="news-container">').attr('data-news-id', newsId);
let $newsContent = $(html);
// Add checkbox for marking as read
let $markRead = $('<div class="news-mark-read">');
let $checkbox = $('<input type="checkbox">').attr('id', 'news-read-' + newsId);
let $label = $('<label>').attr('for', 'news-read-' + newsId).text('Mark as read');
$markRead.append($checkbox).append($label);
$newsContent.append($markRead);
$newsContainer.append($newsContent);
self.$newsDisplay.append($newsContainer);
// Bind checkbox event
$checkbox.on('change', function() {
if ($(this).is(':checked')) {
self.markNewsAsRead(newsId);
}
});
loadedCount++;
},
error: function(xhr, status, error) {
console.error('Error loading news snippet ' + newsId + ':', error);
loadedCount++;
}
});
});
}
markNewsAsRead(newsId) {
let self = this;
$.ajax({
url: '/mark_news_snippet_as_read',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ news_snippet_id: newsId }),
success: function(response) {
if (response.status === 'success') {
// Reload news to show updated list
self.loadNews();
} else {
console.error('Error marking news as read:', response.message);
}
},
error: function(xhr, status, error) {
console.error('Error marking news as read:', error);
}
});
}
// ===== Serena Config Editing Methods =====
openEditSerenaConfigModal() {
@@ -55,6 +55,13 @@
<div class="overview-container">
<div class="overview-left">
<section id="news-section" class="news-section" style="display:none">
<h2>What's New</h2>
<div id="news-display">
<div class="loading">Loading news...</div>
</div>
</section>
<section class="config-section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h2 style="margin: 0;">Current Configuration</h2>
@@ -0,0 +1,14 @@
<div class="news-item">
<h3>New Type Hierarchy and Documentation Tools</h3>
<p class="date">January 9, 2026</p>
<p>
We're excited to announce the introduction of powerful new tools for code navigation and understanding:
</p>
<ul>
<li><strong>Type Hierarchy Tool:</strong> Navigate and explore type relationships in your codebase with ease. View superclasses, subclasses, and implementation hierarchies.</li>
<li><strong>Documentation Tools:</strong> Access comprehensive documentation directly from your code, including API references, examples, and usage patterns.</li>
</ul>
<p>
These tools enhance Serena's code intelligence capabilities, making it easier to understand complex codebases and navigate large projects efficiently.
</p>
</div>