Add stats widget to session cards
This commit is contained in:
parent
298faecf72
commit
348b56183e
1 changed files with 172 additions and 0 deletions
|
|
@ -447,6 +447,59 @@
|
||||||
border-top: 1px solid #3a3a3a;
|
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 {
|
.empty-state {
|
||||||
padding: 32px 16px;
|
padding: 32px 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -471,6 +524,19 @@
|
||||||
border-color: #ff9800;
|
border-color: #ff9800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stats-widget {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-bottom-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-value {
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-name {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
.session-output {
|
.session-output {
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
border-top-color: #e0e0e0;
|
border-top-color: #e0e0e0;
|
||||||
|
|
@ -621,6 +687,15 @@
|
||||||
outputRenderedLength: 0,
|
outputRenderedLength: 0,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
state: 'ready',
|
state: 'ready',
|
||||||
|
prompts: 0,
|
||||||
|
completions: 0,
|
||||||
|
tools: 0,
|
||||||
|
compressions: 0,
|
||||||
|
thinking_seconds: 0,
|
||||||
|
work_seconds: 0,
|
||||||
|
mode: 'normal',
|
||||||
|
model: null,
|
||||||
|
idle_since: null,
|
||||||
});
|
});
|
||||||
renderSessions();
|
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;
|
state.eventSource = es;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -692,6 +784,60 @@
|
||||||
return `<span class="state-badge state-${sessionState}">${label}</span>`;
|
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() {
|
function renderSessions() {
|
||||||
if (state.sessions.size === 0) {
|
if (state.sessions.size === 0) {
|
||||||
$sessions.innerHTML = '<div class="empty-state">No active sessions</div>';
|
$sessions.innerHTML = '<div class="empty-state">No active sessions</div>';
|
||||||
|
|
@ -710,6 +856,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="session-toggle">▼</div>
|
<div class="session-toggle">▼</div>
|
||||||
</div>
|
</div>
|
||||||
|
${renderStatsWidget(s)}
|
||||||
<div class="session-output">
|
<div class="session-output">
|
||||||
<div class="terminal" id="output-${s.id}">${escapeHtml(s.output)}</div>
|
<div class="terminal" id="output-${s.id}">${escapeHtml(s.output)}</div>
|
||||||
</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() {
|
function renderPrompts() {
|
||||||
if (state.prompts.size === 0) {
|
if (state.prompts.size === 0) {
|
||||||
$prompts.innerHTML = '<div class="empty-state">No pending prompts</div>';
|
$prompts.innerHTML = '<div class="empty-state">No pending prompts</div>';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue