<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>lean-ctx Visualizer</title>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--bg-card: #1c2128;
--border: #30363d;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-muted: #6e7681;
--accent-blue: #58a6ff;
--accent-green: #3fb950;
--accent-orange: #d29922;
--accent-red: #f85149;
--accent-purple: #bc8cff;
--accent-cyan: #39d2c0;
--shadow: 0 1px 3px rgba(0,0,0,0.4);
--radius: 8px;
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
--mono: 'SF Mono', 'Fira Code', Consolas, monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font);
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.5;
overflow-x: hidden;
}
a { color: var(--accent-blue); text-decoration: none; }
a:hover { text-decoration: underline; }
.header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
padding: 12px 24px;
display: flex;
align-items: center;
gap: 16px;
position: sticky;
top: 0;
z-index: 100;
}
.header .logo {
font-size: 18px;
font-weight: 700;
color: var(--accent-cyan);
letter-spacing: -0.5px;
}
.header .logo span { color: var(--text-muted); font-weight: 400; }
.tabs {
display: flex;
gap: 4px;
margin-left: auto;
}
.tab {
padding: 6px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
border: 1px solid transparent;
background: transparent;
transition: all 0.15s ease;
user-select: none;
}
.tab:hover { color: var(--text-primary); background: var(--bg-tertiary); }
.tab.active {
color: var(--text-primary);
background: var(--bg-tertiary);
border-color: var(--border);
}
.search-bar {
padding: 8px 24px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
}
.search-bar input {
width: 100%;
max-width: 400px;
padding: 6px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-size: 13px;
outline: none;
}
.search-bar input::placeholder { color: var(--text-muted); }
.search-bar input:focus { border-color: var(--accent-blue); }
.panel { display: none; padding: 24px; min-height: calc(100vh - 100px); }
.panel.active { display: block; }
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
margin-bottom: 16px;
box-shadow: var(--shadow);
}
.card-title {
font-size: 14px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin-bottom: 24px;
}
.stat-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
text-align: center;
}
.stat-value {
font-size: 28px;
font-weight: 700;
line-height: 1.2;
}
.stat-label {
font-size: 12px;
color: var(--text-muted);
margin-top: 4px;
}
.stat-blue .stat-value { color: var(--accent-blue); }
.stat-green .stat-value { color: var(--accent-green); }
.stat-orange .stat-value { color: var(--accent-orange); }
.stat-purple .stat-value { color: var(--accent-purple); }
.stat-cyan .stat-value { color: var(--accent-cyan); }
.stat-red .stat-value { color: var(--accent-red); }
#graph-container {
width: 100%;
height: calc(100vh - 200px);
min-height: 500px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
position: relative;
}
#graph-container svg { width: 100%; height: 100%; }
.graph-controls {
position: absolute;
top: 12px;
right: 12px;
display: flex;
gap: 6px;
z-index: 10;
}
.graph-btn {
padding: 6px 10px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-secondary);
cursor: pointer;
font-size: 12px;
}
.graph-btn:hover { color: var(--text-primary); border-color: var(--accent-blue); }
.tooltip {
position: absolute;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 12px;
font-size: 12px;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
z-index: 200;
max-width: 320px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
}
.tooltip.visible { opacity: 1; }
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.data-table th {
text-align: left;
padding: 8px 12px;
border-bottom: 2px solid var(--border);
color: var(--text-muted);
font-weight: 600;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
position: sticky;
top: 0;
background: var(--bg-card);
cursor: pointer;
user-select: none;
}
.data-table th:hover { color: var(--text-primary); }
.data-table td {
padding: 6px 12px;
border-bottom: 1px solid var(--border);
color: var(--text-secondary);
}
.data-table tr:hover td { background: rgba(88,166,255,0.04); color: var(--text-primary); }
.data-table .mono { font-family: var(--mono); font-size: 12px; }
.data-table .path { color: var(--accent-blue); max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.progress-bar {
height: 6px;
background: var(--bg-tertiary);
border-radius: 3px;
overflow: hidden;
min-width: 60px;
}
.progress-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
}
.badge-blue { background: rgba(88,166,255,0.15); color: var(--accent-blue); }
.badge-green { background: rgba(63,185,80,0.15); color: var(--accent-green); }
.badge-orange { background: rgba(210,153,34,0.15); color: var(--accent-orange); }
.badge-red { background: rgba(248,81,73,0.15); color: var(--accent-red); }
.badge-purple { background: rgba(188,140,255,0.15); color: var(--accent-purple); }
.badge-cyan { background: rgba(57,210,192,0.15); color: var(--accent-cyan); }
.timeline { position: relative; padding-left: 24px; }
.timeline::before {
content: '';
position: absolute;
left: 8px;
top: 0;
bottom: 0;
width: 2px;
background: var(--border);
}
.timeline-item {
position: relative;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.timeline-item::before {
content: '';
position: absolute;
left: -20px;
top: 16px;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--accent-blue);
border: 2px solid var(--bg-primary);
}
.timeline-item.decision::before { background: var(--accent-purple); }
.timeline-item.finding::before { background: var(--accent-green); }
.timeline-item.progress::before { background: var(--accent-orange); }
.timeline-time {
font-size: 11px;
color: var(--text-muted);
font-family: var(--mono);
}
.timeline-text { font-size: 13px; margin-top: 4px; }
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-muted);
}
.empty-state h3 { font-size: 16px; margin-bottom: 8px; color: var(--text-secondary); }
.legend {
display: flex;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 16px;
font-size: 12px;
color: var(--text-muted);
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
@media (max-width: 768px) {
.stats-row { grid-template-columns: repeat(2, 1fr); }
.header { flex-wrap: wrap; }
.tabs { margin-left: 0; width: 100%; }
}
</style>
</head>
<body>
<div class="header">
<div class="logo">lean-ctx <span>visualizer</span></div>
<div class="tabs">
<div class="tab active" data-panel="graph">Dependency Graph</div>
<div class="tab" data-panel="knowledge">Knowledge</div>
<div class="tab" data-panel="savings">Token Savings</div>
<div class="tab" data-panel="session">Session</div>
</div>
</div>
<div class="search-bar">
<input type="text" id="global-search" placeholder="Search files, knowledge, events...">
</div>
<div class="tooltip" id="tooltip"></div>
<div class="panel active" id="panel-graph">
<div class="stats-row">
<div class="stat-card stat-blue"><div class="stat-value" id="sg-nodes">0</div><div class="stat-label">Nodes</div></div>
<div class="stat-card stat-green"><div class="stat-value" id="sg-edges">0</div><div class="stat-label">Edges</div></div>
<div class="stat-card stat-purple"><div class="stat-value" id="sg-components">0</div><div class="stat-label">Components</div></div>
</div>
<div class="legend" id="edge-legend"></div>
<div id="graph-container">
<div class="graph-controls">
<button class="graph-btn" onclick="resetZoom()">Reset Zoom</button>
<button class="graph-btn" onclick="toggleLabels()">Toggle Labels</button>
</div>
<svg id="graph-svg"></svg>
</div>
</div>
<div class="panel" id="panel-knowledge">
<div class="stats-row">
<div class="stat-card stat-cyan"><div class="stat-value" id="sk-total">0</div><div class="stat-label">Total Facts</div></div>
<div class="stat-card stat-purple"><div class="stat-value" id="sk-categories">0</div><div class="stat-label">Categories</div></div>
<div class="stat-card stat-green"><div class="stat-value" id="sk-avg-conf">0%</div><div class="stat-label">Avg Confidence</div></div>
<div class="stat-card stat-orange"><div class="stat-value" id="sk-retrievals">0</div><div class="stat-label">Total Retrievals</div></div>
</div>
<div class="card">
<div class="card-title">Knowledge Facts</div>
<div style="overflow-x:auto;">
<table class="data-table" id="knowledge-table">
<thead><tr>
<th data-sort="archetype">Archetype</th>
<th data-sort="category">Category</th>
<th data-sort="key">Key</th>
<th>Value</th>
<th data-sort="confidence">Confidence</th>
<th data-sort="retrievals">Retrievals</th>
<th data-sort="created">Created</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<div class="panel" id="panel-savings">
<div class="stats-row">
<div class="stat-card stat-green"><div class="stat-value" id="ss-saved">0</div><div class="stat-label">Tokens Saved</div></div>
<div class="stat-card stat-blue"><div class="stat-value" id="ss-original">0</div><div class="stat-label">Original Tokens</div></div>
<div class="stat-card stat-cyan"><div class="stat-value" id="ss-ratio">0%</div><div class="stat-label">Overall Compression</div></div>
<div class="stat-card stat-orange"><div class="stat-value" id="ss-files">0</div><div class="stat-label">Files Tracked</div></div>
</div>
<div class="card">
<div class="card-title">Per-File Compression</div>
<div style="overflow-x:auto;">
<table class="data-table" id="savings-table">
<thead><tr>
<th data-sort="path">File</th>
<th data-sort="accesses">Accesses</th>
<th data-sort="original">Original Tokens</th>
<th data-sort="saved">Saved Tokens</th>
<th data-sort="ratio">Compression</th>
<th>Bar</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<div class="panel" id="panel-session">
<div class="stats-row">
<div class="stat-card stat-blue"><div class="stat-value" id="sh-calls">0</div><div class="stat-label">Tool Calls</div></div>
<div class="stat-card stat-green"><div class="stat-value" id="sh-saved">0</div><div class="stat-label">Tokens Saved</div></div>
<div class="stat-card stat-orange"><div class="stat-value" id="sh-cache">0</div><div class="stat-label">Cache Hits</div></div>
<div class="stat-card stat-purple"><div class="stat-value" id="sh-files">0</div><div class="stat-label">Files Read</div></div>
<div class="stat-card stat-cyan"><div class="stat-value" id="sh-cmds">0</div><div class="stat-label">Commands Run</div></div>
</div>
<div id="session-task" class="card" style="display:none">
<div class="card-title">Current Task</div>
<div id="session-task-text" style="font-size:14px;"></div>
</div>
<div class="card">
<div class="card-title">Files Touched</div>
<div style="overflow-x:auto;">
<table class="data-table" id="files-table">
<thead><tr>
<th>File</th>
<th>Reads</th>
<th>Modified</th>
<th>Mode</th>
<th>Tokens</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-title">Timeline</div>
<div class="timeline" id="session-timeline"></div>
</div>
</div>
<script>
function fmt(n) {
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
return String(n);
}
function pct(n) { return (n * 100).toFixed(1) + '%'; }
function shortDate(iso) {
if (!iso) return '-';
const d = new Date(iso);
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}
function escHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
tab.classList.add('active');
document.getElementById('panel-' + tab.dataset.panel).classList.add('active');
});
});
const searchInput = document.getElementById('global-search');
searchInput.addEventListener('input', () => {
const q = searchInput.value.toLowerCase();
document.querySelectorAll('.data-table tbody tr').forEach(row => {
row.style.display = row.textContent.toLowerCase().includes(q) ? '' : 'none';
});
document.querySelectorAll('.timeline-item').forEach(item => {
item.style.display = item.textContent.toLowerCase().includes(q) ? '' : 'none';
});
});
document.querySelectorAll('.data-table th[data-sort]').forEach(th => {
th.addEventListener('click', () => {
const table = th.closest('table');
const tbody = table.querySelector('tbody');
const idx = Array.from(th.parentNode.children).indexOf(th);
const asc = th.dataset.dir !== 'asc';
th.dataset.dir = asc ? 'asc' : 'desc';
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
const av = (a.children[idx]?.dataset?.val ?? a.children[idx]?.textContent ?? '').trim();
const bv = (b.children[idx]?.dataset?.val ?? b.children[idx]?.textContent ?? '').trim();
const an = parseFloat(av), bn = parseFloat(bv);
if (!isNaN(an) && !isNaN(bn)) return asc ? an - bn : bn - an;
return asc ? av.localeCompare(bv) : bv.localeCompare(av);
});
rows.forEach(r => tbody.appendChild(r));
});
});
const tooltip = document.getElementById('tooltip');
function showTooltip(x, y, html) {
tooltip.innerHTML = html;
tooltip.style.left = (x + 12) + 'px';
tooltip.style.top = (y - 10) + 'px';
tooltip.classList.add('visible');
}
function hideTooltip() { tooltip.classList.remove('visible'); }
(function renderGraph() {
const { nodes, edges } = DATA.graph;
document.getElementById('sg-nodes').textContent = fmt(nodes.length);
document.getElementById('sg-edges').textContent = fmt(edges.length);
if (nodes.length === 0) {
document.getElementById('graph-container').innerHTML =
'<div class="empty-state"><h3>No graph data</h3><p>Run <code>lean-ctx index</code> to build the dependency graph.</p></div>';
document.getElementById('sg-components').textContent = '0';
return;
}
const parent = {};
nodes.forEach(n => parent[n.id] = n.id);
function find(x) { return parent[x] === x ? x : (parent[x] = find(parent[x])); }
edges.forEach(e => { parent[find(e.source)] = find(e.target); });
const roots = new Set(nodes.map(n => find(n.id)));
document.getElementById('sg-components').textContent = roots.size;
const edgeColors = {
imports: '#58a6ff', calls: '#3fb950', defines: '#d29922',
exports: '#bc8cff', type_ref: '#39d2c0', module: '#f0883e',
cochange: '#8b949e', sibling: '#6e7681',
tested_by: '#f85149', changed_in: '#d29922',
mentioned_in: '#8b949e', affects: '#f0883e', breaks: '#f85149',
built_in: '#6e7681'
};
const usedKinds = [...new Set(edges.map(e => e.kind))];
const legend = document.getElementById('edge-legend');
usedKinds.forEach(k => {
const item = document.createElement('div');
item.className = 'legend-item';
item.innerHTML = `<span class="legend-dot" style="background:${edgeColors[k] || '#8b949e'}"></span>${k}`;
legend.appendChild(item);
});
const svg = document.getElementById('graph-svg');
const width = svg.parentElement.clientWidth;
const height = svg.parentElement.clientHeight;
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
const degree = {};
nodes.forEach(n => degree[n.id] = 0);
edges.forEach(e => { degree[e.source] = (degree[e.source] || 0) + 1; degree[e.target] = (degree[e.target] || 0) + 1; });
const maxDeg = Math.max(1, ...Object.values(degree));
const simNodes = nodes.map((n, i) => ({
...n, x: width/2 + (Math.random()-0.5) * width * 0.6,
y: height/2 + (Math.random()-0.5) * height * 0.6,
vx: 0, vy: 0,
r: 3 + 7 * (degree[n.id] / maxDeg)
}));
const nodeMap = {};
simNodes.forEach((n, i) => nodeMap[n.id] = i);
const simEdges = edges.filter(e => nodeMap[e.source] !== undefined && nodeMap[e.target] !== undefined)
.map(e => ({ source: nodeMap[e.source], target: nodeMap[e.target], kind: e.kind }));
let showLabels = nodes.length < 80;
const alpha = { value: 1.0 };
const alphaDecay = 0.02;
const alphaMin = 0.01;
function tick() {
const n = simNodes.length;
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
let dx = simNodes[j].x - simNodes[i].x;
let dy = simNodes[j].y - simNodes[i].y;
let d = Math.sqrt(dx * dx + dy * dy) || 1;
let force = -300 * alpha.value / (d * d);
let fx = dx / d * force;
let fy = dy / d * force;
simNodes[i].vx -= fx; simNodes[i].vy -= fy;
simNodes[j].vx += fx; simNodes[j].vy += fy;
}
}
simEdges.forEach(e => {
const s = simNodes[e.source], t = simNodes[e.target];
let dx = t.x - s.x, dy = t.y - s.y;
let d = Math.sqrt(dx * dx + dy * dy) || 1;
let force = (d - 80) * 0.005 * alpha.value;
let fx = dx / d * force, fy = dy / d * force;
s.vx += fx; s.vy += fy;
t.vx -= fx; t.vy -= fy;
});
let cx = 0, cy = 0;
simNodes.forEach(n => { cx += n.x; cy += n.y; });
cx /= n; cy /= n;
simNodes.forEach(n => { n.vx += (width/2 - cx) * 0.01; n.vy += (height/2 - cy) * 0.01; });
simNodes.forEach(n => {
n.vx *= 0.6; n.vy *= 0.6;
n.x += n.vx; n.y += n.vy;
n.x = Math.max(n.r, Math.min(width - n.r, n.x));
n.y = Math.max(n.r, Math.min(height - n.r, n.y));
});
alpha.value *= (1 - alphaDecay);
}
const maxIter = Math.min(300, 100 + nodes.length);
for (let i = 0; i < maxIter && alpha.value > alphaMin; i++) tick();
let transform = { x: 0, y: 0, k: 1 };
function renderSVG() {
let html = `<g transform="translate(${transform.x},${transform.y}) scale(${transform.k})">`;
simEdges.forEach(e => {
const s = simNodes[e.source], t = simNodes[e.target];
const color = edgeColors[e.kind] || '#8b949e';
html += `<line x1="${s.x}" y1="${s.y}" x2="${t.x}" y2="${t.y}" stroke="${color}" stroke-opacity="0.3" stroke-width="1"/>`;
});
simNodes.forEach((n, i) => {
const color = edgeColors.imports;
const deg = degree[n.id] || 0;
html += `<circle cx="${n.x}" cy="${n.y}" r="${n.r}" fill="${color}" fill-opacity="0.7" stroke="${color}" stroke-width="1" data-idx="${i}" class="graph-node" style="cursor:pointer"/>`;
});
if (showLabels) {
simNodes.forEach(n => {
html += `<text x="${n.x}" y="${n.y - n.r - 3}" text-anchor="middle" fill="${'#8b949e'}" font-size="9" font-family="var(--mono)">${escHtml(n.label)}</text>`;
});
}
html += '</g>';
svg.innerHTML = html;
svg.querySelectorAll('.graph-node').forEach(circle => {
const idx = parseInt(circle.dataset.idx);
const n = simNodes[idx];
circle.addEventListener('mouseenter', (ev) => {
circle.setAttribute('fill-opacity', '1');
circle.setAttribute('r', n.r + 2);
const deg = degree[n.id] || 0;
showTooltip(ev.pageX, ev.pageY, `<strong>${escHtml(n.id)}</strong><br/>Connections: ${deg}`);
});
circle.addEventListener('mouseleave', () => {
circle.setAttribute('fill-opacity', '0.7');
circle.setAttribute('r', n.r);
hideTooltip();
});
});
}
renderSVG();
let isPanning = false, startX, startY;
svg.addEventListener('wheel', e => {
e.preventDefault();
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
const rect = svg.getBoundingClientRect();
const mx = e.clientX - rect.left, my = e.clientY - rect.top;
transform.x = mx - (mx - transform.x) * zoomFactor;
transform.y = my - (my - transform.y) * zoomFactor;
transform.k *= zoomFactor;
renderSVG();
}, { passive: false });
svg.addEventListener('mousedown', e => { isPanning = true; startX = e.clientX - transform.x; startY = e.clientY - transform.y; });
svg.addEventListener('mousemove', e => { if (!isPanning) return; transform.x = e.clientX - startX; transform.y = e.clientY - startY; renderSVG(); });
svg.addEventListener('mouseup', () => isPanning = false);
svg.addEventListener('mouseleave', () => isPanning = false);
window.resetZoom = function() { transform = { x: 0, y: 0, k: 1 }; renderSVG(); };
window.toggleLabels = function() { showLabels = !showLabels; renderSVG(); };
})();
(function renderKnowledge() {
const facts = DATA.knowledge;
document.getElementById('sk-total').textContent = fmt(facts.length);
const cats = new Set(facts.map(f => f.category));
document.getElementById('sk-categories').textContent = cats.size;
const avgConf = facts.length > 0 ? facts.reduce((s, f) => s + f.confidence, 0) / facts.length : 0;
document.getElementById('sk-avg-conf').textContent = pct(avgConf);
const totalRet = facts.reduce((s, f) => s + f.retrieval_count, 0);
document.getElementById('sk-retrievals').textContent = fmt(totalRet);
const tbody = document.querySelector('#knowledge-table tbody');
if (facts.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-state"><h3>No knowledge facts</h3><p>Use <code>ctx_knowledge</code> to record facts.</p></td></tr>';
return;
}
const archetypeBadge = {
Architecture: 'badge-purple', Decision: 'badge-blue', Gotcha: 'badge-red',
Convention: 'badge-cyan', Pattern: 'badge-green', Workflow: 'badge-orange',
Observation: 'badge-green', Fact: 'badge-blue', Preference: 'badge-cyan',
Dependency: 'badge-orange'
};
facts.forEach(f => {
const tr = document.createElement('tr');
const bc = archetypeBadge[f.archetype] || 'badge-blue';
tr.innerHTML = `
<td><span class="badge ${bc}">${escHtml(f.archetype)}</span></td>
<td class="mono" data-val="${escHtml(f.category)}">${escHtml(f.category)}</td>
<td>${escHtml(f.key)}</td>
<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escHtml(f.value)}">${escHtml(f.value)}</td>
<td data-val="${f.confidence}"><div class="progress-bar" style="display:inline-block;width:50px;vertical-align:middle;margin-right:6px;"><div class="progress-fill" style="width:${f.confidence*100}%;background:${f.confidence>0.7?'var(--accent-green)':f.confidence>0.4?'var(--accent-orange)':'var(--accent-red)'}"></div></div>${pct(f.confidence)}</td>
<td data-val="${f.retrieval_count}">${f.retrieval_count}</td>
<td data-val="${f.created_at}">${shortDate(f.created_at)}</td>`;
tbody.appendChild(tr);
});
})();
(function renderSavings() {
const s = DATA.savings;
document.getElementById('ss-saved').textContent = fmt(s.total_saved);
document.getElementById('ss-original').textContent = fmt(s.total_original);
document.getElementById('ss-ratio').textContent = pct(s.overall_ratio);
document.getElementById('ss-files').textContent = fmt(s.files.length);
const tbody = document.querySelector('#savings-table tbody');
if (s.files.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-state"><h3>No savings data</h3><p>Use lean-ctx to read files and accumulate savings.</p></td></tr>';
return;
}
const sorted = [...s.files].sort((a, b) => b.saved_tokens - a.saved_tokens);
sorted.forEach(f => {
const tr = document.createElement('tr');
const barColor = f.compression_ratio > 0.5 ? 'var(--accent-green)' : f.compression_ratio > 0.2 ? 'var(--accent-blue)' : 'var(--accent-orange)';
tr.innerHTML = `
<td class="path mono" title="${escHtml(f.path)}" data-val="${escHtml(f.path)}">${escHtml(f.path)}</td>
<td data-val="${f.access_count}">${f.access_count}</td>
<td class="mono" data-val="${f.original_tokens}">${fmt(f.original_tokens)}</td>
<td class="mono" data-val="${f.saved_tokens}">${fmt(f.saved_tokens)}</td>
<td data-val="${f.compression_ratio}">${pct(f.compression_ratio)}</td>
<td><div class="progress-bar"><div class="progress-fill" style="width:${f.compression_ratio*100}%;background:${barColor}"></div></div></td>`;
tbody.appendChild(tr);
});
})();
(function renderSession() {
const h = DATA.history;
document.getElementById('sh-calls').textContent = fmt(h.stats.total_tool_calls);
document.getElementById('sh-saved').textContent = fmt(h.stats.total_tokens_saved);
document.getElementById('sh-cache').textContent = fmt(h.stats.cache_hits);
document.getElementById('sh-files').textContent = fmt(h.stats.files_read);
document.getElementById('sh-cmds').textContent = fmt(h.stats.commands_run);
if (h.task) {
document.getElementById('session-task').style.display = '';
document.getElementById('session-task-text').textContent = h.task;
}
const ftbody = document.querySelector('#files-table tbody');
if (h.files_touched.length === 0) {
ftbody.innerHTML = '<tr><td colspan="5" style="color:var(--text-muted);text-align:center;padding:20px;">No files in this session.</td></tr>';
} else {
h.files_touched.forEach(f => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="path mono">${escHtml(f.path)}</td>
<td>${f.read_count}</td>
<td>${f.modified ? '<span class="badge badge-orange">modified</span>' : '<span class="badge badge-blue">read</span>'}</td>
<td class="mono">${escHtml(f.mode)}</td>
<td class="mono">${fmt(f.tokens)}</td>`;
ftbody.appendChild(tr);
});
}
const timeline = document.getElementById('session-timeline');
const events = [];
h.findings.forEach(f => events.push({ type: 'finding', time: f.timestamp, text: f.summary, sub: f.file }));
h.decisions.forEach(d => events.push({ type: 'decision', time: d.timestamp, text: d.summary, sub: d.rationale }));
h.progress.forEach(p => events.push({ type: 'progress', time: p.timestamp, text: p.action, sub: p.detail }));
events.sort((a, b) => new Date(a.time) - new Date(b.time));
if (events.length === 0) {
timeline.innerHTML = '<div class="empty-state"><h3>No events</h3><p>Session has no recorded findings, decisions, or progress.</p></div>';
} else {
events.forEach(e => {
const div = document.createElement('div');
div.className = `timeline-item ${e.type}`;
div.innerHTML = `
<div class="timeline-time">${shortDate(e.time)} · <span class="badge badge-${e.type === 'decision' ? 'purple' : e.type === 'finding' ? 'green' : 'orange'}">${e.type}</span></div>
<div class="timeline-text">${escHtml(e.text)}${e.sub ? '<br/><small style="color:var(--text-muted)">' + escHtml(e.sub) + '</small>' : ''}</div>`;
timeline.appendChild(div);
});
}
})();
</script>
</body>
</html>