Add stats widget to session cards

This commit is contained in:
Jared Miller 2026-01-28 13:55:57 -05:00
parent 298faecf72
commit 348b56183e
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C

View file

@ -447,6 +447,59 @@
border-top: 1px solid #3a3a3a;
}
.stats-widget {
padding: 8px 12px;
background: #1e1e1e;
border-bottom: 1px solid #333;
font-size: 12px;
color: #888;
}
.stats-line {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 4px;
}
.stats-line:last-child {
margin-bottom: 0;
}
.stats-item {
display: flex;
align-items: center;
gap: 4px;
}
.stats-value {
color: #fff;
font-weight: 500;
}
.mode-badge {
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
}
.mode-plan {
background: #7c3aed;
color: white;
}
.mode-auto {
background: #059669;
color: white;
}
.model-name {
color: #666;
font-size: 11px;
}
.empty-state {
padding: 32px 16px;
text-align: center;
@ -471,6 +524,19 @@
border-color: #ff9800;
}
.stats-widget {
background: #f5f5f5;
border-bottom-color: #e0e0e0;
}
.stats-value {
color: #1a1a1a;
}
.model-name {
color: #999;
}
.session-output {
background: #f5f5f5;
border-top-color: #e0e0e0;
@ -621,6 +687,15 @@
outputRenderedLength: 0,
expanded: false,
state: 'ready',
prompts: 0,
completions: 0,
tools: 0,
compressions: 0,
thinking_seconds: 0,
work_seconds: 0,
mode: 'normal',
model: null,
idle_since: null,
});
renderSessions();
});
@ -666,6 +741,23 @@
}
});
es.addEventListener('stats', (e) => {
const data = JSON.parse(e.data);
const session = state.sessions.get(data.session_id);
if (session) {
if (data.prompts !== undefined) session.prompts = data.prompts;
if (data.completions !== undefined) session.completions = data.completions;
if (data.tools !== undefined) session.tools = data.tools;
if (data.compressions !== undefined) session.compressions = data.compressions;
if (data.thinking_seconds !== undefined) session.thinking_seconds = data.thinking_seconds;
if (data.work_seconds !== undefined) session.work_seconds = data.work_seconds;
if (data.mode !== undefined) session.mode = data.mode;
if (data.model !== undefined) session.model = data.model;
if (data.idle_since !== undefined) session.idle_since = data.idle_since;
updateStatsWidget(data.session_id);
}
});
state.eventSource = es;
}
@ -692,6 +784,60 @@
return `<span class="state-badge state-${sessionState}">${label}</span>`;
}
function formatDuration(seconds) {
if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
return `${Math.floor(seconds / 3600)}h`;
}
function renderStatsWidget(session) {
const lines = [];
// Counts line
const counts = [];
if (session.prompts > 0) counts.push(`<span class="stats-value">${session.prompts}</span> prompts`);
if (session.completions > 0) counts.push(`<span class="stats-value">${session.completions}</span> completions`);
if (session.tools > 0) counts.push(`<span class="stats-value">${session.tools}</span> tools`);
if (session.compressions > 0) counts.push(`<span class="stats-value">${session.compressions}</span> compressions`);
if (counts.length > 0) {
lines.push(`<div class="stats-line">${counts.join(' | ')}</div>`);
}
// Timing line
const timing = [];
if (session.thinking_seconds > 0) timing.push(`<span class="stats-value">${formatDuration(session.thinking_seconds)}</span> thinking`);
if (session.idle_since) {
const idleSeconds = Math.floor((Date.now() - session.idle_since * 1000) / 1000);
if (idleSeconds > 0) timing.push(`<span class="stats-value">${formatDuration(idleSeconds)}</span> idle`);
}
if (timing.length > 0) {
lines.push(`<div class="stats-line">${timing.join(' | ')}</div>`);
}
// Mode badge and model name
const badges = [];
if (session.mode === 'plan') badges.push('<span class="mode-badge mode-plan">Plan Mode</span>');
else if (session.mode === 'auto_accept') badges.push('<span class="mode-badge mode-auto">Auto-Accept</span>');
if (session.model) {
// "claude-opus-4-5-20251101" -> "opus-4-5"
const match = session.model.match(/claude-(\w+-[\d-]+)/);
if (match) badges.push(`<span class="model-name">${match[1]}</span>`);
}
if (badges.length > 0) {
lines.push(`<div class="stats-line">${badges.join(' ')}</div>`);
}
if (lines.length === 0) {
return '';
}
return `<div class="stats-widget" id="stats-${session.id}">${lines.join('')}</div>`;
}
function renderSessions() {
if (state.sessions.size === 0) {
$sessions.innerHTML = '<div class="empty-state">No active sessions</div>';
@ -710,6 +856,7 @@
</div>
<div class="session-toggle"></div>
</div>
${renderStatsWidget(s)}
<div class="session-output">
<div class="terminal" id="output-${s.id}">${escapeHtml(s.output)}</div>
</div>
@ -751,6 +898,31 @@
}
}
function updateStatsWidget(sessionId) {
const session = state.sessions.get(sessionId);
if (!session) return;
const $statsWidget = document.getElementById(`stats-${sessionId}`);
if ($statsWidget) {
const newHtml = renderStatsWidget(session);
if (newHtml) {
$statsWidget.outerHTML = newHtml;
} else {
$statsWidget.remove();
}
} else if (session) {
// Widget doesn't exist but should - find the header and insert after it
const $sessionCard = document.querySelector(`[data-session="${sessionId}"]`);
if ($sessionCard) {
const $header = $sessionCard.querySelector('.session-header');
const newHtml = renderStatsWidget(session);
if (newHtml && $header) {
$header.insertAdjacentHTML('afterend', newHtml);
}
}
}
}
function renderPrompts() {
if (state.prompts.size === 0) {
$prompts.innerHTML = '<div class="empty-state">No pending prompts</div>';