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
2 changes: 1 addition & 1 deletion src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function buildViewerCsp(nonce: string): string {
"script-src-attr 'none'",
"style-src 'unsafe-inline'",
"connect-src 'self' http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* wss://localhost:* wss://127.0.0.1:*",
"img-src 'self'",
"img-src 'self' data:",
"font-src 'self'",
].join("; ");
}
185 changes: 144 additions & 41 deletions src/viewer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>agentmemory viewer</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='10' fill='%23111111'/%3E%3Ctext x='32' y='41' text-anchor='middle' font-family='Arial,sans-serif' font-size='24' font-weight='700' fill='%232D6A4F'%3EAM%3C/text%3E%3C/svg%3E">
<!-- Removed Google Fonts <link> in #323: the viewer CSP is strict
(default-src 'none', style-src 'unsafe-inline', font-src 'self')
and external stylesheets from fonts.googleapis.com were blocked,
Expand Down Expand Up @@ -103,12 +104,21 @@
align-items: center;
justify-content: space-between;
background: var(--bg);
flex: 0 0 auto;
position: relative;
z-index: 3;
}
.app-header .brand {
display: flex;
align-items: baseline;
gap: 10px;
color: inherit;
text-decoration: none;
cursor: pointer;
}
.app-header .brand:hover h1,
.app-header .brand:focus-visible h1 { color: var(--accent); }
.app-header .brand:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
.app-header .brand h1 {
font-size: 22px;
color: var(--ink);
Expand Down Expand Up @@ -157,6 +167,9 @@
border-bottom: 1px solid var(--border-light);
background: var(--bg);
overflow-x: auto;
flex: 0 0 auto;
position: relative;
z-index: 2;
}
.tab-bar button {
background: none;
Expand Down Expand Up @@ -427,22 +440,46 @@
margin-bottom: 12px;
border-left: 3px solid var(--border-light);
transition: box-shadow 0.15s;
min-width: 0;
max-width: 100%;
overflow: hidden;
}
.obs-card:hover { box-shadow: 3px 3px 0px 0px var(--border-light); }
.obs-card.imp-high { border-left-color: var(--accent); }
.obs-card.imp-med { border-left-color: var(--yellow); }
.obs-card.imp-low { border-left-color: var(--green); }
.obs-card .obs-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 12px;
margin-bottom: 6px;
}
.obs-card .obs-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
gap: 6px;
min-width: 0;
}
.obs-card .obs-meta {
display: flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
white-space: nowrap;
}
.obs-card .obs-type-icon {
flex: 0 0 auto;
}
.obs-card .obs-title {
font-size: 14px;
font-weight: 700;
color: var(--ink);
font-family: var(--font-display);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.obs-card .obs-time {
font-size: 10px;
Expand All @@ -454,6 +491,14 @@
font-size: 13px;
color: var(--ink-muted);
margin-bottom: 6px;
overflow-wrap: anywhere;
word-break: break-word;
}
.obs-card pre {
max-width: 100%;
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
}
.obs-card .obs-facts {
margin: 6px 0 6px 16px;
Expand All @@ -470,6 +515,9 @@
color: var(--blue);
font-family: var(--font-mono);
font-weight: 500;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.tag.file-tag { border-color: var(--green); color: var(--green); }

Expand Down Expand Up @@ -598,7 +646,14 @@
.empty-state .empty-link { color: var(--accent); text-decoration: underline; font-size: 13px; font-style: normal; }

/* Feature flag banner system — compact collapsed by default */
.flag-banners { padding: 0 0 10px 0; }
.flag-banners {
padding: 0 12px 10px 12px;
background: var(--bg);
flex: 0 0 auto;
position: relative;
z-index: 1;
}
.flag-banners:empty { display: none; }
button.flag-summary {
display: flex; align-items: center; gap: 12px;
padding: 8px 14px; border-radius: 4px;
Expand All @@ -609,6 +664,7 @@
cursor: pointer; user-select: none;
width: 100%; text-align: left;
appearance: none;
flex: 1 1 auto;
}
button.flag-summary:hover,
button.flag-summary:focus-visible { background: var(--bg-alt); outline: 2px solid var(--border); outline-offset: 1px; }
Expand All @@ -623,6 +679,8 @@
.flag-list {
display: none; flex-direction: column; gap: 6px;
margin-top: 6px;
max-height: min(30vh, 260px);
overflow-y: auto;
}
.flag-list.open { display: flex; }
.flag-banner {
Expand All @@ -645,11 +703,13 @@
font-family: var(--font-mono); font-size: 10px; color: var(--ink);
white-space: pre-wrap; word-break: break-all;
}
.flag-banner .flag-close {
.flag-close {
background: none; border: none; color: var(--ink-faint); cursor: pointer;
font-size: 16px; line-height: 1; padding: 0 4px; font-family: inherit;
flex: 0 0 auto;
}
.flag-banner .flag-close:hover { color: var(--ink); }
.flag-close:hover,
.flag-close:focus-visible { color: var(--ink); outline: 2px solid var(--border); outline-offset: 1px; }

/* Viewer footer */
.viewer-footer {
Expand Down Expand Up @@ -811,7 +871,7 @@

.timeline-container { position: relative; padding: 20px 0; }
.timeline-container::before { content: ''; position: absolute; left: 50%; top: 0; bottom: 0; width: 2px; background: var(--border-light); transform: translateX(-50%); }
.timeline-item { position: relative; width: 45%; margin-bottom: 20px; }
.timeline-item { position: relative; width: 45%; margin-bottom: 20px; min-width: 0; }
.timeline-item.tl-left { margin-left: 0; margin-right: auto; text-align: right; padding-right: 30px; }
.timeline-item.tl-right { margin-left: auto; margin-right: 0; padding-left: 30px; }
.timeline-dot { position: absolute; width: 12px; height: 12px; border-radius: 50%; top: 16px; z-index: 1; border: 2px solid var(--bg); }
Expand Down Expand Up @@ -874,10 +934,10 @@
</head>
<body>
<div class="app-header">
<div class="brand">
<a class="brand" href="#dashboard" data-tab-link="dashboard" aria-label="Open dashboard">
<h1>agentmemory</h1>
<span class="version">v__AGENTMEMORY_VERSION__</span>
</div>
</a>
<div class="header-right">
<span class="dateline" id="dateline"></span>
<button id="theme-toggle" class="btn" style="font-size:9px;padding:3px 10px;letter-spacing:0.1em;margin-right:8px;" data-action="toggle-theme">DARK</button>
Expand All @@ -886,7 +946,7 @@ <h1>agentmemory</h1>
</div>

<div class="tab-bar" id="tab-bar">
<button class="active" data-tab="dashboard">Dashboard</button>
<button class="active" data-tab="dashboard" aria-current="page">Dashboard</button>
<button data-tab="graph">Graph</button>
<button data-tab="memories">Memories</button>
<button data-tab="timeline">Timeline</button>
Expand Down Expand Up @@ -1002,6 +1062,7 @@ <h1>agentmemory</h1>
task: '&#9745;', other: '&#128196;'
};
var CB_STATE_COLORS = { closed: 'badge-green', open: 'badge-red', 'half-open': 'badge-yellow' };
var TAB_IDS = ['dashboard', 'graph', 'memories', 'timeline', 'sessions', 'lessons', 'actions', 'crystals', 'audit', 'activity', 'profile', 'replay'];

var state = {
activeTab: 'dashboard',
Expand All @@ -1018,6 +1079,7 @@ <h1>agentmemory</h1>
profile: { loaded: false, projects: [], selectedProject: '', data: null },
replay: { loaded: false, sessions: [], selectedId: '', timeline: null, cursor: 0, playing: false, speed: 1, timer: null, startAt: 0, offsetAt: 0 },
flagsConfig: null,
flagsDismissed: {},
ws: null
};

Expand Down Expand Up @@ -1080,17 +1142,52 @@ <h1>agentmemory</h1>
});
}

