@agentmemory/agentmemory 0.9.17 → 0.9.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>agentmemory viewer</title>
7
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
7
8
  <!-- Removed Google Fonts <link> in #323: the viewer CSP is strict
8
9
  (default-src 'none', style-src 'unsafe-inline', font-src 'self')
9
10
  and external stylesheets from fonts.googleapis.com were blocked,
@@ -103,12 +104,21 @@
103
104
  align-items: center;
104
105
  justify-content: space-between;
105
106
  background: var(--bg);
107
+ flex: 0 0 auto;
108
+ position: relative;
109
+ z-index: 3;
106
110
  }
107
111
  .app-header .brand {
108
112
  display: flex;
109
113
  align-items: baseline;
110
114
  gap: 10px;
115
+ color: inherit;
116
+ text-decoration: none;
117
+ cursor: pointer;
111
118
  }
119
+ .app-header .brand:hover h1,
120
+ .app-header .brand:focus-visible h1 { color: var(--accent); }
121
+ .app-header .brand:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
112
122
  .app-header .brand h1 {
113
123
  font-size: 22px;
114
124
  color: var(--ink);
@@ -157,6 +167,9 @@
157
167
  border-bottom: 1px solid var(--border-light);
158
168
  background: var(--bg);
159
169
  overflow-x: auto;
170
+ flex: 0 0 auto;
171
+ position: relative;
172
+ z-index: 2;
160
173
  }
161
174
  .tab-bar button {
162
175
  background: none;
@@ -427,22 +440,46 @@
427
440
  margin-bottom: 12px;
428
441
  border-left: 3px solid var(--border-light);
429
442
  transition: box-shadow 0.15s;
443
+ min-width: 0;
444
+ max-width: 100%;
445
+ overflow: hidden;
430
446
  }
431
447
  .obs-card:hover { box-shadow: 3px 3px 0px 0px var(--border-light); }
432
448
  .obs-card.imp-high { border-left-color: var(--accent); }
433
449
  .obs-card.imp-med { border-left-color: var(--yellow); }
434
450
  .obs-card.imp-low { border-left-color: var(--green); }
435
451
  .obs-card .obs-head {
452
+ display: grid;
453
+ grid-template-columns: minmax(0, 1fr) auto;
454
+ align-items: start;
455
+ gap: 12px;
456
+ margin-bottom: 6px;
457
+ }
458
+ .obs-card .obs-title-row {
436
459
  display: flex;
437
- justify-content: space-between;
438
460
  align-items: center;
439
- margin-bottom: 6px;
461
+ gap: 6px;
462
+ min-width: 0;
463
+ }
464
+ .obs-card .obs-meta {
465
+ display: flex;
466
+ align-items: center;
467
+ gap: 8px;
468
+ flex: 0 0 auto;
469
+ white-space: nowrap;
470
+ }
471
+ .obs-card .obs-type-icon {
472
+ flex: 0 0 auto;
440
473
  }
441
474
  .obs-card .obs-title {
442
475
  font-size: 14px;
443
476
  font-weight: 700;
444
477
  color: var(--ink);
445
478
  font-family: var(--font-display);
479
+ min-width: 0;
480
+ overflow: hidden;
481
+ text-overflow: ellipsis;
482
+ white-space: nowrap;
446
483
  }
447
484
  .obs-card .obs-time {
448
485
  font-size: 10px;
@@ -454,6 +491,14 @@
454
491
  font-size: 13px;
455
492
  color: var(--ink-muted);
456
493
  margin-bottom: 6px;
494
+ overflow-wrap: anywhere;
495
+ word-break: break-word;
496
+ }
497
+ .obs-card pre {
498
+ max-width: 100%;
499
+ white-space: pre-wrap;
500
+ overflow-wrap: anywhere;
501
+ word-break: break-word;
457
502
  }
458
503
  .obs-card .obs-facts {
459
504
  margin: 6px 0 6px 16px;
@@ -470,6 +515,9 @@
470
515
  color: var(--blue);
471
516
  font-family: var(--font-mono);
472
517
  font-weight: 500;
518
+ max-width: 100%;
519
+ overflow: hidden;
520
+ text-overflow: ellipsis;
473
521
  }
