twining-mcp 1.7.1 → 1.8.0

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.
@@ -156,6 +156,11 @@ function fetchBlackboard() {
156
156
  state.connected = true;
157
157
  updateConnectionIndicator();
158
158
  renderBlackboard();
159
+ // Refresh stream if visible
160
+ var streamView = document.getElementById('blackboard-stream-view');
161
+ if (streamView && streamView.style.display !== 'none') {
162
+ renderStream();
163
+ }
159
164
  renderActivityBreakdown();
160
165
  renderRecentActivity();
161
166
  })
@@ -430,8 +435,13 @@ function handleSort(tabName, key) {
430
435
  }
431
436
  tabState.page = 1;
432
437
 
433
- if (tabName === "blackboard") renderBlackboard();
434
- else if (tabName === "decisions") renderDecisions();
438
+ if (tabName === "blackboard") {
439
+ renderBlackboard();
440
+ var streamView = document.getElementById('blackboard-stream-view');
441
+ if (streamView && streamView.style.display !== 'none') {
442
+ renderStream();
443
+ }
444
+ } else if (tabName === "decisions") renderDecisions();
435
445
  else if (tabName === "graph") renderGraph();
436
446
  else if (tabName === "search") renderSearchResults();
437
447
  else if (tabName === "agents") renderAgents();