function switchTab(tab) {
function normalizeTab(tab) {
var normalized = String(tab || '').replace(/^#/, '').toLowerCase();
return TAB_IDS.indexOf(normalized) >= 0 ? normalized : 'dashboard';
}

function tabFromRoute() {
try {
return normalizeTab(decodeURIComponent(window.location.hash.slice(1)));
} catch (_) {
return 'dashboard';
}
}

function updateTabRoute(tab, replace) {
var target = '#' + tab;
if (window.location.hash === target) return;
if (replace) {
history.replaceState(null, '', target);
} else {
history.pushState(null, '', target);
}
}

function switchTab(tab, opts) {
opts = opts || {};
tab = normalizeTab(tab);
if (state.activeTab === 'replay' && tab !== 'replay' && typeof stopReplayTimer === 'function') {
stopReplayTimer();
}
if (!opts.skipRoute) {
updateTabRoute(tab, !!opts.replaceRoute);
}
state.activeTab = tab;
document.querySelectorAll('.tab-bar button').forEach(function(b) {
b.classList.toggle('active', b.dataset.tab === tab);
var isActive = b.dataset.tab === tab;
b.classList.toggle('active', isActive);
if (isActive) {
b.setAttribute('aria-current', 'page');
} else {
b.removeAttribute('aria-current');
}
});
document.querySelectorAll('.view').forEach(function(v) {
v.classList.toggle('active', v.id === 'view-' + tab);
});
if (state.flagsConfig) renderFlagBanners(state.flagsConfig);
loadTab(tab);
}

Expand Down Expand Up @@ -2319,12 +2416,12 @@ <h1>agentmemory</h1>

html += '<div class="obs-card imp-' + impClass + '" style="border-left-color:' + typeColor + ';text-align:left;">';
html += '<div class="obs-head">';
html += '<div style="display:flex;align-items:center;gap:6px;">';
html += '<div class="obs-title-row">';
html += '<span class="obs-type-icon">' + icon + '</span>';
html += '<span class="obs-title">' + esc(title) + '</span>';
html += '<span class="obs-title" title="' + esc(title) + '">' + esc(title) + '</span>';
if (isRaw) html += '<span class="badge badge-muted" style="font-size:8px;margin-left:4px;">raw</span>';
html += '</div>';
html += '<div style="display:flex;align-items:center;gap:8px;">';
html += '<div class="obs-meta">';
if (isCompressed) html += '<span class="obs-importance imp-' + impVal + '" title="Importance: ' + impVal + '/10">' + impVal + '</span>';
html += '<span class="obs-time">' + esc(shortTime(o.timestamp)) + '</span>';
html += '</div></div>';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -3320,26 +3417,39 @@ <h1>agentmemory</h1>
}

document.getElementById('tab-bar').addEventListener('click', function(e) {
if (e.target.tagName === 'BUTTON' && e.target.dataset.tab) {
switchTab(e.target.dataset.tab);
}
var btn = e.target instanceof Element ? e.target.closest('button[data-tab]') : null;
if (btn) switchTab(btn.dataset.tab);
});

// --- Feature flag banners ---------------------------------------------
var FLAG_DISMISS_KEY = 'agentmemory.viewer.flags.dismissed.v1';
function loadDismissedFlags() {
try {
var raw = localStorage.getItem(FLAG_DISMISS_KEY);
return raw ? JSON.parse(raw) : {};
} catch (_) { return {}; }
document.querySelectorAll('[data-tab-link]').forEach(function(link) {
link.addEventListener('click', function(e) {
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
e.preventDefault();
switchTab(link.getAttribute('data-tab-link'));
});
});

function syncTabFromRoute() {
switchTab(tabFromRoute(), { replaceRoute: true });
}
function saveDismissedFlags(d) {
try { localStorage.setItem(FLAG_DISMISS_KEY, JSON.stringify(d)); } catch (_) {}
window.addEventListener('hashchange', syncTabFromRoute);
window.addEventListener('popstate', syncTabFromRoute);

// --- Feature flag banners ---------------------------------------------
function getDismissedFlags() {
if (!state.flagsDismissed) state.flagsDismissed = {};
return state.flagsDismissed;
}
function dismissFlags(keys) {
var dismissed = getDismissedFlags();
keys.forEach(function(key) {
if (key) dismissed[key] = true;
});
}
function renderFlagBanners(cfg) {
var host = document.getElementById('flag-banners');
if (!host) return;
var dismissed = loadDismissedFlags();
var dismissed = getDismissedFlags();
var banners = [];
// Per-flag banner (only for off flags, affecting current tab or dashboard)
(cfg.flags || []).forEach(function(f) {
Expand Down Expand Up @@ -3395,15 +3505,15 @@ <h1>agentmemory</h1>
});
};
var listHtml = banners.map(function(b) {
return '<div class="flag-banner ' + b.kind + '" data-flag="' + b.dismissKey + '">' +
return '<div class="flag-banner ' + b.kind + '" data-flag="' + escHtml(b.dismissKey) + '">' +
'<span class="flag-icon">' + b.icon + '</span>' +
'<div class="flag-body">' +
'<div class="flag-title">' + b.title + ' <code>' + b.keyLabel + '</code></div>' +
'<div class="flag-title">' + escHtml(b.title) + ' <code>' + escHtml(b.keyLabel) + '</code></div>' +
'<div class="flag-desc">' + escHtml(b.desc) + '</div>' +
'<code class="flag-enable">' + escHtml(b.enable) + '</code>' +
(b.docs ? ' <a class="empty-link" href="' + b.docs + '" target="_blank" rel="noopener">Learn more &rarr;</a>' : '') +
(b.docs ? ' <a class="empty-link" href="' + escHtml(b.docs) + '" target="_blank" rel="noopener">Learn more &rarr;</a>' : '') +
'</div>' +
'<button class="flag-close" data-dismiss-flag="' + b.dismissKey + '" aria-label="Dismiss">&times;</button>' +
'<button type="button" class="flag-close" data-dismiss-flag="' + escHtml(b.dismissKey) + '" aria-label="Dismiss">&times;</button>' +
'</div>';
}).join('');
host.innerHTML = '<button type="button" class="flag-summary" data-action="toggle-flags" aria-expanded="' + (expanded ? 'true' : 'false') + '" aria-controls="flag-list">' +
Expand Down Expand Up @@ -3446,11 +3556,10 @@ <h1>agentmemory</h1>
if (!(e.target instanceof Element)) return;
var btn = e.target.closest('[data-dismiss-flag]');
if (btn) {
e.preventDefault();
e.stopPropagation();
var key = btn.getAttribute('data-dismiss-flag');
var d = loadDismissedFlags();
d[key] = true;
saveDismissedFlags(d);
dismissFlags([key]);
if (state.flagsConfig) renderFlagBanners(state.flagsConfig);
return;
}
Expand All @@ -3462,12 +3571,6 @@ <h1>agentmemory</h1>
if (state.flagsConfig) renderFlagBanners(state.flagsConfig);
}
});
// Re-render banners when switching tabs so tab-specific banners appear
var _origSwitchTab = switchTab;
switchTab = function(tab) {
_origSwitchTab(tab);
if (state.flagsConfig) renderFlagBanners(state.flagsConfig);
};
fetchFlags();
document.addEventListener('click', function(e) {
if (!(e.target instanceof Element)) return;
Expand Down Expand Up @@ -3778,7 +3881,7 @@ <h1>agentmemory</h1>
else if (e.key === 'ArrowRight') { e.preventDefault(); stepReplay(1); }
});

loadTab('dashboard');
switchTab(tabFromRoute(), { replaceRoute: true });
connectWs();
startDashboardAutoRefresh();
</script>
Expand Down
Loading