Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions lib/public/css/diagnostics.css
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,38 @@
min-height: 0;
}

/* Header actions group: Clear all + Close, side by side (parity with the
Notifications panel's .notif-banner-clear-all placement). */
.diagnostics-panel-header-actions {
display: flex;
align-items: center;
gap: 6px;
}

.diagnostics-panel-clear-all {
display: inline-flex;
align-items: center;
padding: 3px 8px;
background: none;
border: 1px solid var(--border);
border-radius: 999px;
font-family: var(--font-mono, monospace);
font-size: 9.5px;
font-weight: 400;
letter-spacing: 0.06em;
color: var(--text-muted);
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
}
.diagnostics-panel-clear-all:hover {
color: var(--text);
border-color: var(--border-bright, var(--border));
}
.diagnostics-panel-clear-all:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}

/* --------------------------------------------------------
Diagnostic items
-------------------------------------------------------- */
Expand Down Expand Up @@ -114,6 +146,31 @@
letter-spacing: 0.08em;
}

/* Per-entry dismiss (x) control, pushed to the end of the meta row. */
.diagnostics-item-dismiss {
margin-left: auto;
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 2px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
flex-shrink: 0;
transition: color 0.15s, background 0.15s;
}
.diagnostics-item-dismiss:hover {
color: var(--text);
background: rgba(var(--overlay-rgb), 0.06);
}
.diagnostics-item-dismiss:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.diagnostics-item-dismiss .lucide { width: 12px; height: 12px; }

/* Message text */
.diagnostics-message {
font-size: 12px;
Expand Down
5 changes: 4 additions & 1 deletion lib/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,10 @@ <h1 id="hub-greeting-text"></h1>
<div id="diagnostics-panel" class="hidden" role="region" aria-label="Diagnostics" aria-expanded="false" tabindex="-1">
<div class="usage-panel-header">
<span>Diagnostics</span>
<button class="diagnostics-panel-close" aria-label="Close diagnostics panel"><i data-lucide="x"></i></button>
<div class="diagnostics-panel-header-actions">
<button class="diagnostics-panel-clear-all" aria-label="Clear all diagnostics">Clear all</button>
<button class="diagnostics-panel-close" aria-label="Close diagnostics panel"><i data-lucide="x"></i></button>
</div>
</div>
<div class="usage-panel-body diagnostics-panel-body">
<div id="diagnostics-panel-list">
Expand Down
99 changes: 95 additions & 4 deletions lib/public/modules/diagnostics.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,31 @@
// initDiagnostics() — call once on page load (wires up panel button)
// addDiagnostic(msg) — call from processMessage case 'diagnostic'
// formatDiagnosticSource(source) — pure helper, exported for tests
//
// Dismiss / clear-all (parity with app-notifications.js banner dismiss +
// clear-all, see initAppNotifications/dismissNotif/clearAllBanners there):
// diagnostics are client-side-only session state — there is no backend
// message type or persistence for them (unlike notifications, which round-
// trip a notification_dismiss/_dismiss_all message over the websocket).
// Dismissing here only ever mutates the in-memory `diagnostics` array and
// the DOM; a dismissed diagnostic can legitimately reappear if the next
// settings-preflight run re-emits the same underlying condition — that is
// expected and not a bug in this module.

import { iconHtml, refreshIcons } from './icons.js';
import { formatDiagnosticSource as _formatSource } from './diagnostic-format.js';

// --- Module state ---
var diagnostics = []; // [{severity, source, message, actionable, ts}]
var diagnostics = []; // [{id, severity, source, message, actionable, ts}]
var panelEl = null;
var panelListEl = null;
var panelToggleBtn = null;
var panelBadgeEl = null;
var clearAllBtn = null;

// Stable per-entry identity so a specific rendered item can be located and
// removed on dismiss. The array otherwise carries no unique key.
var _nextDiagnosticId = 1;

// Severity priority (higher = more prominent).
var SEVERITY_ORDER = { error: 2, warning: 1, info: 0 };
Expand Down Expand Up @@ -61,6 +76,7 @@ export function initDiagnostics() {
panelListEl = document.getElementById("diagnostics-panel-list");
panelToggleBtn = document.getElementById("diagnostics-panel-btn");
panelBadgeEl = document.getElementById("diagnostics-panel-badge");
clearAllBtn = panelEl ? panelEl.querySelector(".diagnostics-panel-clear-all") : null;

if (!panelEl || !panelListEl || !panelToggleBtn) return;

Expand All @@ -80,6 +96,14 @@ export function initDiagnostics() {
});
}

