diff --git a/plugins/session-report/skills/session-report/analyze-sessions.mjs b/plugins/session-report/skills/session-report/analyze-sessions.mjs index 7ea712a..1a5e593 100644 --- a/plugins/session-report/skills/session-report/analyze-sessions.mjs +++ b/plugins/session-report/skills/session-report/analyze-sessions.mjs @@ -166,6 +166,7 @@ const toolUseIdToPrompt = new Map() // tool_use id -> promptKey (Agent spawned d const agentIdToPrompt = new Map() // agentId -> promptKey const prompts = new Map() // promptKey -> { text, ts, project, sessionId, ...usage } const sessionTurns = new Map() // sessionId -> [promptKey, ...] in transcript order +const sessionSpans = new Map() // sessionId -> {project, firstTs, lastTs, tokens} function promptRecord(key, init) { let r = prompts.get(key) @@ -333,11 +334,29 @@ async function processFile(p, info, buckets) { } } + // session span (for by_day timeline) — subagent files roll into parent sessionId + let span = sessionSpans.get(info.sessionId) + if (!span) { + span = { project: info.project, firstTs: null, lastTs: null, tokens: 0 } + sessionSpans.set(info.sessionId, span) + } + if (firstTs !== null) { + if (span.firstTs === null || firstTs < span.firstTs) span.firstTs = firstTs + if (span.lastTs === null || lastTs > span.lastTs) span.lastTs = lastTs + } + // commit API calls for (const [key, { usage, ts, skill, prompt }] of fileApiCalls) { if (key && seenRequestIds.has(key)) continue seenRequestIds.add(key) + const tot = + (usage.input_tokens || 0) + + (usage.cache_creation_input_tokens || 0) + + (usage.cache_read_input_tokens || 0) + + (usage.output_tokens || 0) + span.tokens += tot + const targets = [overall, project] if (subagent) targets.push(subagent) if (skill && skillStats) { @@ -359,11 +378,6 @@ async function processFile(p, info, buckets) { // subagent token accounting on parent buckets if (info.kind === 'subagent') { - const tot = - (usage.input_tokens || 0) + - (usage.cache_creation_input_tokens || 0) + - (usage.cache_read_input_tokens || 0) + - (usage.output_tokens || 0) overall.subagentTokens += tot project.subagentTokens += tot if (subagent) subagent.subagentTokens += tot @@ -656,10 +670,55 @@ function printJson({ overall, perProject, perSubagent, perSkill }) { [...perSkill].map(([k, v]) => [k, summarize(v)]), ), top_prompts: topPrompts(100), + by_day: buildByDay(), } process.stdout.write(JSON.stringify(out, null, 2) + '\n') } +// Group sessions into local-date buckets for the timeline view. A session is +// placed on the day its first message landed; tokens for that session (incl. +// subagents) count toward that day even if it ran past midnight. +function buildByDay() { + const DOW = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + const days = new Map() // yyyy-mm-dd -> {date, dow, tokens, sessions:[]} + for (const [id, s] of sessionSpans) { + if (s.firstTs === null || s.tokens === 0) continue + const d0 = new Date(s.firstTs) + const key = `${d0.getFullYear()}-${String(d0.getMonth() + 1).padStart(2, '0')}-${String(d0.getDate()).padStart(2, '0')}` + let day = days.get(key) + if (!day) { + day = { date: key, dow: DOW[d0.getDay()], tokens: 0, sessions: [] } + days.set(key, day) + } + const base = new Date( + d0.getFullYear(), + d0.getMonth(), + d0.getDate(), + ).getTime() + day.tokens += s.tokens + day.sessions.push({ + id, + project: s.project, + tokens: s.tokens, + start_min: Math.max(0, Math.round((s.firstTs - base) / 60000)), + end_min: Math.max(1, Math.round((s.lastTs - base) / 60000)), + }) + } + for (const d of days.values()) { + // peak concurrency via 10-min buckets, capped at 24h for display + const b = new Array(144).fill(0) + for (const s of d.sessions) { + const lo = Math.min(143, Math.floor(s.start_min / 10)) + const hi = Math.min(144, Math.ceil(Math.min(s.end_min, 1440) / 10)) + for (let i = lo; i < hi; i++) b[i]++ + } + d.peak = Math.max(0, ...b) + d.peak_at_min = d.peak > 0 ? b.indexOf(d.peak) * 10 : 0 + d.sessions.sort((a, b) => a.start_min - b.start_min) + } + return [...days.values()].sort((a, b) => a.date.localeCompare(b.date)) +} + function promptTotal(r) { return ( r.inputUncached + r.inputCacheCreate + r.inputCacheRead + r.outputTokens diff --git a/plugins/session-report/skills/session-report/template.html b/plugins/session-report/skills/session-report/template.html index 7a89e97..98a9910 100644 --- a/plugins/session-report/skills/session-report/template.html +++ b/plugins/session-report/skills/session-report/template.html @@ -102,6 +102,42 @@ color: var(--dim); margin: 6px 0; } .callout b, .callout code { color: var(--term-fg); } + /* ——— day pills + session gantt ——— */ + .days { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; } + .dpill { flex: 1; min-width: 84px; max-width: 140px; background: none; + border: 1px solid var(--subtle); border-radius: 4px; + padding: 9px 6px; font: inherit; color: var(--dim); + cursor: pointer; text-align: center; } + .dpill:hover { border-color: var(--dim); background: var(--hover); } + .dpill .dow { font-size: 10px; color: var(--subtle); display: block; } + .dpill .date { font-size: 11px; color: var(--term-fg); font-weight: 500; + display: block; margin: 2px 0 4px; } + .dpill .pct { font-size: 16px; font-weight: 700; color: var(--term-fg); display: block; } + .dpill .ns { font-size: 10px; color: var(--subtle); display: block; margin-top: 2px; } + .dpill.heaviest .pct { color: var(--clay); } + .dpill.sel { border-color: var(--clay); background: rgba(217,119,87,0.10); } + .gantt-hd { display: flex; justify-content: space-between; align-items: baseline; + margin-bottom: 6px; } + .gantt-hd .day { color: var(--term-fg); font-weight: 500; } + .gantt-hd .stats { font-size: 11px; color: var(--dim); } + .gantt-hd .stats b { color: var(--clay); } + .gantt { position: relative; border-top: 1px solid var(--outline); + border-bottom: 1px solid var(--outline); min-height: 32px; } + .lane { position: relative; height: 16px; + border-bottom: 1px dashed rgba(255,255,255,0.04); } + .seg { position: absolute; top: 2px; height: 12px; border-radius: 2px; + opacity: .85; cursor: crosshair; } + .seg:hover { opacity: 1; outline: 1px solid var(--term-fg); z-index: 2; } + .gantt-rule { position: absolute; top: 0; bottom: 0; width: 0; + border-left: 1px dashed var(--subtle); opacity: .4; + pointer-events: none; } + .gantt-axis { display: flex; justify-content: space-between; + font-size: 10px; color: var(--subtle); padding: 4px 0; } + .gantt-leg { font-size: 10px; color: var(--subtle); margin-top: 8px; + display: flex; gap: 14px; flex-wrap: wrap; } + .gantt-leg .sw { display: inline-block; width: 14px; height: 10px; + border-radius: 2px; vertical-align: middle; margin-right: 4px; } + /* ——— block-char bars ——— */ .bar { display: grid; grid-template-columns: 26ch 1fr 8ch; gap: 14px; padding: 2px 0; align-items: center; } @@ -231,6 +267,21 @@
+${esc(p.session)}${esc(b.session)}${esc(p.session)}${esc(b.session)}