@@ -1688,16 +1698,14 @@ function buildSearchUrl() {
1688
1698
  if (!q) return null;
1689
1699
  var params = "q=" + encodeURIComponent(q);
1690
1700
 
1691
- // Type filter
1692
- var typesSelect = document.getElementById("filter-types");
1693
- if (typesSelect) {
1701
+ // Type filter (from chips)
1702
+ var typeChips = document.querySelectorAll(".search-chip.active[data-type]");
1703
+ if (typeChips.length > 0 && typeChips.length < 3) {
1694
1704
  var selectedTypes = [];
1695
- for (var i = 0; i < typesSelect.options.length; i++) {
1696
- if (typesSelect.options[i].selected) selectedTypes.push(typesSelect.options[i].value);
1697
- }
1698
- if (selectedTypes.length > 0 && selectedTypes.length < 3) {
1699
- params += "&types=" + selectedTypes.join(",");
1705
+ for (var i = 0; i < typeChips.length; i++) {
1706
+ selectedTypes.push(typeChips[i].getAttribute("data-type"));
1700
1707
  }
1708
+ params += "&types=" + selectedTypes.join(",");
1701
1709
  }
1702
1710
 
1703
1711
  // Status filter
@@ -1932,6 +1940,11 @@ function toggleView(tab, viewName) {
1932
1940
  if (viewName === 'delegations' && state.delegations.data.length === 0) fetchDelegations();
1933
1941
  if (viewName === 'handoffs' && state.handoffs.data.length === 0) fetchHandoffs();
1934
1942
  }
1943
+ if (tab === 'blackboard') {
1944
+ document.getElementById('blackboard-table-view').style.display = viewName === 'table' ? 'block' : 'none';
1945
+ document.getElementById('blackboard-stream-view').style.display = viewName === 'stream' ? 'block' : 'none';
1946
+ if (viewName === 'stream' && typeof renderStream === 'function') renderStream();
1947
+ }
1935
1948
  }
1936
1949
 
1937
1950
  /* ========== Decision Timeline Visualization ========== */
@@ -1939,14 +1952,48 @@ function toggleView(tab, viewName) {
1939
1952
  var CONFIDENCE_CLASSES = { high: 'confidence-high', medium: 'confidence-medium', low: 'confidence-low' };
1940
1953
  var STATUS_CLASSES = { provisional: 'status-provisional', superseded: 'status-superseded', overridden: 'status-overridden' };
1941
1954
 
1955
+ var DOMAIN_COLORS = {
1956
+ architecture: '#6366f1',
1957
+ implementation: '#3b82f6',
1958
+ testing: '#10b981',
1959
+ deployment: '#f59e0b',
1960
+ security: '#ef4444',
1961
+ performance: '#8b5cf6',
1962
+ 'api-design': '#06b6d4',
1963
+ 'data-model': '#ec4899'
1964
+ };
1965
+ var DOMAIN_COLOR_DEFAULT = '#6b7280';
1966
+
1967
+ // Track which domains are active in filters (null = all active)
1968
+ var timelineDomainFilter = null;
1969
+
1942
1970
  function getDecisionClassName(d) {
1971
+ var classes = [];
1943
1972
  if (d.status && d.status !== 'active' && STATUS_CLASSES[d.status]) {
1944
- return STATUS_CLASSES[d.status];
1973
+ classes.push(STATUS_CLASSES[d.status]);
1945
1974
  }
1946
1975
  if (d.confidence && CONFIDENCE_CLASSES[d.confidence]) {
1947
- return CONFIDENCE_CLASSES[d.confidence];
1976
+ classes.push(CONFIDENCE_CLASSES[d.confidence]);
1948
1977
  }
1949
- return '';
1978
+ return classes.join(' ');
1979
+ }
1980
+
1981
+ function buildTimelineGroups(decisions) {
1982
+ var domainSet = {};
1983
+ for (var i = 0; i < decisions.length; i++) {
1984
+ var domain = decisions[i].domain || 'other';
1985
+ domainSet[domain] = true;
1986
+ }
1987
+ var groups = [];
1988
+ var sorted = Object.keys(domainSet).sort();
1989
+ for (var j = 0; j < sorted.length; j++) {
1990
+ groups.push({
1991
+ id: sorted[j],
1992
+ content: sorted[j],
1993
+ style: 'border-left: 3px solid ' + (DOMAIN_COLORS[sorted[j]] || DOMAIN_COLOR_DEFAULT) + '; padding-left: 8px;'
1994
+ });
1995
+ }
1996
+ return groups;
1950
1997
  }
1951
1998
 
1952
1999
  function buildTimelineItems(decisions) {
@@ -1954,12 +2001,22 @@ function buildTimelineItems(decisions) {
1954
2001
  var items = [];
1955
2002
  for (var i = 0; i < scoped.length; i++) {
1956
2003
  var d = scoped[i];
2004
+ var domain = d.domain || 'other';
2005
+ // Apply domain filter
2006
+ if (timelineDomainFilter && !timelineDomainFilter[domain]) continue;
2007
+ var conf = d.confidence || 'unknown';
2008
+ var stat = d.status || 'active';
2009
+ var tooltipParts = [d.summary];
2010
+ tooltipParts.push(conf.charAt(0).toUpperCase() + conf.slice(1) + ' confidence');
2011
+ tooltipParts.push('Status: ' + stat);
2012
+ if (d.scope) tooltipParts.push('Scope: ' + d.scope);
1957
2013
  items.push({
1958
2014
  id: d.id,
1959
- content: truncate(d.summary, 60),
2015
+ group: domain,
2016
+ content: truncate(d.summary, 50),
1960
2017
  start: d.timestamp,
1961
2018
  className: getDecisionClassName(d),
1962
- title: d.summary + ' [' + (d.status || 'unknown') + ', ' + (d.confidence || 'unknown') + ']'
2019
+ title: tooltipParts.join('\n')
1963
2020
  });
1964
2021
  }
1965
2022
  return items;
@@ -1972,24 +2029,31 @@ function initTimeline() {
1972
2029
  }
1973
2030
  if (typeof vis === 'undefined' || !vis.Timeline) return;
1974
2031
 
2032
+ var scoped = applyGlobalScope(state.decisions.data, 'scope');
2033
+ var groups = buildTimelineGroups(scoped);
1975
2034
  var items = buildTimelineItems(state.decisions.data);
1976
2035
  window.timelineDataSet = new vis.DataSet(items);
2036
+ window.timelineGroupSet = new vis.DataSet(groups);
1977
2037
 
1978
2038
  var container = document.getElementById('timeline-container');
1979
2039
  if (!container) return;
1980
2040
 
1981
2041
  var options = {
1982
- zoomMin: 1000 * 60 * 60,
1983
- zoomMax: 1000 * 60 * 60 * 24 * 365,
2042
+ zoomMin: 1000 * 60 * 5,
2043
+ zoomMax: 1000 * 60 * 60 * 24 * 365 * 2,
1984
2044
  orientation: { axis: 'top' },
1985
2045
  selectable: true,
1986
- tooltip: { followMouse: true },
1987
- margin: { item: { horizontal: 10, vertical: 20 } },
2046
+ tooltip: { followMouse: true, delay: 150 },
2047
+ margin: { item: { horizontal: 8, vertical: 5 } },
1988
2048
  stack: true,
1989
- maxHeight: 600
2049
+ maxHeight: 600,
2050
+ verticalScroll: true,
2051
+ zoomKey: '',
2052
+ showCurrentTime: true,
2053
+ groupOrder: 'content'
1990
2054
  };
1991
2055
 
1992
- window.timelineInstance = new vis.Timeline(container, window.timelineDataSet, options);
2056
+ window.timelineInstance = new vis.Timeline(container, window.timelineDataSet, window.timelineGroupSet, options);
1993
2057
 
1994
2058
  window.timelineInstance.on('select', function(properties) {
1995
2059
  if (properties.items.length > 0) {
@@ -1999,26 +2063,51 @@ function initTimeline() {
1999
2063
  }
2000
2064
  });
2001
2065
 
2002
- window.timelineInstance.fit();
2066
+ window.timelineInstance.fit({ animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
2067
+
2068
+ // Wire up controls
2069
+ wireTimelineControls();
2003
2070
  renderTimelineLegend();
2071
+ renderTimelineDomainFilters();
2072
+ }
2073
+
2074
+ function wireTimelineControls() {
2075
+ var zoomIn = document.getElementById('timeline-zoom-in');
2076
+ var zoomOut = document.getElementById('timeline-zoom-out');
2077
+ var fit = document.getElementById('timeline-fit');
2078
+ var today = document.getElementById('timeline-today');
2079
+
2080
+ if (zoomIn) zoomIn.addEventListener('click', function() {
2081
+ if (window.timelineInstance) window.timelineInstance.zoomIn(0.4, { animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
2082
+ });
2083
+ if (zoomOut) zoomOut.addEventListener('click', function() {
2084
+ if (window.timelineInstance) window.timelineInstance.zoomOut(0.4, { animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
2085
+ });
2086
+ if (fit) fit.addEventListener('click', function() {
2087
+ if (window.timelineInstance) window.timelineInstance.fit({ animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
2088
+ });
2089
+ if (today) today.addEventListener('click', function() {
2090
+ if (window.timelineInstance) window.timelineInstance.moveTo(new Date(), { animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
2091
+ });
2004
2092
  }
2005
2093
 
2006
2094
  function renderTimelineLegend() {
2007
2095
  var legend = document.getElementById('timeline-legend');
2008
- if (!legend || legend.children.length > 0) return;
2096
+ if (!legend) return;
2097
+ clearElement(legend);
2009
2098
  var items = [
2010
- { color: '#4caf50', label: 'High confidence' },
2011
- { color: '#ff9800', label: 'Medium confidence' },
2012
- { color: '#f44336', label: 'Low confidence' },
2013
- { color: 'var(--muted)', label: 'Provisional (dashed)', dashed: true },
2014
- { color: 'var(--muted)', label: 'Superseded (strikethrough)', strike: true }
2099
+ { color: 'var(--success)', label: 'High confidence' },
2100
+ { color: 'var(--warning)', label: 'Medium confidence' },
2101
+ { color: 'var(--error)', label: 'Low confidence' },
2102
+ { color: null, label: 'Provisional', dashed: true },
2103
+ { color: null, label: 'Superseded', strike: true }
2015
2104
  ];
2016
2105
  for (var i = 0; i < items.length; i++) {
2017
2106
  var item = document.createElement('span');
2018
- item.style.cssText = 'display:inline-flex;align-items:center;gap:4px;margin-right:12px;font-size:0.8rem;';
2107
+ item.className = 'timeline-legend-item';
2019
2108
  var dot = document.createElement('span');
2020
- dot.style.cssText = 'width:10px;height:10px;border-radius:50%;display:inline-block;background:' + items[i].color;
2021
- if (items[i].dashed) dot.style.cssText += ';border:2px dashed var(--text);background:transparent';
2109
+ dot.className = 'timeline-legend-dot' + (items[i].dashed ? ' dashed' : '');
2110
+ if (items[i].color) dot.style.background = items[i].color;
2022
2111
  var label = document.createElement('span');
2023
2112
  label.textContent = items[i].label;
2024
2113
  if (items[i].strike) label.style.textDecoration = 'line-through';
@@ -2028,11 +2117,80 @@ function renderTimelineLegend() {
2028
2117
  }
2029
2118
  }
2030
2119
 
2120
+ function renderTimelineDomainFilters() {
2121
+ var container = document.getElementById('timeline-domain-filters');
2122
+ if (!container) return;
2123
+ clearElement(container);
2124
+
2125
+ var scoped = applyGlobalScope(state.decisions.data, 'scope');
2126
+ var domainSet = {};
2127
+ for (var i = 0; i < scoped.length; i++) {
2128
+ var domain = scoped[i].domain || 'other';
2129
+ domainSet[domain] = (domainSet[domain] || 0) + 1;
2130
+ }
2131
+
2132
+ var domains = Object.keys(domainSet).sort();
2133
+ if (domains.length <= 1) return; // No point showing filter for 1 domain
2134
+
2135
+ // "All" chip
2136
+ var allChip = document.createElement('button');
2137
+ allChip.className = 'timeline-domain-chip' + (timelineDomainFilter === null ? ' active' : '');
2138
+ allChip.textContent = 'All';
2139
+ allChip.style.setProperty('--domain-color', 'var(--accent)');
2140
+ allChip.addEventListener('click', function() {
2141
+ timelineDomainFilter = null;
2142
+ updateTimelineData();
2143
+ renderTimelineDomainFilters();
2144
+ });
2145
+ container.appendChild(allChip);
2146
+
2147
+ for (var j = 0; j < domains.length; j++) {
2148
+ (function(domain) {
2149
+ var color = DOMAIN_COLORS[domain] || DOMAIN_COLOR_DEFAULT;
2150
+ var isActive = timelineDomainFilter === null || !!timelineDomainFilter[domain];
2151
+ var chip = document.createElement('button');
2152
+ chip.className = 'timeline-domain-chip' + (timelineDomainFilter !== null && isActive ? ' active' : '');
2153
+ chip.style.setProperty('--domain-color', color);
2154
+ var dot = document.createElement('span');
2155
+ dot.className = 'chip-dot';
2156
+ chip.appendChild(dot);
2157
+ var text = document.createElement('span');
2158
+ text.textContent = domain + ' (' + domainSet[domain] + ')';
2159
+ chip.appendChild(text);
2160
+ chip.addEventListener('click', function() {
2161
+ if (timelineDomainFilter === null) {
2162
+ // Switch from "all" to just this domain
2163
+ timelineDomainFilter = {};
2164
+ timelineDomainFilter[domain] = true;
2165
+ } else if (timelineDomainFilter[domain]) {
2166
+ // Toggle off
2167
+ delete timelineDomainFilter[domain];
2168
+ if (Object.keys(timelineDomainFilter).length === 0) {
2169
+ timelineDomainFilter = null; // Back to all
2170
+ }
2171
+ } else {
2172
+ // Toggle on
2173
+ timelineDomainFilter[domain] = true;
2174
+ }
2175
+ updateTimelineData();
2176
+ renderTimelineDomainFilters();
2177
+ });
2178
+ container.appendChild(chip);
2179
+ })(domains[j]);
2180
+ }
2181
+ }
2182
+
2031
2183
  function updateTimelineData() {
2032
2184
  if (!window.timelineDataSet) return;
2185
+ var scoped = applyGlobalScope(state.decisions.data, 'scope');
2186
+ var groups = buildTimelineGroups(scoped);
2033
2187
  var items = buildTimelineItems(state.decisions.data);
2034
2188
  window.timelineDataSet.clear();
2035
2189
  window.timelineDataSet.add(items);
2190
+ if (window.timelineGroupSet) {
2191
+ window.timelineGroupSet.clear();
2192
+ window.timelineGroupSet.add(groups);
2193
+ }
2036
2194
  }
2037
2195
 
2038
2196
  function fetchTimelineDecisionDetail(id) {
@@ -2070,11 +2228,348 @@ var ENTITY_COLORS = {
2070
2228
  api_endpoint: '#ec4899'
2071
2229
  };
2072
2230
 
2231
+ // Track which entity types are visible (null = all)
2232
+ var graphTypeFilter = null;
2233
+
2234
+ /* ========== Blackboard Stream View ========== */
2235
+
2236
+ var ENTRY_TYPE_COLORS = {
2237
+ warning: '#ffaa00',
2238
+ constraint: '#ff4466',
2239
+ need: '#a78bfa',
2240
+ finding: '#6366f1',
2241
+ decision: '#00d4aa',
2242
+ question: '#22d3ee',
2243
+ answer: '#2dd4bf',
2244
+ status: '#8892a8',
2245
+ offer: '#10b981',
2246
+ artifact: '#fbbf24'
2247
+ };
2248
+
2249
+ var streamTypeFilter = null;
2250
+
2251
+ function getTimeGroupLabel(dateStr) {
2252
+ var d = new Date(dateStr);
2253
+ if (isNaN(d.getTime())) return 'Unknown';
2254
+ var now = new Date();
2255
+ var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
2256
+ var yesterday = new Date(today);
2257
+ yesterday.setDate(yesterday.getDate() - 1);
2258
+ var entryDate = new Date(d.getFullYear(), d.getMonth(), d.getDate());
2259
+ if (entryDate.getTime() === today.getTime()) return 'Today';
2260
+ if (entryDate.getTime() === yesterday.getTime()) return 'Yesterday';
2261
+ return d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
2262
+ }
2263
+
2264
+ function formatTimeOnly(dateStr) {
2265
+ var d = new Date(dateStr);
2266
+ if (isNaN(d.getTime())) return '--';
2267
+ return d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
2268
+ }
2269
+
2270
+ function renderStream() {
2271
+ var container = document.getElementById('stream-container');
2272
+ if (!container) return;
2273
+ clearElement(container);
2274
+
2275
+ var entries = applyGlobalScope(state.blackboard.data || [], 'scope');
2276
+
2277
+ // Apply type filter
2278
+ if (streamTypeFilter) {
2279
+ entries = entries.filter(function(e) {
2280
+ return !!streamTypeFilter[e.entry_type];
2281
+ });
2282
+ }
2283
+
2284
+ // Sort newest first
2285
+ entries = entries.slice().sort(function(a, b) {
2286
+ return new Date(b.timestamp) - new Date(a.timestamp);
2287
+ });
2288
+
2289
+ if (entries.length === 0) {
2290
+ container.appendChild(el('p', 'placeholder', 'No entries to display'));
2291
+ return;
2292
+ }
2293
+
2294
+ // Build ID lookup for visible entries (for threading)
2295
+ var visibleIds = {};
2296
+ for (var i = 0; i < entries.length; i++) {
2297
+ visibleIds[entries[i].id] = true;
2298
+ }
2299
+
2300
+ // Group by date and render cards
2301
+ var currentGroup = null;
2302
+ for (var i = 0; i < entries.length; i++) {
2303
+ var entry = entries[i];
2304
+ var groupLabel = getTimeGroupLabel(entry.timestamp);
2305
+
2306
+ // Insert time group header if new group
2307
+ if (groupLabel !== currentGroup) {
2308
+ currentGroup = groupLabel;
2309
+ var header = el('div', 'stream-time-group');
2310
+ var headerLine = el('span', 'stream-time-line');
2311
+ var headerLabel = el('span', 'stream-time-label', groupLabel);
2312
+ header.appendChild(headerLine);
2313
+ header.appendChild(headerLabel);
2314
+ header.appendChild(headerLine.cloneNode(true));
2315
+ container.appendChild(header);
2316
+ }
2317
+
2318
+ // Create card
2319
+ var typeColor = ENTRY_TYPE_COLORS[entry.entry_type] || '#6b7280';
2320
+ var card = el('div', 'stream-card');
2321
+ card.setAttribute('data-id', entry.id || '');
2322
+ card.style.setProperty('--card-color', typeColor);
2323
+
2324
+ if (state.blackboard.selectedId && entry.id === state.blackboard.selectedId) {
2325
+ card.classList.add('selected');
2326
+ }
2327
+
2328
+ // Type badge
2329
+ var badge = el('div', 'stream-card-badge');
2330
+ var badgeDot = el('span', 'stream-badge-dot');
2331
+ badge.appendChild(badgeDot);
2332
+ badge.appendChild(document.createTextNode(' ' + (entry.entry_type || 'unknown')));
2333
+ card.appendChild(badge);
2334
+
2335
+ // Summary
2336
+ var summary = el('div', 'stream-card-summary', truncate(entry.summary, 120));
2337
+ card.appendChild(summary);
2338
+
2339
+ // Footer: scope + time
2340
+ var footer = el('div', 'stream-card-footer');
2341
+ if (entry.scope) {
2342
+ footer.appendChild(el('span', 'stream-card-scope', truncate(entry.scope, 40)));
2343
+ }
2344
+ footer.appendChild(el('span', 'stream-card-time', formatTimeOnly(entry.timestamp)));
2345
+
2346
+ // Linked icon for off-screen relates_to
2347
+ if (entry.relates_to && entry.relates_to.length) {
2348
+ var hasOffscreen = false;
2349
+ for (var r = 0; r < entry.relates_to.length; r++) {
2350
+ if (!visibleIds[entry.relates_to[r]]) { hasOffscreen = true; break; }
2351
+ }
2352
+ if (hasOffscreen) {
2353
+ var linkIcon = el('span', 'stream-card-link', '\u{1F517}');
2354
+ linkIcon.title = 'Has related entries not visible in current filter';
2355
+ footer.appendChild(linkIcon);
2356
+ }
2357
+ }
2358
+
2359
+ card.appendChild(footer);
2360
+
2361
+ // Click handler — render detail in stream detail panel
2362
+ (function(e) {
2363
+ card.addEventListener('click', function() {
2364
+ state.blackboard.selectedId = e.id;
2365
+ var allCards = container.querySelectorAll('.stream-card');
2366
+ for (var c = 0; c < allCards.length; c++) {
2367
+ allCards[c].classList.remove('selected');
2368
+ }
2369
+ card.classList.add('selected');
2370
+ // Render detail directly into the stream detail panel
2371
+ renderStreamDetail(e);
2372
+ });
2373
+ })(entry);
2374
+
2375
+ container.appendChild(card);
2376
+ }
2377
+
2378
+ // Render thread lines (stub — implemented in Task 4)
2379
+ if (typeof renderStreamThreads === 'function') {
2380
+ renderStreamThreads(container, entries, visibleIds);
2381
+ }
2382
+
2383
+ // Render type filter chips (stub — implemented in Task 5)
2384
+ if (typeof renderStreamTypeFilters === 'function') {
2385
+ renderStreamTypeFilters();
2386
+ }
2387
+ }
2388
+
2389
+ function renderStreamDetail(entry) {
2390
+ var panel = document.getElementById('blackboard-stream-detail');
2391
+ if (!panel) return;
2392
+ clearElement(panel);
2393
+
2394
+ panel.appendChild(el('h3', null, 'Entry Details'));
2395
+
2396
+ var fields = [
2397
+ { label: 'ID', value: entry.id },
2398
+ { label: 'Timestamp', value: formatTimestamp(entry.timestamp) },
2399
+ { label: 'Type', value: entry.entry_type },
2400
+ { label: 'Summary', value: entry.summary },
2401
+ { label: 'Scope', value: entry.scope },
2402
+ { label: 'Agent ID', value: entry.agent_id },
2403
+ { label: 'Tags', value: (entry.tags && entry.tags.length) ? entry.tags.join(', ') : null }
2404
+ ];
2405
+
2406
+ for (var i = 0; i < fields.length; i++) {
2407
+ var f = fields[i];
2408
+ if (f.value === undefined || f.value === null) continue;
2409
+ var div = el('div', 'detail-field');
2410
+ div.appendChild(el('div', 'detail-label', f.label));
2411
+ if (f.label === 'ID') {
2412
+ var valDiv = el('div', 'detail-value');
2413
+ renderIdValue(valDiv, String(f.value));
2414
+ div.appendChild(valDiv);
2415
+ } else {
2416
+ div.appendChild(el('div', 'detail-value', String(f.value)));
2417
+ }
2418
+ panel.appendChild(div);
2419
+ }
2420
+
2421
+ // Relates To with clickable IDs
2422
+ if (entry.relates_to && entry.relates_to.length) {
2423
+ var rtDiv = el('div', 'detail-field');
2424
+ rtDiv.appendChild(el('div', 'detail-label', 'Relates To'));
2425
+ var rtVal = el('div', 'detail-value');
2426
+ renderIdList(rtVal, entry.relates_to);
2427
+ rtDiv.appendChild(rtVal);
2428
+ panel.appendChild(rtDiv);
2429
+ }
2430
+
2431
+ // Detail field (long text)
2432
+ if (entry.detail) {
2433
+ var detDiv = el('div', 'detail-field');
2434
+ detDiv.appendChild(el('div', 'detail-label', 'Detail'));
2435
+ var detVal = el('div', 'detail-value detail-long-text', entry.detail);
2436
+ detDiv.appendChild(detVal);
2437
+ panel.appendChild(detDiv);
2438
+ }
2439
+ }
2440
+
2441
+ function renderStreamThreads(container, entries, visibleIds) {
2442
+ // Build map of entry ID -> DOM card element
2443
+ var cardElements = {};
2444
+ var cards = container.querySelectorAll('.stream-card');
2445
+ for (var i = 0; i < cards.length; i++) {
2446
+ var id = cards[i].getAttribute('data-id');
2447
+ if (id) cardElements[id] = cards[i];
2448
+ }
2449
+
2450
+ // Use getBoundingClientRect for positioning (offsetTop unreliable with position:relative cards)
2451
+ var containerRect = container.getBoundingClientRect();
2452
+ var scrollTop = container.scrollTop;
2453
+
2454
+ function cardTop(card) {
2455
+ return card.getBoundingClientRect().top - containerRect.top + scrollTop;
2456
+ }
2457
+ function cardBottom(card) {
2458
+ return card.getBoundingClientRect().bottom - containerRect.top + scrollTop;
2459
+ }
2460
+
2461
+ // For each entry with relates_to, draw thread to visible targets
2462
+ for (var i = 0; i < entries.length; i++) {
2463
+ var entry = entries[i];
2464
+ if (!entry.relates_to || !entry.relates_to.length) continue;
2465
+
2466
+ var sourceCard = cardElements[entry.id];
2467
+ if (!sourceCard) continue;
2468
+
2469
+ for (var r = 0; r < entry.relates_to.length; r++) {
2470
+ var targetId = entry.relates_to[r];
2471
+ var targetCard = cardElements[targetId];
2472
+ if (!targetCard) continue;
2473
+
2474
+ // Determine which card is higher in the DOM
2475
+ var topEl, bottomEl;
2476
+ if (cardTop(sourceCard) < cardTop(targetCard)) {
2477
+ topEl = sourceCard;
2478
+ bottomEl = targetCard;
2479
+ } else {
2480
+ topEl = targetCard;
2481
+ bottomEl = sourceCard;
2482
+ }
2483
+
2484
+ var topBottom = cardBottom(topEl);
2485
+ var bottomTop = cardTop(bottomEl);
2486
+ var height = bottomTop - topBottom;
2487
+
2488
+ if (height > 0) {
2489
+ var thread = el('div', 'stream-thread');
2490
+ var typeColor = ENTRY_TYPE_COLORS[entry.entry_type] || '#6b7280';
2491
+ thread.style.setProperty('--thread-color', typeColor);
2492
+ thread.style.top = topBottom + 'px';
2493
+ thread.style.height = height + 'px';
2494
+ container.appendChild(thread);
2495
+ }
2496
+ }
2497
+ }
2498
+ }
2499
+
2500
+ function renderStreamTypeFilters() {
2501
+ var container = document.getElementById('stream-type-filters');
2502
+ if (!container) return;
2503
+ clearElement(container);
2504
+
2505
+ // Count entry types from scoped data
2506
+ var scoped = applyGlobalScope(state.blackboard.data || [], 'scope');
2507
+ var typeSet = {};
2508
+ for (var i = 0; i < scoped.length; i++) {
2509
+ var t = scoped[i].entry_type || 'unknown';
2510
+ typeSet[t] = (typeSet[t] || 0) + 1;
2511
+ }
2512
+
2513
+ var types = Object.keys(typeSet).sort(function(a, b) {
2514
+ return typeSet[b] - typeSet[a];
2515
+ });
2516
+
2517
+ if (types.length === 0) return;
2518
+
2519
+ // "All" chip
2520
+ var allChip = document.createElement('button');
2521
+ allChip.className = 'stream-type-chip' + (streamTypeFilter === null ? ' active' : '');
2522
+ allChip.style.setProperty('--type-color', 'var(--accent)');
2523
+ var allDot = document.createElement('span');
2524
+ allDot.className = 'chip-dot';
2525
+ allChip.appendChild(allDot);
2526
+ var allText = document.createElement('span');
2527
+ allText.textContent = 'All';
2528
+ allChip.appendChild(allText);
2529
+ allChip.addEventListener('click', function() {
2530
+ streamTypeFilter = null;
2531
+ renderStream();
2532
+ });
2533
+ container.appendChild(allChip);
2534
+
2535
+ // Per-type chips
2536
+ for (var j = 0; j < types.length; j++) {
2537
+ (function(type) {
2538
+ var color = ENTRY_TYPE_COLORS[type] || '#6b7280';
2539
+ var isActive = streamTypeFilter === null || !!streamTypeFilter[type];
2540
+ var chip = document.createElement('button');
2541
+ chip.className = 'stream-type-chip' + (isActive ? ' active' : '');
2542
+ chip.style.setProperty('--type-color', color);
2543
+ var dot = document.createElement('span');
2544
+ dot.className = 'chip-dot';
2545
+ chip.appendChild(dot);
2546
+ var text = document.createElement('span');
2547
+ text.textContent = type + ' (' + typeSet[type] + ')';
2548
+ chip.appendChild(text);
2549
+ chip.addEventListener('click', function() {
2550
+ if (streamTypeFilter === null) {
2551
+ streamTypeFilter = {};
2552
+ streamTypeFilter[type] = true;
2553
+ } else if (streamTypeFilter[type]) {
2554
+ delete streamTypeFilter[type];
2555
+ if (Object.keys(streamTypeFilter).length === 0) streamTypeFilter = null;
2556
+ } else {
2557
+ streamTypeFilter[type] = true;
2558
+ }
2559
+ renderStream();
2560
+ });
2561
+ container.appendChild(chip);
2562
+ })(types[j]);
2563
+ }
2564
+ }
2565
+
2073
2566
  function buildGraphStyles() {
2074
2567
  var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
2075
- var textColor = isDark ? '#e2e8f0' : '#1a1a2e';
2076
- var edgeColor = isDark ? '#64748b' : '#94a3b8';
2077
- var accentColor = isDark ? '#60a5fa' : '#3b82f6';
2568
+ var textColor = isDark ? '#c8d0e0' : '#1a1a2e';
2569
+ var textHaloColor = isDark ? 'rgba(11, 15, 26, 0.85)' : 'rgba(255, 255, 255, 0.85)';
2570
+ var edgeColor = isDark ? 'rgba(100, 116, 139, 0.5)' : 'rgba(148, 163, 184, 0.6)';
2571
+ var edgeLabelColor = isDark ? '#64748b' : '#94a3b8';
2572
+ var accentColor = isDark ? '#00d4aa' : '#00a88a';
2078
2573
 
2079
2574
  var styles = [
2080
2575
  {
@@ -2083,36 +2578,112 @@ function buildGraphStyles() {
2083
2578
  'label': 'data(label)',
2084
2579
  'text-valign': 'bottom',
2085
2580
  'text-halign': 'center',
2086
- 'font-size': '11px',
2087
- 'width': 40,
2088
- 'height': 40,
2581
+ 'font-size': '10px',
2582
+ 'font-weight': 500,
2583
+ 'width': 32,
2584
+ 'height': 32,
2089
2585
  'color': textColor,
2090
2586
  'text-margin-y': 6,
2091
2587
  'background-color': '#6b7280',
2092
- 'text-wrap': 'wrap',
2093
- 'text-max-width': '120px'
2588
+ 'background-opacity': 0.9,
2589
+ 'border-width': 2,
2590
+ 'border-color': 'rgba(255, 255, 255, 0.08)',
2591
+ 'border-opacity': 1,
2592
+ 'text-wrap': 'ellipsis',
2593
+ 'text-max-width': '100px',
2594
+ 'text-outline-width': 0,
2595
+ 'text-background-opacity': 0.8,
2596
+ 'text-background-color': textHaloColor,
2597
+ 'text-background-padding': '2px',
2598
+ 'text-background-shape': 'roundrectangle',
2599
+ 'overlay-opacity': 0,
2600
+ 'transition-property': 'border-width, border-color, width, height, background-opacity',
2601
+ 'transition-duration': '0.15s'
2602
+ }
2603
+ },
2604
+ {
2605
+ selector: 'node:active',
2606
+ style: {
2607
+ 'overlay-opacity': 0
2094
2608
  }
2095
2609
  },
2096
2610
  {
2097
2611
  selector: 'edge',
2098
2612
  style: {
2099
- 'width': 2,
2613
+ 'width': 1.5,
2100
2614
  'line-color': edgeColor,
2101
2615
  'target-arrow-color': edgeColor,
2102
2616
  'target-arrow-shape': 'triangle',
2617
+ 'arrow-scale': 0.8,
2103
2618
  'curve-style': 'bezier',
2104
2619
  'label': 'data(label)',
2105
2620
  'font-size': '8px',
2106
- 'color': textColor,
2621
+ 'color': edgeLabelColor,
2107
2622
  'text-rotation': 'autorotate',
2108
- 'text-margin-y': -8
2623
+ 'text-margin-y': -8,
2624
+ 'text-outline-width': 0,
2625
+ 'text-background-opacity': 0.75,
2626
+ 'text-background-color': textHaloColor,
2627
+ 'text-background-padding': '1px',
2628
+ 'text-background-shape': 'roundrectangle',
2629
+ 'opacity': 0.7,
2630
+ 'transition-property': 'opacity, width, line-color',
2631
+ 'transition-duration': '0.15s'
2109
2632
  }
2110
2633
  },
2111
2634
  {
2112
2635
  selector: 'node:selected',
2113
2636
  style: {
2114
2637
  'border-width': 3,
2115
- 'border-color': accentColor
2638
+ 'border-color': accentColor,
2639
+ 'border-opacity': 1,
2640
+ 'width': 40,
2641
+ 'height': 40,
2642
+ 'background-opacity': 1,
2643
+ 'overlay-color': accentColor,
2644
+ 'overlay-padding': 6,
2645
+ 'overlay-opacity': 0.15,
2646
+ 'z-index': 10
2647
+ }
2648
+ },
2649
+ {
2650
+ selector: 'node.hover',
2651
+ style: {
2652
+ 'border-width': 2.5,
2653
+ 'border-color': accentColor,
2654
+ 'width': 36,
2655
+ 'height': 36,
2656
+ 'background-opacity': 1,
2657
+ 'z-index': 5
2658
+ }
2659
+ },
2660
+ {
2661
+ selector: 'node.dimmed',
2662
+ style: {
2663
+ 'opacity': 0.2,
2664
+ 'text-opacity': 0.15
2665
+ }
2666
+ },
2667
+ {
2668
+ selector: 'edge.dimmed',
2669
+ style: {
2670
+ 'opacity': 0.08
2671
+ }
2672
+ },
2673
+ {
2674
+ selector: 'node.highlighted',
2675
+ style: {
2676
+ 'opacity': 1,
2677
+ 'text-opacity': 1
2678
+ }
2679
+ },
2680
+ {
2681
+ selector: 'edge.highlighted',
2682
+ style: {
2683
+ 'opacity': 1,
2684
+ 'width': 2.5,
2685
+ 'line-color': accentColor,
2686
+ 'target-arrow-color': accentColor
2116
2687
  }
2117
2688
  }
2118
2689
  ];
@@ -2139,10 +2710,9 @@ function renderGraphLegend() {
2139
2710
  for (var i = 0; i < types.length; i++) {
2140
2711
  var item = document.createElement('span');
2141
2712
  item.className = 'graph-legend-item';
2142
- item.style.cssText = 'display:inline-flex;align-items:center;gap:4px;margin-right:12px;font-size:0.8rem;';
2143
2713
  var dot = document.createElement('span');
2144
2714
  dot.className = 'graph-legend-dot';
2145
- dot.style.cssText = 'width:10px;height:10px;border-radius:50%;display:inline-block;background:' + ENTITY_COLORS[types[i]];
2715
+ dot.style.background = ENTITY_COLORS[types[i]];
2146
2716
  var label = document.createElement('span');
2147
2717
  label.textContent = types[i];
2148
2718
  item.appendChild(dot);
@@ -2151,6 +2721,90 @@ function renderGraphLegend() {
2151
2721
  }
2152
2722
  }
2153
2723
 
2724
+ function renderGraphTypeFilters() {
2725
+ var container = document.getElementById('graph-type-filters');
2726
+ if (!container) return;
2727
+ clearElement(container);
2728
+
2729
+ // Count entity types
2730
+ var typeSet = {};
2731
+ var scoped = applyGlobalScope(state.graph.data, 'scope');
2732
+ for (var i = 0; i < scoped.length; i++) {
2733
+ var t = scoped[i].type || 'concept';
2734
+ typeSet[t] = (typeSet[t] || 0) + 1;
2735
+ }
2736
+ var types = Object.keys(typeSet).sort();
2737
+ if (types.length <= 1) return;
2738
+
2739
+ // "All" chip
2740
+ var allChip = document.createElement('button');
2741
+ allChip.className = 'graph-type-chip' + (graphTypeFilter === null ? ' active' : '');
2742
+ allChip.textContent = 'All';
2743
+ allChip.style.setProperty('--type-color', 'var(--accent)');
2744
+ allChip.addEventListener('click', function() {
2745
+ graphTypeFilter = null;
2746
+ applyGraphTypeFilter();
2747
+ renderGraphTypeFilters();
2748
+ });
2749
+ container.appendChild(allChip);
2750
+
2751
+ for (var j = 0; j < types.length; j++) {
2752
+ (function(type) {
2753
+ var color = ENTITY_COLORS[type] || '#6b7280';
2754
+ var isActive = graphTypeFilter === null || !!graphTypeFilter[type];
2755
+ var chip = document.createElement('button');
2756
+ chip.className = 'graph-type-chip' + (graphTypeFilter !== null && isActive ? ' active' : '');
2757
+ chip.style.setProperty('--type-color', color);
2758
+ var dot = document.createElement('span');
2759
+ dot.className = 'chip-dot';
2760
+ chip.appendChild(dot);
2761
+ var text = document.createElement('span');
2762
+ text.textContent = type + ' (' + typeSet[type] + ')';
2763
+ chip.appendChild(text);
2764
+ chip.addEventListener('click', function() {
2765
+ if (graphTypeFilter === null) {
2766
+ graphTypeFilter = {};
2767
+ graphTypeFilter[type] = true;
2768
+ } else if (graphTypeFilter[type]) {
2769
+ delete graphTypeFilter[type];
2770
+ if (Object.keys(graphTypeFilter).length === 0) graphTypeFilter = null;
2771
+ } else {
2772
+ graphTypeFilter[type] = true;
2773
+ }
2774
+ applyGraphTypeFilter();
2775
+ renderGraphTypeFilters();
2776
+ });
2777
+ container.appendChild(chip);
2778
+ })(types[j]);
2779
+ }
2780
+ }
2781
+
2782
+ function applyGraphTypeFilter() {
2783
+ if (!window.cyInstance) return;
2784
+ if (graphTypeFilter === null) {
2785
+ // Show all
2786
+ window.cyInstance.nodes().show();
2787
+ window.cyInstance.edges().show();
2788
+ } else {
2789
+ window.cyInstance.nodes().forEach(function(n) {
2790
+ var type = n.data('type') || 'concept';
2791
+ if (graphTypeFilter[type]) {
2792
+ n.show();
2793
+ } else {
2794
+ n.hide();
2795
+ }
2796
+ });
2797
+ // Only show edges where both endpoints are visible
2798
+ window.cyInstance.edges().forEach(function(e) {
2799
+ if (e.source().visible() && e.target().visible()) {
2800
+ e.show();
2801
+ } else {
2802
+ e.hide();
2803
+ }
2804
+ });
2805
+ }
2806
+ }
2807
+
2154
2808
  function buildGraphElements() {
2155
2809
  var entities = state.graph.data;
2156
2810
  var relations = state.graph.relations;
@@ -2201,12 +2855,14 @@ function initGraphVis() {
2201
2855
  canvas.appendChild(msg);
2202
2856
  }
2203
2857
  renderGraphLegend();
2858
+ renderGraphTypeFilters();
2204
2859
  return;
2205
2860
  }
2206
2861
 
2207
2862
  // Create-once guard: if already initialized, just update data
2208
2863
  if (window.cyInstance) {
2209
2864
  updateGraphData();
2865
+ renderGraphTypeFilters();
2210
2866
  return;
2211
2867
  }
2212
2868
 
@@ -2225,16 +2881,34 @@ function initGraphVis() {
2225
2881
  layout: {
2226
2882
  name: 'cose',
2227
2883
  animate: true,
2228
- animationDuration: 500,
2229
- nodeRepulsion: function() { return 12000; },
2230
- idealEdgeLength: function() { return 100; },
2231
- nodeOverlap: 20,
2232
- padding: 40
2884
+ animationDuration: 600,
2885
+ animationEasing: 'ease-out',
2886
+ nodeRepulsion: function() { return 14000; },
2887
+ idealEdgeLength: function() { return 120; },
2888
+ nodeOverlap: 24,
2889
+ padding: 50
2233
2890
  },
2234
2891
  style: buildGraphStyles(),
2235
- minZoom: 0.2,
2892
+ minZoom: 0.15,
2236
2893
  maxZoom: 5,
2237
- wheelSensitivity: 0.2
2894
+ wheelSensitivity: 0.3,
2895
+ pixelRatio: 'auto'
2896
+ });
2897
+
2898
+ // Hover: highlight node and its neighborhood
2899
+ window.cyInstance.on('mouseover', 'node', function(evt) {
2900
+ var node = evt.target;
2901
+ node.addClass('hover');
2902
+ // Dim everything, highlight neighborhood
2903
+ var neighborhood = node.closedNeighborhood();
2904
+ window.cyInstance.elements().not(neighborhood).addClass('dimmed');
2905
+ neighborhood.addClass('highlighted');
2906
+ neighborhood.connectedEdges().addClass('highlighted');
2907
+ });
2908
+
2909
+ window.cyInstance.on('mouseout', 'node', function(evt) {
2910
+ evt.target.removeClass('hover');
2911
+ window.cyInstance.elements().removeClass('dimmed highlighted');
2238
2912
  });
2239
2913
 
2240
2914
  // Click node to show detail and expand neighbors
@@ -2261,7 +2935,15 @@ function initGraphVis() {
2261
2935
  }
2262
2936
  });
2263
2937
 
2938
+ // Click background to clear highlight
2939
+ window.cyInstance.on('tap', function(evt) {
2940
+ if (evt.target === window.cyInstance) {
2941
+ window.cyInstance.elements().removeClass('dimmed highlighted hover');
2942
+ }
2943
+ });
2944
+
2264
2945
  renderGraphLegend();
2946
+ renderGraphTypeFilters();
2265
2947
  setupGraphControls();
2266
2948
  }
2267
2949
 
@@ -2273,33 +2955,42 @@ function setupGraphControls() {
2273
2955
 
2274
2956
  if (zoomIn) {
2275
2957
  zoomIn.onclick = function() {
2276
- if (window.cyInstance) window.cyInstance.zoom(window.cyInstance.zoom() * 1.2);
2958
+ if (window.cyInstance) {
2959
+ window.cyInstance.animate({ zoom: window.cyInstance.zoom() * 1.3, center: window.cyInstance.extent() }, { duration: 250, easing: 'ease-in-out-quad' });
2960
+ }
2277
2961
  };
2278
2962
  }
2279
2963
 
2280
2964
  if (zoomOut) {
2281
2965
  zoomOut.onclick = function() {
2282
- if (window.cyInstance) window.cyInstance.zoom(window.cyInstance.zoom() * 0.8);
2966
+ if (window.cyInstance) {
2967
+ window.cyInstance.animate({ zoom: window.cyInstance.zoom() * 0.7, center: window.cyInstance.extent() }, { duration: 250, easing: 'ease-in-out-quad' });
2968
+ }
2283
2969
  };
2284
2970
  }
2285
2971
 
2286
2972
  if (fit) {
2287
2973
  fit.onclick = function() {
2288
- if (window.cyInstance) window.cyInstance.fit(null, 40);
2974
+ if (window.cyInstance) window.cyInstance.animate({ fit: { padding: 50 } }, { duration: 400, easing: 'ease-in-out-quad' });
2289
2975
  };
2290
2976
  }
2291
2977
 
2292
2978
  if (reset) {
2293
2979
  reset.onclick = function() {
2294
2980
  if (window.cyInstance) {
2981
+ // Reset type filter
2982
+ graphTypeFilter = null;
2983
+ applyGraphTypeFilter();
2984
+ renderGraphTypeFilters();
2295
2985
  var layout = window.cyInstance.layout({
2296
2986
  name: 'cose',
2297
2987
  animate: true,
2298
- animationDuration: 500,
2299
- nodeRepulsion: function() { return 12000; },
2300
- idealEdgeLength: function() { return 100; },
2301
- nodeOverlap: 20,
2302
- padding: 40
2988
+ animationDuration: 600,
2989
+ animationEasing: 'ease-out',
2990
+ nodeRepulsion: function() { return 14000; },
2991
+ idealEdgeLength: function() { return 120; },
2992
+ nodeOverlap: 24,
2993
+ padding: 50
2303
2994
  });
2304
2995
  layout.run();
2305
2996
  }
@@ -2655,9 +3346,14 @@ document.addEventListener("DOMContentLoaded", function() {
2655
3346
 
2656
3347
  // Search input: debounced performSearch on "input" event
2657
3348
  var searchInput = document.getElementById("search-input");
3349
+ var clearBtn = document.getElementById("search-clear-btn");
2658
3350
  if (searchInput) {
2659
3351
  var debouncedSearch = debounce(performSearch, 300);
2660
- searchInput.addEventListener("input", debouncedSearch);
3352
+ searchInput.addEventListener("input", function() {
3353
+ // Show/hide clear icon based on input content
3354
+ if (clearBtn) clearBtn.style.display = searchInput.value ? "flex" : "none";
3355
+ debouncedSearch();
3356
+ });
2661
3357
  // Enter key: immediate performSearch (bypass debounce)
2662
3358
  searchInput.addEventListener("keydown", function(e) {
2663
3359
  if (e.key === "Enter") {
@@ -2667,18 +3363,12 @@ document.addEventListener("DOMContentLoaded", function() {
2667
3363
  });
2668
3364
  }
2669
3365
 
2670
- // Search button: performSearch on click
2671
- var searchBtn = document.getElementById("search-btn");
2672
- if (searchBtn) {
2673
- searchBtn.addEventListener("click", performSearch);
2674
- }
2675
-
2676
3366
  // Clear button: clear search input, reset search state, switch to stats tab
2677
- var clearBtn = document.getElementById("search-clear-btn");
2678
3367
  if (clearBtn) {
2679
3368
  clearBtn.addEventListener("click", function() {
2680
3369
  var input = document.getElementById("search-input");
2681
3370
  if (input) input.value = "";
3371
+ clearBtn.style.display = "none";
2682
3372
  state.search.query = "";
2683
3373
  state.search.results = [];
2684
3374
  state.search.selectedId = null;
@@ -2688,6 +3378,21 @@ document.addEventListener("DOMContentLoaded", function() {
2688
3378
  });
2689
3379
  }
2690
3380
 
3381
+ // Search type chips: toggle active state
3382
+ var typeChips = document.querySelectorAll(".search-chip[data-type]");
3383
+ for (var tc = 0; tc < typeChips.length; tc++) {
3384
+ (function(chip) {
3385
+ chip.addEventListener("click", function() {
3386
+ chip.classList.toggle("active");
3387
+ // Ensure at least one type remains active
3388
+ var active = document.querySelectorAll(".search-chip.active[data-type]");
3389
+ if (active.length === 0) chip.classList.add("active");
3390
+ // Re-run search if there's a query
3391
+ if (state.search.query) performSearch();
3392
+ });
3393
+ })(typeChips[tc]);
3394
+ }
3395
+
2691
3396
  // Global scope filter
2692
3397
  var globalScopeInput = document.getElementById("global-scope");
2693
3398
  if (globalScopeInput) {
@@ -2704,6 +3409,7 @@ document.addEventListener("DOMContentLoaded", function() {
2704
3409
  if (indicator2) indicator2.style.display = "none";
2705
3410
  }
2706
3411
  // Reset pagination, re-render active tab
3412
+ streamTypeFilter = null;
2707
3413
  state.blackboard.page = 1;
2708
3414
  state.decisions.page = 1;
2709
3415
  state.graph.page = 1;