// Clear-all button (parity with app-notifications.js clearAllBanners()).
if (clearAllBtn) {
clearAllBtn.addEventListener("click", function () {
clearAllDiagnostics();
if (closeBtn) closeBtn.focus();
});
}

// Toggle button in the topbar.
panelToggleBtn.addEventListener("click", function () {
if (panelEl.classList.contains("hidden")) {
Expand Down Expand Up @@ -125,6 +149,7 @@ export function addDiagnostic(msg) {
message: msg.message || "",
actionable: msg.actionable || null,
ts: now,
id: _nextDiagnosticId++,
};
diagnostics.push(entry);

Expand Down Expand Up @@ -266,11 +291,11 @@ function _appendEntry(entry) {
if (!panelListEl) return;

// Remove the empty-state placeholder if present.
var emptyEl = panelListEl.querySelector(".diagnostics-empty");
if (emptyEl) emptyEl.parentNode.removeChild(emptyEl);
_removeEmptyPlaceholder();

var item = document.createElement("div");
item.className = "diagnostics-item diagnostics-item-" + entry.severity;
item.setAttribute("data-diag-id", String(entry.id));

// Icon strip: severity pill.
var meta = document.createElement("div");
Expand All @@ -285,8 +310,17 @@ function _appendEntry(entry) {
srcEl.className = "diagnostics-source";
srcEl.textContent = _formatSource(entry.source, entry.scope);

var dismissBtn = document.createElement("button");
dismissBtn.className = "diagnostics-item-dismiss";
dismissBtn.setAttribute("aria-label", "Dismiss this diagnostic");
dismissBtn.innerHTML = iconHtml("x");
dismissBtn.addEventListener("click", function () {
dismissDiagnostic(entry.id);
});

meta.appendChild(sevEl);
meta.appendChild(srcEl);
meta.appendChild(dismissBtn);

var msgEl = document.createElement("div");
msgEl.className = "diagnostics-message";
Expand All @@ -311,15 +345,72 @@ function _appendEntry(entry) {
actionEl.appendChild(fixIcon);
actionEl.appendChild(fixText);
item.appendChild(actionEl);
refreshIcons();
}

panelListEl.appendChild(item);
refreshIcons();

// Scroll the panel to the newest entry.
panelListEl.scrollTop = panelListEl.scrollHeight;
}

// ============================================================
// Dismiss / clear-all (client-side only — see module header note)
// ============================================================

function _removeEmptyPlaceholder() {
if (!panelListEl) return;
var emptyEl = panelListEl.querySelector(".diagnostics-empty");
if (emptyEl) emptyEl.parentNode.removeChild(emptyEl);
}

// Restore the empty-state placeholder that _appendEntry removes on first
// append. Mirrors the markup baked into index.html so a fresh page load and
// a fully-cleared panel render identically.
function _restoreEmptyPlaceholder() {
if (!panelListEl) return;
if (panelListEl.querySelector(".diagnostics-empty")) return;
var emptyEl = document.createElement("div");
emptyEl.className = "diagnostics-empty";
emptyEl.textContent = "No diagnostics this session.";
panelListEl.appendChild(emptyEl);
}

/**
* Remove a single diagnostic by id from both the in-memory array and the
* rendered panel list, then refresh the badge. No-op if the id is unknown
* (e.g. a stale dismiss click racing a clear-all).
*/
function dismissDiagnostic(id) {
var idx = -1;
for (var i = 0; i < diagnostics.length; i++) {
if (diagnostics[i].id === id) { idx = i; break; }
}
if (idx === -1) return;
diagnostics.splice(idx, 1);

if (panelListEl) {
var itemEl = panelListEl.querySelector('[data-diag-id="' + id + '"]');
if (itemEl && itemEl.parentNode) itemEl.parentNode.removeChild(itemEl);
}

if (diagnostics.length === 0) _restoreEmptyPlaceholder();
_updateBadge();
}

/**
* Empty the diagnostics array and panel list, restore the empty-state
* placeholder, and hide the badge. Parity with clearAllBanners() in
* app-notifications.js, minus the websocket round-trip — diagnostics have
* no server-side dismiss contract (see module header note).
*/
function clearAllDiagnostics() {
diagnostics = [];
if (panelListEl) panelListEl.innerHTML = "";
_restoreEmptyPlaceholder();
_updateBadge();
}

// ============================================================
// Badge
// ============================================================
Expand Down
Loading
Loading