474
522
  .tag.file-tag { border-color: var(--green); color: var(--green); }
475
523
 
@@ -598,7 +646,14 @@
598
646
  .empty-state .empty-link { color: var(--accent); text-decoration: underline; font-size: 13px; font-style: normal; }
599
647
 
600
648
  /* Feature flag banner system — compact collapsed by default */
601
- .flag-banners { padding: 0 0 10px 0; }
649
+ .flag-banners {
650
+ padding: 0 12px 10px 12px;
651
+ background: var(--bg);
652
+ flex: 0 0 auto;
653
+ position: relative;
654
+ z-index: 1;
655
+ }
656
+ .flag-banners:empty { display: none; }
602
657
  button.flag-summary {
603
658
  display: flex; align-items: center; gap: 12px;
604
659
  padding: 8px 14px; border-radius: 4px;
@@ -609,6 +664,7 @@
609
664
  cursor: pointer; user-select: none;
610
665
  width: 100%; text-align: left;
611
666
  appearance: none;
667
+ flex: 1 1 auto;
612
668
  }
613
669
  button.flag-summary:hover,
614
670
  button.flag-summary:focus-visible { background: var(--bg-alt); outline: 2px solid var(--border); outline-offset: 1px; }
@@ -623,6 +679,8 @@
623
679
  .flag-list {
624
680
  display: none; flex-direction: column; gap: 6px;
625
681
  margin-top: 6px;
682
+ max-height: min(30vh, 260px);
683
+ overflow-y: auto;
626
684
  }
627
685
  .flag-list.open { display: flex; }
628
686
  .flag-banner {
@@ -645,11 +703,13 @@
645
703
  font-family: var(--font-mono); font-size: 10px; color: var(--ink);
646
704
  white-space: pre-wrap; word-break: break-all;
647
705
  }
648
- .flag-banner .flag-close {
706
+ .flag-close {
649
707
  background: none; border: none; color: var(--ink-faint); cursor: pointer;
650
708
  font-size: 16px; line-height: 1; padding: 0 4px; font-family: inherit;
709
+ flex: 0 0 auto;
651
710
  }
652
- .flag-banner .flag-close:hover { color: var(--ink); }
711
+ .flag-close:hover,
712
+ .flag-close:focus-visible { color: var(--ink); outline: 2px solid var(--border); outline-offset: 1px; }
653
713
 
654
714
  /* Viewer footer */
655
715
  .viewer-footer {
@@ -811,7 +871,7 @@
811
871
 
812
872
  .timeline-container { position: relative; padding: 20px 0; }
813
873
  .timeline-container::before { content: ''; position: absolute; left: 50%; top: 0; bottom: 0; width: 2px; background: var(--border-light); transform: translateX(-50%); }
814
- .timeline-item { position: relative; width: 45%; margin-bottom: 20px; }
874
+ .timeline-item { position: relative; width: 45%; margin-bottom: 20px; min-width: 0; }
815
875
  .timeline-item.tl-left { margin-left: 0; margin-right: auto; text-align: right; padding-right: 30px; }
816
876
  .timeline-item.tl-right { margin-left: auto; margin-right: 0; padding-left: 30px; }
817
877
  .timeline-dot { position: absolute; width: 12px; height: 12px; border-radius: 50%; top: 16px; z-index: 1; border: 2px solid var(--bg); }
@@ -874,10 +934,10 @@
874
934
  </head>
875
935
  <body>
876
936
  <div class="app-header">
877
- <div class="brand">
937
+ <a class="brand" href="#dashboard" data-tab-link="dashboard" aria-label="Open dashboard">
878
938
  <h1>agentmemory</h1>
879
939
  <span class="version">v__AGENTMEMORY_VERSION__</span>
880
- </div>
940
+ </a>
881
941
  <div class="header-right">
882
942
  <span class="dateline" id="dateline"></span>
883
943
  <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>
@@ -886,7 +946,7 @@
886
946
  </div>
887
947
 
888
948
  <div class="tab-bar" id="tab-bar">
889
- <button class="active" data-tab="dashboard">Dashboard</button>
949
+ <button class="active" data-tab="dashboard" aria-current="page">Dashboard</button>
890
950
  <button data-tab="graph">Graph</button>
891
951
  <button data-tab="memories">Memories</button>
892
952
  <button data-tab="timeline">Timeline</button>
@@ -1002,6 +1062,7 @@
1002
1062
  task: '&#9745;', other: '&#128196;'
1003
1063
  };
1004
1064
  var CB_STATE_COLORS = { closed: 'badge-green', open: 'badge-red', 'half-open': 'badge-yellow' };
1065
+ var TAB_IDS = ['dashboard', 'graph', 'memories', 'timeline', 'sessions', 'lessons', 'actions', 'crystals', 'audit', 'activity', 'profile', 'replay'];
1005
1066
 
1006
1067
  var state = {
1007
1068
  activeTab: 'dashboard',
@@ -1018,6 +1079,7 @@
1018
1079
  profile: { loaded: false, projects: [], selectedProject: '', data: null },
1019
1080
  replay: { loaded: false, sessions: [], selectedId: '', timeline: null, cursor: 0, playing: false, speed: 1, timer: null, startAt: 0, offsetAt: 0 },
1020
1081
  flagsConfig: null,
1082
+ flagsDismissed: {},
1021
1083
  ws: null
1022
1084
  };
1023
1085
 
@@ -1039,6 +1101,23 @@
1039
1101
  if (!s) return '';
1040
1102
  return s.length > n ? s.slice(0, n) + '...' : s;
1041
1103
  }
1104
+ function sessionId(s) {
1105
+ return s && s.id !== undefined && s.id !== null ? String(s.id) : '';
1106
+ }
1107
+ function shortSessionId(s, n) {
1108
+ var id = sessionId(s);
1109
+ return id ? id.slice(0, n || 8) : '';
1110
+ }
1111
+ function sessionDisplayName(s) {
1112
+ var project = s && s.project ? String(s.project).split('/').pop() : '';
1113
+ if (project) return project;
1114
+ return shortSessionId(s, 8) || 'Unknown session';
1115
+ }
1116
+ function sessionLabel(s) {
1117
+ var id = shortSessionId(s, 8);
1118
+ var name = sessionDisplayName(s);
1119
+ return id ? name + ' (' + id + ')' : name + ' (missing id)';
1120
+ }
1042
1121
  function debounce(fn, ms) {
1043
1122
  var t;
1044
1123
  return function() {
@@ -1080,17 +1159,52 @@
1080
1159
  });
1081
1160
  }
1082
1161
 
1083
- function switchTab(tab) {
1162
+ function normalizeTab(tab) {
1163
+ var normalized = String(tab || '').replace(/^#/, '').toLowerCase();
1164
+ return TAB_IDS.indexOf(normalized) >= 0 ? normalized : 'dashboard';
1165
+ }
1166
+
1167
+ function tabFromRoute() {
1168
+ try {
1169
+ return normalizeTab(decodeURIComponent(window.location.hash.slice(1)));
1170
+ } catch (_) {
1171
+ return 'dashboard';
1172
+ }
1173
+ }
1174
+
1175
+ function updateTabRoute(tab, replace) {
1176
+ var target = '#' + tab;
1177
+ if (window.location.hash === target) return;
1178
+ if (replace) {
1179
+ history.replaceState(null, '', target);
1180
+ } else {
1181
+ history.pushState(null, '', target);
1182
+ }
1183
+ }
1184
+
1185
+ function switchTab(tab, opts) {
1186
+ opts = opts || {};
1187
+ tab = normalizeTab(tab);
1084
1188
  if (state.activeTab === 'replay' && tab !== 'replay' && typeof stopReplayTimer === 'function') {
1085
1189
  stopReplayTimer();
1086
1190
  }
1191
+ if (!opts.skipRoute) {
1192
+ updateTabRoute(tab, !!opts.replaceRoute);
1193
+ }
1087
1194
  state.activeTab = tab;
1088
1195
  document.querySelectorAll('.tab-bar button').forEach(function(b) {
1089
- b.classList.toggle('active', b.dataset.tab === tab);
1196
+ var isActive = b.dataset.tab === tab;
1197
+ b.classList.toggle('active', isActive);
1198
+ if (isActive) {
1199
+ b.setAttribute('aria-current', 'page');
1200
+ } else {
1201
+ b.removeAttribute('aria-current');
1202
+ }
1090
1203
  });
1091
1204
  document.querySelectorAll('.view').forEach(function(v) {
1092
1205
  v.classList.toggle('active', v.id === 'view-' + tab);
1093
1206
  });
1207
+ if (state.flagsConfig) renderFlagBanners(state.flagsConfig);
1094
1208
  loadTab(tab);
1095
1209
  }
1096
1210
 
@@ -1278,7 +1392,7 @@
1278
1392
  html += '<table><tr><th>Project</th><th>Status</th><th>Obs</th><th>Started</th></tr>';
1279
1393
  recent.forEach(function(s) {
1280
1394
  var statusBadge = s.status === 'active' ? 'badge-green' : s.status === 'completed' ? 'badge-blue' : 'badge-muted';
1281
- html += '<tr><td style="color:var(--ink);font-weight:500;">' + esc(s.project ? s.project.split('/').pop() : s.id.slice(0,8)) + '</td>';
1395
+ html += '<tr><td style="color:var(--ink);font-weight:500;">' + esc(sessionDisplayName(s)) + '</td>';
1282
1396
  html += '<td><span class="badge ' + statusBadge + '">' + esc(s.status) + '</span></td>';
1283
1397
  html += '<td style="color:var(--ink-muted);font-family:var(--font-mono);font-size:12px;">' + (s.observationCount || 0) + '</td>';
1284
1398
  html += '<td style="font-family:var(--font-mono);font-size:11px;color:var(--ink-faint);">' + esc(shortTime(s.startedAt)) + '</td></tr>';
@@ -2192,7 +2306,8 @@
2192
2306
 
2193
2307
  if (sessions.length > 0 && !state.timeline.sessionId) {
2194
2308
  var sorted = sessions.slice().sort(function(a, b) { return (b.startedAt || '').localeCompare(a.startedAt || ''); });
2195
- state.timeline.sessionId = sorted[0].id;
2309
+ var firstSelectable = sorted.find(function(s) { return sessionId(s); });
2310
+ state.timeline.sessionId = firstSelectable ? sessionId(firstSelectable) : '';
2196
2311
  }
2197
2312
 
2198
2313
  renderTimelineToolbar(sessions);
@@ -2204,8 +2319,9 @@
2204
2319
  var html = '<div class="toolbar">';
2205
2320
  html += '<select id="tl-session"><option value="">Select session</option>';
2206
2321
  sessions.sort(function(a, b) { return (b.startedAt || '').localeCompare(a.startedAt || ''); }).forEach(function(s) {
2207
- var label = (s.project ? s.project.split('/').pop() : s.id.slice(0,8)) + ' (' + s.id.slice(0,8) + ')';
2208
- html += '<option value="' + esc(s.id) + '"' + (state.timeline.sessionId === s.id ? ' selected' : '') + '>' + esc(label) + '</option>';
2322
+ var id = sessionId(s);
2323
+ var disabled = id ? '' : ' disabled';
2324
+ html += '<option value="' + esc(id) + '"' + (id && state.timeline.sessionId === id ? ' selected' : '') + disabled + '>' + esc(sessionLabel(s)) + '</option>';
2209
2325
  });
2210
2326
  html += '</select>';
2211
2327
  html += '<select id="tl-importance"><option value="0">All importance</option>';
@@ -2319,12 +2435,12 @@
2319
2435
 
2320
2436
  html += '<div class="obs-card imp-' + impClass + '" style="border-left-color:' + typeColor + ';text-align:left;">';
2321
2437
  html += '<div class="obs-head">';
2322
- html += '<div style="display:flex;align-items:center;gap:6px;">';
2438
+ html += '<div class="obs-title-row">';
2323
2439
  html += '<span class="obs-type-icon">' + icon + '</span>';
2324
- html += '<span class="obs-title">' + esc(title) + '</span>';
2440
+ html += '<span class="obs-title" title="' + esc(title) + '">' + esc(title) + '</span>';
2325
2441
  if (isRaw) html += '<span class="badge badge-muted" style="font-size:8px;margin-left:4px;">raw</span>';
2326
2442
  html += '</div>';
2327
- html += '<div style="display:flex;align-items:center;gap:8px;">';
2443
+ html += '<div class="obs-meta">';
2328
2444
  if (isCompressed) html += '<span class="obs-importance imp-' + impVal + '" title="Importance: ' + impVal + '/10">' + impVal + '</span>';
2329
2445
  html += '<span class="obs-time">' + esc(shortTime(o.timestamp)) + '</span>';
2330
2446
  html += '</div></div>';
@@ -2421,8 +2537,8 @@
2421
2537
  var sorted = sessions.slice().sort(function(a, b) { return (b.startedAt || '').localeCompare(a.startedAt || ''); });
2422
2538
  var recentSessions = sorted.slice(0, 5);
2423
2539
 
2424
- var obsResults = await Promise.all(recentSessions.map(function(s) {
2425
- return apiGet('observations?sessionId=' + encodeURIComponent(s.id));
2540
+ var obsResults = await Promise.all(recentSessions.filter(function(s) { return sessionId(s); }).map(function(s) {
2541
+ return apiGet('observations?sessionId=' + encodeURIComponent(sessionId(s)));
2426
2542
  }));
2427
2543
  obsResults.forEach(function(r) {
2428
2544
  if (r && r.observations) allObs = allObs.concat(r.observations);
@@ -2565,15 +2681,16 @@
2565
2681
  } else {
2566
2682
  items.forEach(function(s) {
2567
2683
  var statusBadge = s.status === 'active' ? 'badge-green' : s.status === 'completed' ? 'badge-blue' : 'badge-muted';
2568
- var selected = state.sessions.selectedId === s.id;
2569
- html += '<div class="session-item' + (selected ? ' selected' : '') + '" data-action="select-session" data-session-id="' + esc(s.id) + '">';
2570
- html += '<div class="session-top"><span class="session-project">' + esc(s.project ? s.project.split('/').pop() : 'Unknown') + '</span>';
2684
+ var id = sessionId(s);
2685
+ var selected = id && state.sessions.selectedId === id;
2686
+ html += '<div class="session-item' + (selected ? ' selected' : '') + '"' + (id ? ' data-action="select-session" data-session-id="' + esc(id) + '"' : '') + '>';
2687
+ html += '<div class="session-top"><span class="session-project">' + esc(sessionDisplayName(s)) + '</span>';
2571
2688
  html += '<span class="badge ' + statusBadge + '">' + esc(s.status) + '</span></div>';
2572
2689
  var preview = s.firstPrompt || s.summary || '';
2573
2690
  if (preview) {
2574
2691
  html += '<div class="session-preview" style="font-size:13px;color:var(--ink);margin:4px 0;line-height:1.4;">' + esc(truncate(preview, 140)) + '</div>';
2575
2692
  }
2576
- html += '<div class="session-meta">' + esc(s.id.slice(0, 12)) + ' &middot; ' + esc(formatTime(s.startedAt));
2693
+ html += '<div class="session-meta">' + esc(shortSessionId(s, 12) || 'missing id') + ' &middot; ' + esc(formatTime(s.startedAt));
2577
2694
  html += ' &middot; ' + (s.observationCount || 0) + ' obs';
2578
2695
  if (s.model) html += ' &middot; ' + esc(s.model);
2579
2696
  html += '</div></div>';
@@ -2594,12 +2711,13 @@
2594
2711
  async function renderSessionDetail() {
2595
2712
  var panel = document.getElementById('session-detail');
2596
2713
  if (!panel) return;
2597
- var s = state.sessions.items.find(function(x) { return x.id === state.sessions.selectedId; });
2598
- if (!s) { panel.innerHTML = ''; return; }
2714
+ var s = state.sessions.items.find(function(x) { return sessionId(x) === state.sessions.selectedId; });
2715
+ var id = sessionId(s);
2716
+ if (!s || !id) { panel.innerHTML = ''; return; }
2599
2717
 
2600
2718
  panel.innerHTML = '<div class="detail-panel"><h3>Loading session details…</h3></div>';
2601
2719
 
2602
- var obsRes = await apiGet('observations?sessionId=' + encodeURIComponent(s.id));
2720
+ var obsRes = await apiGet('observations?sessionId=' + encodeURIComponent(id));
2603
2721
  var obs = (obsRes && obsRes.observations) || [];
2604
2722
 
2605
2723
  var typeCounts = {};
@@ -2672,7 +2790,8 @@
2672
2790
 
2673
2791
  html += '<div class="card" style="margin-bottom:12px;"><div class="card-title">Metadata</div>';
2674
2792
  html += '<div style="font-size:12px;font-family:var(--font-mono);margin-top:8px;line-height:1.7;">';
2675
- html += '<div><span style="color:var(--ink-muted);">id:</span> ' + esc(s.id) + '</div>';
2793
+ var detailId = sessionId(s);
2794
+ html += '<div><span style="color:var(--ink-muted);">id:</span> ' + esc(detailId || 'missing id') + '</div>';
2676
2795
  html += '<div><span style="color:var(--ink-muted);">cwd:</span> ' + esc(s.cwd || '-') + '</div>';
2677
2796
  html += '<div><span style="color:var(--ink-muted);">started:</span> ' + esc(formatTime(s.startedAt)) + '</div>';
2678
2797
  if (s.endedAt) html += '<div><span style="color:var(--ink-muted);">ended:</span> ' + esc(formatTime(s.endedAt)) + '</div>';
@@ -2681,10 +2800,14 @@
2681
2800
  html += '</div></div>';
2682
2801
 
2683
2802
  html += '<div style="display:flex;gap:8px;">';
2684
- if (s.status === 'active') {
2685
- html += '<button class="btn btn-danger" data-action="end-session" data-session-id="' + esc(s.id) + '">End Session</button>';
2803
+ if (detailId && s.status === 'active') {
2804
+ html += '<button class="btn btn-danger" data-action="end-session" data-session-id="' + esc(detailId) + '">End Session</button>';
2805
+ }
2806
+ if (detailId) {
2807
+ html += '<button class="btn btn-primary" data-action="summarize-session" data-session-id="' + esc(detailId) + '">Summarize</button>';
2808
+ } else {
2809
+ html += '<button class="btn btn-primary" disabled>Summarize unavailable</button>';
2686
2810
  }
2687
- html += '<button class="btn btn-primary" data-action="summarize-session" data-session-id="' + esc(s.id) + '">Summarize</button>';
2688
2811
  html += '</div></div>';
2689
2812
  panel.innerHTML = html;
2690
2813
  }
@@ -3320,26 +3443,39 @@
3320
3443
  }
3321
3444
 
3322
3445
  document.getElementById('tab-bar').addEventListener('click', function(e) {
3323
- if (e.target.tagName === 'BUTTON' && e.target.dataset.tab) {
3324
- switchTab(e.target.dataset.tab);
3325
- }
3446
+ var btn = e.target instanceof Element ? e.target.closest('button[data-tab]') : null;
3447
+ if (btn) switchTab(btn.dataset.tab);
3326
3448
  });
3327
3449
 
3328
- // --- Feature flag banners ---------------------------------------------
3329
- var FLAG_DISMISS_KEY = 'agentmemory.viewer.flags.dismissed.v1';
3330
- function loadDismissedFlags() {
3331
- try {
3332
- var raw = localStorage.getItem(FLAG_DISMISS_KEY);
3333
- return raw ? JSON.parse(raw) : {};
3334
- } catch (_) { return {}; }
3450
+ document.querySelectorAll('[data-tab-link]').forEach(function(link) {
3451
+ link.addEventListener('click', function(e) {
3452
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
3453
+ e.preventDefault();
3454
+ switchTab(link.getAttribute('data-tab-link'));
3455
+ });
3456
+ });
3457
+
3458
+ function syncTabFromRoute() {
3459
+ switchTab(tabFromRoute(), { replaceRoute: true });
3335
3460
  }
3336
- function saveDismissedFlags(d) {
3337
- try { localStorage.setItem(FLAG_DISMISS_KEY, JSON.stringify(d)); } catch (_) {}
3461
+ window.addEventListener('hashchange', syncTabFromRoute);
3462
+ window.addEventListener('popstate', syncTabFromRoute);
3463
+
3464
+ // --- Feature flag banners ---------------------------------------------
3465
+ function getDismissedFlags() {
3466
+ if (!state.flagsDismissed) state.flagsDismissed = {};
3467
+ return state.flagsDismissed;
3468
+ }
3469
+ function dismissFlags(keys) {
3470
+ var dismissed = getDismissedFlags();
3471
+ keys.forEach(function(key) {
3472
+ if (key) dismissed[key] = true;
3473
+ });
3338
3474
  }
3339
3475
  function renderFlagBanners(cfg) {
3340
3476
  var host = document.getElementById('flag-banners');
3341
3477
  if (!host) return;
3342
- var dismissed = loadDismissedFlags();
3478
+ var dismissed = getDismissedFlags();
3343
3479
  var banners = [];
3344
3480
  // Per-flag banner (only for off flags, affecting current tab or dashboard)
3345
3481
  (cfg.flags || []).forEach(function(f) {
@@ -3395,15 +3531,15 @@
3395
3531
  });
3396
3532
  };
3397
3533
  var listHtml = banners.map(function(b) {
3398
- return '<div class="flag-banner ' + b.kind + '" data-flag="' + b.dismissKey + '">' +
3534
+ return '<div class="flag-banner ' + b.kind + '" data-flag="' + escHtml(b.dismissKey) + '">' +
3399
3535
  '<span class="flag-icon">' + b.icon + '</span>' +
3400
3536
  '<div class="flag-body">' +
3401
- '<div class="flag-title">' + b.title + ' <code>' + b.keyLabel + '</code></div>' +
3537
+ '<div class="flag-title">' + escHtml(b.title) + ' <code>' + escHtml(b.keyLabel) + '</code></div>' +
3402
3538
  '<div class="flag-desc">' + escHtml(b.desc) + '</div>' +
3403
3539
  '<code class="flag-enable">' + escHtml(b.enable) + '</code>' +
3404
- (b.docs ? ' <a class="empty-link" href="' + b.docs + '" target="_blank" rel="noopener">Learn more &rarr;</a>' : '') +
3540
+ (b.docs ? ' <a class="empty-link" href="' + escHtml(b.docs) + '" target="_blank" rel="noopener">Learn more &rarr;</a>' : '') +
3405
3541
  '</div>' +
3406
- '<button class="flag-close" data-dismiss-flag="' + b.dismissKey + '" aria-label="Dismiss">&times;</button>' +
3542
+ '<button type="button" class="flag-close" data-dismiss-flag="' + escHtml(b.dismissKey) + '" aria-label="Dismiss">&times;</button>' +
3407
3543
  '</div>';
3408
3544
  }).join('');
3409
3545
  host.innerHTML = '<button type="button" class="flag-summary" data-action="toggle-flags" aria-expanded="' + (expanded ? 'true' : 'false') + '" aria-controls="flag-list">' +
@@ -3446,11 +3582,10 @@
3446
3582
  if (!(e.target instanceof Element)) return;
3447
3583
  var btn = e.target.closest('[data-dismiss-flag]');
3448
3584
  if (btn) {
3585
+ e.preventDefault();
3449
3586
  e.stopPropagation();
3450
3587
  var key = btn.getAttribute('data-dismiss-flag');
3451
- var d = loadDismissedFlags();
3452
- d[key] = true;
3453
- saveDismissedFlags(d);
3588
+ dismissFlags([key]);
3454
3589
  if (state.flagsConfig) renderFlagBanners(state.flagsConfig);
3455
3590
  return;
3456
3591
  }
@@ -3462,12 +3597,6 @@
3462
3597
  if (state.flagsConfig) renderFlagBanners(state.flagsConfig);
3463
3598
  }
3464
3599
  });
3465
- // Re-render banners when switching tabs so tab-specific banners appear
3466
- var _origSwitchTab = switchTab;
3467
- switchTab = function(tab) {
3468
- _origSwitchTab(tab);
3469
- if (state.flagsConfig) renderFlagBanners(state.flagsConfig);
3470
- };
3471
3600
  fetchFlags();
3472
3601
  document.addEventListener('click', function(e) {
3473
3602
  if (!(e.target instanceof Element)) return;
@@ -3587,8 +3716,9 @@
3587
3716
  var el = document.getElementById('view-replay');
3588
3717
  var sessions = state.replay.sessions || [];
3589
3718
  var options = '<option value="">— pick a session —</option>' + sessions.map(function(s) {
3590
- var label = (s.project || 'unknown') + ' · ' + (s.id || '').slice(0, 8) + ' · ' + (s.observationCount || 0) + ' obs';
3591
- return '<option value="' + esc(s.id) + '"' + (s.id === state.replay.selectedId ? ' selected' : '') + '>' + esc(label) + '</option>';
3719
+ var id = sessionId(s);
3720
+ var label = sessionDisplayName(s) + ' · ' + (shortSessionId(s, 8) || 'missing id') + ' · ' + (s.observationCount || 0) + ' obs';
3721
+ return '<option value="' + esc(id) + '"' + (id && id === state.replay.selectedId ? ' selected' : '') + (id ? '' : ' disabled') + '>' + esc(label) + '</option>';
3592
3722
  }).join('');
3593
3723
 
3594
3724
  var tl = state.replay.timeline;
@@ -3778,7 +3908,7 @@
3778
3908
  else if (e.key === 'ArrowRight') { e.preventDefault(); stepReplay(1); }
3779
3909
  });
3780
3910
 
3781
- loadTab('dashboard');
3911
+ switchTab(tabFromRoute(), { replaceRoute: true });
3782
3912
  connectWs();
3783
3913
  startDashboardAutoRefresh();
3784
3914
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentmemory/agentmemory",
3
- "version": "0.9.17",
3
+ "version": "0.9.18",
4
4
  "description": "Persistent memory for AI coding agents, powered by iii-engine's three primitives",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -17,7 +17,7 @@
17
17
  "agentmemory": "dist/cli.mjs"
18
18
  },
19
19
  "scripts": {
20
- "build": "tsdown && (cp iii-config.yaml dist/ 2>/dev/null || true) && (cp iii-config.docker.yaml dist/ 2>/dev/null || true) && (cp docker-compose.yml dist/ 2>/dev/null || true) && (cp .env.example dist/ 2>/dev/null || true) && mkdir -p dist/viewer && cp src/viewer/index.html dist/viewer/",
20
+ "build": "tsdown && (cp iii-config.yaml dist/ 2>/dev/null || true) && (cp iii-config.docker.yaml dist/ 2>/dev/null || true) && (cp docker-compose.yml dist/ 2>/dev/null || true) && (cp .env.example dist/ 2>/dev/null || true) && mkdir -p dist/viewer && cp src/viewer/index.html dist/viewer/ && cp src/viewer/favicon.svg dist/viewer/",
21
21
  "dev": "tsx src/index.ts",
22
22
  "start": "node dist/cli.mjs",
23
23
  "migrate": "node dist/functions/migrate.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentmemory",
3
- "version": "0.9.17",
3
+ "version": "0.9.18",
4
4
  "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 51 MCP tools, 4 skills, real-time viewer.",
5
5
  "author": {
6
6
  "name": "Rohit Ghumare",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentmemory",
3
- "version": "0.9.17",
3
+ "version": "0.9.18",
4
4
  "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 6 hooks, 51 MCP tools, 4 skills, real-time viewer.",
5
5
  "author": {
6
6
  "name": "Rohit Ghumare",