diff --git a/lib/aggregator.js b/lib/aggregator.js index 5d603ae..5b73bfe 100644 --- a/lib/aggregator.js +++ b/lib/aggregator.js @@ -24,44 +24,80 @@ async function parseSessionFile(filePath) { const stat = fs.statSync(filePath); const cached = fileCache.get(filePath); if (cached && cached.mtimeMs === stat.mtimeMs) { - return cached.days; + return cached.session; } const isZstd = filePath.endsWith(".zstd"); const days = {}; + let sessionId = path.basename(path.dirname(filePath)); + let title = ""; + let createdAt = stat.birthtimeMs || stat.mtimeMs; + let totalTokens = 0; + let totalInputTokens = 0; + let totalOutputTokens = 0; + let totalCacheReadTokens = 0; + let totalTurns = 0; await new Promise((resolve) => { let buffer = ""; const handleLine = (line) => { - if (!line.includes('"usage"')) return; - try { - const obj = JSON.parse(line); - const time = obj.time || obj.data && obj.data.time; - const usage = obj.usage || obj.data && obj.data.usage; - if (time && usage) { - const d = new Date(time); - const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; - const input = Number(usage.inputTokens) || 0; - const output = Number(usage.outputTokens) || 0; - const cacheRead = Number(usage.cacheReadTokens) || 0; - const cacheWrite = Number(usage.cacheWriteTokens) || 0; - const total = input + output + cacheRead; - if (!days[dateKey]) { - days[dateKey] = { - totalTokens: 0, - inputTokens: 0, - outputTokens: 0, - cacheReadTokens: 0, - cacheWriteTokens: 0, - turns: 0 - }; - } - days[dateKey].totalTokens += total; - days[dateKey].inputTokens += input; - days[dateKey].outputTokens += output; - days[dateKey].cacheReadTokens += cacheRead; - days[dateKey].cacheWriteTokens += cacheWrite; - days[dateKey].turns += 1; + if (line.includes('"type":"session"')) { + try { + const obj = JSON.parse(line); + if (obj.id) sessionId = obj.id; + if (obj.createdAt) createdAt = obj.createdAt; + } catch { + } + } + if (!title && line.includes('"session/title"')) { + try { + const obj = JSON.parse(line); + if (obj.data?.title) title = obj.data.title; + } catch { + } + } + if (!title && line.includes('"subagent/descriptor"')) { + try { + const obj = JSON.parse(line); + if (obj.data?.label) title = obj.data.label; + } catch { + } + } + if (line.includes('"usage"')) { + try { + const obj = JSON.parse(line); + const time = obj.time || obj.data && obj.data.time; + const usage = obj.usage || obj.data && obj.data.usage; + if (time && usage) { + const d = new Date(time); + const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; + const input = Number(usage.inputTokens) || 0; + const output = Number(usage.outputTokens) || 0; + const cacheRead = Number(usage.cacheReadTokens) || 0; + const cacheWrite = Number(usage.cacheWriteTokens) || 0; + const total = input + output + cacheRead; + if (!days[dateKey]) { + days[dateKey] = { + totalTokens: 0, + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: 0, + cacheWriteTokens: 0, + turns: 0 + }; + } + days[dateKey].totalTokens += total; + days[dateKey].inputTokens += input; + days[dateKey].outputTokens += output; + days[dateKey].cacheReadTokens += cacheRead; + days[dateKey].cacheWriteTokens += cacheWrite; + days[dateKey].turns += 1; + totalTokens += total; + totalInputTokens += input; + totalOutputTokens += output; + totalCacheReadTokens += cacheRead; + totalTurns += 1; + } + } catch { } - } catch { } }; if (isZstd) { @@ -98,19 +134,31 @@ async function parseSessionFile(filePath) { stream.on("error", () => resolve()); } }); - fileCache.set(filePath, { mtimeMs: stat.mtimeMs, days }); - return days; + const session = { + id: sessionId, + title: title || `Session ${sessionId.slice(0, 8)}`, + createdAt, + totalTokens, + inputTokens: totalInputTokens, + outputTokens: totalOutputTokens, + cacheReadTokens: totalCacheReadTokens, + turns: totalTurns, + days + }; + fileCache.set(filePath, { mtimeMs: stat.mtimeMs, session }); + return session; } catch { - return {}; + return null; } } async function aggregateTokens(customSessionsRoot) { const sessionsRoot = customSessionsRoot || path.join(os.homedir(), ".dsh", "sessions"); const files = findSessionFiles(sessionsRoot); - const parsedResults = await Promise.all(files.map(parseSessionFile)); + const parsedSessions = (await Promise.all(files.map(parseSessionFile))).filter(Boolean); + parsedSessions.sort((a, b) => b.createdAt - a.createdAt); const days = {}; - for (const fileDays of parsedResults) { - for (const [date, stats] of Object.entries(fileDays)) { + for (const session of parsedSessions) { + for (const [date, stats] of Object.entries(session.days)) { if (!days[date]) { days[date] = { totalTokens: 0, @@ -133,12 +181,10 @@ async function aggregateTokens(customSessionsRoot) { let q1 = 5e6; let q2 = 25e6; let q3 = 75e6; - let q4 = 15e7; if (positiveValues.length >= 4) { q1 = positiveValues[Math.floor(positiveValues.length * 0.25)]; q2 = positiveValues[Math.floor(positiveValues.length * 0.5)]; q3 = positiveValues[Math.floor(positiveValues.length * 0.75)]; - q4 = positiveValues[Math.floor(positiveValues.length * 0.9)]; } for (const day of Object.values(days)) { if (day.totalTokens === 0) day.level = 0; @@ -221,6 +267,7 @@ async function aggregateTokens(customSessionsRoot) { dailyAverage }, days, + sessions: parsedSessions, startDate, endDate }; @@ -228,4 +275,3 @@ async function aggregateTokens(customSessionsRoot) { export { aggregateTokens }; -//# sourceMappingURL=aggregator.js.map diff --git a/lib/client.js b/lib/client.js index 07a12ea..f72fda9 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,45 +1,9 @@ -window.__ModuleLoader__.load({ id: "dsh-plugin-token-heatmap", factory: (require) => { var module = { exports: {} }; var exports = module.exports; -var __create = Object.create; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __getProtoOf = Object.getPrototypeOf; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __export = (target, all) => { - for (var name2 in all) - __defProp(target, name2, { get: all[name2], enumerable: true }); -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( - // If the importer is in node compatibility mode or this is not an ESM - // file that has been converted to a CommonJS file using a Babel- - // compatible transform (i.e. "__esModule" has not been set), then set - // "default" to the CommonJS "module.exports" for node compatibility. - isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, - mod -)); -var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); - // src/client/index.tsx -var index_exports = {}; -__export(index_exports, { - apply: () => apply, - inject: () => inject, - name: () => name -}); -module.exports = __toCommonJS(index_exports); -var import_react3 = __toESM(require("react"), 1); -var import_client = require("react-dom/client"); +import React3, { useState as useState2, useEffect as useEffect2 } from "react"; +import { createRoot } from "react-dom/client"; // src/client/HeatmapView.tsx -var import_react = __toESM(require("react"), 1); +import React, { useEffect, useState, useMemo } from "react"; function formatNumber(num) { if (num >= 1e9) return (num / 1e9).toFixed(2) + "B"; if (num >= 1e6) return (num / 1e6).toFixed(1) + "M"; @@ -51,35 +15,20 @@ function formatExact(num) { } var MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; var DAY_LABELS = ["", "Mon", "", "Wed", "", "Fri", ""]; -var COLOR_THEMES = { - green: { - name: "GitHub Emerald", - levels: [ - "var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.08))", - "#0e4429", - "#006d32", - "#26a641", - "#39d353" - ] - }, - deepseek: { - name: "DeepSeek Indigo", - levels: [ - "var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.08))", - "#1e1e4a", - "#2e3188", - "#434fc7", - "#6366f1" - ] - } -}; +var DEEPSEEK_LEVELS = [ + "var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.08))", + "#1e2640", + "#2d3e8b", + "#3d52c4", + "#4f46e5" +]; function HeatmapView() { - const [data, setData] = (0, import_react.useState)(null); - const [loading, setLoading] = (0, import_react.useState)(true); - const [error, setError] = (0, import_react.useState)(null); - const [themeKey, setThemeKey] = (0, import_react.useState)("green"); - const [hoveredDay, setHoveredDay] = (0, import_react.useState)(null); - const [tooltipPos, setTooltipPos] = (0, import_react.useState)(null); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedSessionId, setSelectedSessionId] = useState("all"); + const [hoveredDay, setHoveredDay] = useState(null); + const [tooltipPos, setTooltipPos] = useState(null); const fetchStats = () => { setLoading(true); fetch("/api/dsh-token-heatmap/stats").then((r) => { @@ -93,11 +42,77 @@ function HeatmapView() { setLoading(false); }); }; - (0, import_react.useEffect)(() => { + useEffect(() => { fetchStats(); }, []); - const calendarGrid = (0, import_react.useMemo)(() => { + const activeView = useMemo(() => { if (!data) return null; + if (selectedSessionId === "all") { + return { + summary: data.summary, + days: data.days, + sessionTitle: "All Sessions (Global)" + }; + } + const session = data.sessions.find((s) => s.id === selectedSessionId); + if (!session) { + return { + summary: data.summary, + days: data.days, + sessionTitle: "All Sessions (Global)" + }; + } + let totalTokens = session.totalTokens; + let totalInputTokens = session.inputTokens; + let totalOutputTokens = session.outputTokens; + let totalCacheReadTokens = session.cacheReadTokens; + let totalTurns = session.turns; + let activeDays = Object.values(session.days).filter((d) => d.totalTokens > 0).length; + let dailyAverage = activeDays > 0 ? Math.round(totalTokens / activeDays) : 0; + let peakDay = null; + for (const [date, stats] of Object.entries(session.days)) { + if (!peakDay || stats.totalTokens > peakDay.tokens) { + peakDay = { date, tokens: stats.totalTokens, turns: stats.turns }; + } + } + const positiveValues = Object.values(session.days).map((d) => d.totalTokens).filter((t) => t > 0).sort((a, b) => a - b); + let q1 = 1e6; + let q2 = 5e6; + let q3 = 15e6; + if (positiveValues.length >= 4) { + q1 = positiveValues[Math.floor(positiveValues.length * 0.25)]; + q2 = positiveValues[Math.floor(positiveValues.length * 0.5)]; + q3 = positiveValues[Math.floor(positiveValues.length * 0.75)]; + } + const leveledDays = {}; + for (const [date, stats] of Object.entries(session.days)) { + let lvl = 0; + if (stats.totalTokens === 0) lvl = 0; + else if (stats.totalTokens <= q1) lvl = 1; + else if (stats.totalTokens <= q2) lvl = 2; + else if (stats.totalTokens <= q3) lvl = 3; + else lvl = 4; + leveledDays[date] = { ...stats, level: lvl }; + } + return { + summary: { + totalTokens, + totalInputTokens, + totalOutputTokens, + totalCacheReadTokens, + totalTurns, + activeDays, + currentStreak: 0, + longestStreak: 0, + peakDay, + dailyAverage + }, + days: leveledDays, + sessionTitle: session.title + }; + }, [data, selectedSessionId]); + const calendarGrid = useMemo(() => { + if (!activeView) return null; const weeks = []; const now = /* @__PURE__ */ new Date(); const end = new Date(now); @@ -108,7 +123,7 @@ function HeatmapView() { let currentWeek = []; while (current <= end || currentWeek.length > 0) { const dateKey = `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, "0")}-${String(current.getDate()).padStart(2, "0")}`; - const dayStats = data.days[dateKey] || { + const dayStats = activeView.days[dateKey] || { totalTokens: 0, inputTokens: 0, outputTokens: 0, @@ -130,8 +145,8 @@ function HeatmapView() { if (current > end && currentWeek.length === 0) break; } return weeks; - }, [data]); - const monthHeaders = (0, import_react.useMemo)(() => { + }, [activeView]); + const monthHeaders = useMemo(() => { if (!calendarGrid) return []; const headers = []; let lastMonth = -1; @@ -148,12 +163,11 @@ function HeatmapView() { }); return headers; }, [calendarGrid]); - const activeTheme = COLOR_THEMES[themeKey]; if (loading && !data) { - return /* @__PURE__ */ import_react.default.createElement("div", { style: { padding: "40px 20px", textAlign: "center", color: "var(--dsw-alias-label-secondary)" } }, "Loading token usage heatmap..."); + return /* @__PURE__ */ React.createElement("div", { style: { padding: "40px 20px", textAlign: "center", color: "var(--dsw-alias-label-secondary)" } }, "Loading token usage statistics..."); } if (error) { - return /* @__PURE__ */ import_react.default.createElement("div", { style: { padding: "20px", color: "var(--dsw-alias-state-error-primary, #ef4444)" } }, "Failed to load heatmap: ", error, /* @__PURE__ */ import_react.default.createElement( + return /* @__PURE__ */ React.createElement("div", { style: { padding: "20px", color: "var(--dsw-alias-state-error-primary, #ef4444)" } }, "Failed to load heatmap: ", error, /* @__PURE__ */ React.createElement( "button", { onClick: fetchStats, @@ -170,10 +184,10 @@ function HeatmapView() { "Retry" )); } - if (!data) return null; - const { summary } = data; + if (!data || !activeView) return null; + const { summary } = activeView; const cacheHitPercent = summary.totalTokens > 0 ? Math.round(summary.totalCacheReadTokens / summary.totalTokens * 100) : 0; - return /* @__PURE__ */ import_react.default.createElement( + return /* @__PURE__ */ React.createElement( "div", { style: { @@ -186,7 +200,7 @@ function HeatmapView() { color: "var(--dsw-alias-label-primary, #eeeeee)" } }, - /* @__PURE__ */ import_react.default.createElement( + /* @__PURE__ */ React.createElement( "div", { style: { @@ -194,11 +208,11 @@ function HeatmapView() { alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", - gap: 12, + gap: 14, marginBottom: 20 } }, - /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement( + /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement( "h2", { style: { @@ -212,77 +226,52 @@ function HeatmapView() { } }, "Token Usage Activity", - /* @__PURE__ */ import_react.default.createElement( + /* @__PURE__ */ React.createElement( "span", { style: { - fontSize: 12, - fontWeight: 500, + fontSize: 11, + fontWeight: 600, padding: "2px 8px", borderRadius: 12, - background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.15))", - color: "var(--dsw-alias-label-secondary)" + background: "rgba(79, 70, 229, 0.15)", + color: "var(--dsw-alias-brand-primary, #6366f1)", + border: "1px solid rgba(79, 70, 229, 0.3)" } }, - "Last 12 Months" + "52 Weeks" ) - ), /* @__PURE__ */ import_react.default.createElement( + ), /* @__PURE__ */ React.createElement( "p", { style: { - margin: "6px 0 0", + margin: "5px 0 0", fontSize: 13, color: "var(--dsw-alias-label-tertiary, #a0a0a0)" } }, - "Daily token volume and request step intensity across all sessions" + "Daily token volume and request step intensity across sessions" )), - /* @__PURE__ */ import_react.default.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ import_react.default.createElement( - "div", + /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6 } }, /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary)" } }, "Session:"), /* @__PURE__ */ React.createElement( + "select", { + value: selectedSessionId, + onChange: (e) => setSelectedSessionId(e.target.value), style: { - display: "inline-flex", background: "var(--dsw-alias-bg-layer-2, #2c2c2c)", border: "1px solid var(--dsw-alias-border-l2, #494949)", + color: "var(--dsw-alias-label-primary, #eeeeee)", borderRadius: 8, - padding: 2 + padding: "6px 12px", + fontSize: 12, + outline: "none", + cursor: "pointer", + maxWidth: 240 } }, - /* @__PURE__ */ import_react.default.createElement( - "button", - { - onClick: () => setThemeKey("green"), - style: { - border: 0, - background: themeKey === "green" ? "var(--dsw-alias-bg-layer-1, #1e1e1e)" : "transparent", - color: themeKey === "green" ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-tertiary)", - fontSize: 12, - padding: "4px 10px", - borderRadius: 6, - cursor: "pointer", - fontWeight: themeKey === "green" ? 600 : 400 - } - }, - "Emerald" - ), - /* @__PURE__ */ import_react.default.createElement( - "button", - { - onClick: () => setThemeKey("deepseek"), - style: { - border: 0, - background: themeKey === "deepseek" ? "var(--dsw-alias-bg-layer-1, #1e1e1e)" : "transparent", - color: themeKey === "deepseek" ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-tertiary)", - fontSize: 12, - padding: "4px 10px", - borderRadius: 6, - cursor: "pointer", - fontWeight: themeKey === "deepseek" ? 600 : 400 - } - }, - "Indigo" - ) - ), /* @__PURE__ */ import_react.default.createElement( + /* @__PURE__ */ React.createElement("option", { value: "all" }, "All Sessions (", data.sessions.length, " total)"), + data.sessions.map((s) => /* @__PURE__ */ React.createElement("option", { key: s.id, value: s.id }, new Date(s.createdAt).toLocaleDateString(), " \xB7 ", s.title.slice(0, 32))) + )), /* @__PURE__ */ React.createElement( "button", { onClick: fetchStats, @@ -304,7 +293,7 @@ function HeatmapView() { "\u21BB Refresh" )) ), - /* @__PURE__ */ import_react.default.createElement( + /* @__PURE__ */ React.createElement( "div", { style: { @@ -314,7 +303,7 @@ function HeatmapView() { marginBottom: 20 } }, - /* @__PURE__ */ import_react.default.createElement( + /* @__PURE__ */ React.createElement( "div", { style: { @@ -324,11 +313,11 @@ function HeatmapView() { padding: "14px 16px" } }, - /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 } }, "Total Tokens"), - /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "var(--dsw-alias-label-primary, #ffffff)" } }, formatNumber(summary.totalTokens)), - /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" } }, formatExact(summary.totalTokens), " total") + /* @__PURE__ */ React.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 } }, "Total Tokens"), + /* @__PURE__ */ React.createElement("div", { style: { fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "var(--dsw-alias-brand-primary, #6366f1)" } }, formatNumber(summary.totalTokens)), + /* @__PURE__ */ React.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" } }, formatExact(summary.totalTokens), " tokens") ), - /* @__PURE__ */ import_react.default.createElement( + /* @__PURE__ */ React.createElement( "div", { style: { @@ -338,11 +327,11 @@ function HeatmapView() { padding: "14px 16px" } }, - /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 } }, "Cache Hit Ratio"), - /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "#10b981" } }, cacheHitPercent, "%"), - /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" } }, formatNumber(summary.totalCacheReadTokens), " cached reads") + /* @__PURE__ */ React.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 } }, "Cache Hit Ratio"), + /* @__PURE__ */ React.createElement("div", { style: { fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "#38bdf8" } }, cacheHitPercent, "%"), + /* @__PURE__ */ React.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" } }, formatNumber(summary.totalCacheReadTokens), " cached reads") ), - /* @__PURE__ */ import_react.default.createElement( + /* @__PURE__ */ React.createElement( "div", { style: { @@ -352,11 +341,11 @@ function HeatmapView() { padding: "14px 16px" } }, - /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 } }, "Daily Average"), - /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "var(--dsw-alias-label-primary, #ffffff)" } }, formatNumber(summary.dailyAverage)), - /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" } }, "Across ", summary.activeDays, " active days") + /* @__PURE__ */ React.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 } }, selectedSessionId === "all" ? "Daily Average" : "Total Steps"), + /* @__PURE__ */ React.createElement("div", { style: { fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "var(--dsw-alias-label-primary, #ffffff)" } }, selectedSessionId === "all" ? formatNumber(summary.dailyAverage) : summary.totalTurns), + /* @__PURE__ */ React.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" } }, selectedSessionId === "all" ? `Across ${summary.activeDays} active days` : `${formatNumber(summary.totalInputTokens + summary.totalOutputTokens)} billed`) ), - /* @__PURE__ */ import_react.default.createElement( + /* @__PURE__ */ React.createElement( "div", { style: { @@ -366,12 +355,12 @@ function HeatmapView() { padding: "14px 16px" } }, - /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 } }, "Active Streak"), - /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "var(--dsw-alias-label-primary, #ffffff)" } }, summary.currentStreak, " ", summary.currentStreak === 1 ? "day" : "days"), - /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" } }, "Longest streak: ", summary.longestStreak, " days") + /* @__PURE__ */ React.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 } }, selectedSessionId === "all" ? "Active Streak" : "Active Days"), + /* @__PURE__ */ React.createElement("div", { style: { fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "var(--dsw-alias-label-primary, #ffffff)" } }, selectedSessionId === "all" ? `${summary.currentStreak} days` : `${summary.activeDays} days`), + /* @__PURE__ */ React.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" } }, selectedSessionId === "all" ? `Longest streak: ${summary.longestStreak} days` : `Session: ${activeView.sessionTitle.slice(0, 24)}`) ) ), - /* @__PURE__ */ import_react.default.createElement( + /* @__PURE__ */ React.createElement( "div", { style: { @@ -383,7 +372,7 @@ function HeatmapView() { overflowX: "auto" } }, - /* @__PURE__ */ import_react.default.createElement( + /* @__PURE__ */ React.createElement( "div", { style: { @@ -395,10 +384,10 @@ function HeatmapView() { fontWeight: 600 } }, - /* @__PURE__ */ import_react.default.createElement("span", null, "Contribution Calendar"), - summary.peakDay && /* @__PURE__ */ import_react.default.createElement("span", { style: { fontSize: 12, fontWeight: 400, color: "var(--dsw-alias-label-secondary)" } }, "Peak day: ", /* @__PURE__ */ import_react.default.createElement("strong", { style: { color: "var(--dsw-alias-label-primary)" } }, summary.peakDay.date), " (", formatNumber(summary.peakDay.tokens), " tokens)") + /* @__PURE__ */ React.createElement("span", null, "Contribution Calendar"), + summary.peakDay && /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12, fontWeight: 400, color: "var(--dsw-alias-label-secondary)" } }, "Peak day: ", /* @__PURE__ */ React.createElement("strong", { style: { color: "var(--dsw-alias-label-primary)" } }, summary.peakDay.date), " (", formatNumber(summary.peakDay.tokens), " tokens)") ), - calendarGrid && /* @__PURE__ */ import_react.default.createElement("div", { style: { display: "inline-block", minWidth: 780 } }, /* @__PURE__ */ import_react.default.createElement("div", { style: { display: "flex", marginLeft: 32, marginBottom: 4, height: 16 } }, monthHeaders.map((m, i) => /* @__PURE__ */ import_react.default.createElement( + calendarGrid && /* @__PURE__ */ React.createElement("div", { style: { display: "inline-block", minWidth: 780 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", marginLeft: 32, marginBottom: 4, height: 16 } }, monthHeaders.map((m, i) => /* @__PURE__ */ React.createElement( "div", { key: i, @@ -412,7 +401,7 @@ function HeatmapView() { } }, m.label - ))), /* @__PURE__ */ import_react.default.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ import_react.default.createElement( + ))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement( "div", { style: { @@ -426,14 +415,14 @@ function HeatmapView() { paddingTop: 1 } }, - DAY_LABELS.map((lbl, idx) => /* @__PURE__ */ import_react.default.createElement("div", { key: idx, style: { height: 11, lineHeight: "11px" } }, lbl)) - ), /* @__PURE__ */ import_react.default.createElement("div", { style: { display: "flex", gap: 3 } }, calendarGrid.map((week, colIdx) => /* @__PURE__ */ import_react.default.createElement("div", { key: colIdx, style: { display: "flex", flexDirection: "column", gap: 3 } }, week.map((day, rowIdx) => { + DAY_LABELS.map((lbl, idx) => /* @__PURE__ */ React.createElement("div", { key: idx, style: { height: 11, lineHeight: "11px" } }, lbl)) + ), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 3 } }, calendarGrid.map((week, colIdx) => /* @__PURE__ */ React.createElement("div", { key: colIdx, style: { display: "flex", flexDirection: "column", gap: 3 } }, week.map((day, rowIdx) => { if (!day) { - return /* @__PURE__ */ import_react.default.createElement("div", { key: rowIdx, style: { width: 11, height: 11 } }); + return /* @__PURE__ */ React.createElement("div", { key: rowIdx, style: { width: 11, height: 11 } }); } const lvl = day.stats.level || 0; - const bg = activeTheme.levels[lvl]; - return /* @__PURE__ */ import_react.default.createElement( + const bg = DEEPSEEK_LEVELS[lvl]; + return /* @__PURE__ */ React.createElement( "div", { key: rowIdx, @@ -457,7 +446,7 @@ function HeatmapView() { } } ); - }))))), /* @__PURE__ */ import_react.default.createElement( + }))))), /* @__PURE__ */ React.createElement( "div", { style: { @@ -470,8 +459,8 @@ function HeatmapView() { color: "var(--dsw-alias-label-tertiary, #a0a0a0)" } }, - /* @__PURE__ */ import_react.default.createElement("span", null, "Less"), - activeTheme.levels.map((c, i) => /* @__PURE__ */ import_react.default.createElement( + /* @__PURE__ */ React.createElement("span", null, "Less"), + DEEPSEEK_LEVELS.map((c, i) => /* @__PURE__ */ React.createElement( "div", { key: i, @@ -484,10 +473,10 @@ function HeatmapView() { } } )), - /* @__PURE__ */ import_react.default.createElement("span", null, "More") + /* @__PURE__ */ React.createElement("span", null, "More") )) ), - hoveredDay && tooltipPos && /* @__PURE__ */ import_react.default.createElement( + hoveredDay && tooltipPos && /* @__PURE__ */ React.createElement( "div", { style: { @@ -507,22 +496,22 @@ function HeatmapView() { whiteSpace: "nowrap" } }, - /* @__PURE__ */ import_react.default.createElement("div", { style: { fontWeight: 650, color: "var(--dsw-alias-label-primary, #ffffff)", marginBottom: 4 } }, (/* @__PURE__ */ new Date(hoveredDay.date + "T00:00:00")).toLocaleDateString(void 0, { + /* @__PURE__ */ React.createElement("div", { style: { fontWeight: 650, color: "var(--dsw-alias-label-primary, #ffffff)", marginBottom: 4 } }, (/* @__PURE__ */ new Date(hoveredDay.date + "T00:00:00")).toLocaleDateString(void 0, { weekday: "short", year: "numeric", month: "short", day: "numeric" })), - hoveredDay.stats.totalTokens === 0 ? /* @__PURE__ */ import_react.default.createElement("div", { style: { color: "var(--dsw-alias-label-tertiary)" } }, "No activity recorded") : /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("div", { style: { color: "#34d399", fontWeight: 600 } }, formatExact(hoveredDay.stats.totalTokens), " tokens"), /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary)", marginTop: 2 } }, hoveredDay.stats.turns, " requests / steps"), /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 10, color: "var(--dsw-alias-label-tertiary)", marginTop: 4 } }, "Billed: ", formatNumber(hoveredDay.stats.inputTokens + hoveredDay.stats.outputTokens), " \xB7 Cache: ", formatNumber(hoveredDay.stats.cacheReadTokens))) + hoveredDay.stats.totalTokens === 0 ? /* @__PURE__ */ React.createElement("div", { style: { color: "var(--dsw-alias-label-tertiary)" } }, "No activity recorded") : /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { style: { color: "var(--dsw-alias-brand-primary, #818cf8)", fontWeight: 600 } }, formatExact(hoveredDay.stats.totalTokens), " tokens"), /* @__PURE__ */ React.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary)", marginTop: 2 } }, hoveredDay.stats.turns, " requests / steps"), /* @__PURE__ */ React.createElement("div", { style: { fontSize: 10, color: "var(--dsw-alias-label-tertiary)", marginTop: 4 } }, "Billed: ", formatNumber(hoveredDay.stats.inputTokens + hoveredDay.stats.outputTokens), " \xB7 Cache: ", formatNumber(hoveredDay.stats.cacheReadTokens))) ) ); } // src/client/HeatmapModal.tsx -var import_react2 = __toESM(require("react"), 1); +import React2 from "react"; function HeatmapModal({ open, onClose }) { if (!open) return null; - return /* @__PURE__ */ import_react2.default.createElement( + return /* @__PURE__ */ React2.createElement( "div", { role: "dialog", @@ -542,7 +531,7 @@ function HeatmapModal({ open, onClose }) { overflowY: "auto" } }, - /* @__PURE__ */ import_react2.default.createElement( + /* @__PURE__ */ React2.createElement( "div", { onClick: (e) => e.stopPropagation(), @@ -559,7 +548,7 @@ function HeatmapModal({ open, onClose }) { padding: "24px 28px" } }, - /* @__PURE__ */ import_react2.default.createElement( + /* @__PURE__ */ React2.createElement( "button", { onClick: onClose, @@ -583,7 +572,7 @@ function HeatmapModal({ open, onClose }) { }, "\u2715" ), - /* @__PURE__ */ import_react2.default.createElement(HeatmapView, null) + /* @__PURE__ */ React2.createElement(HeatmapView, null) ) ); } @@ -608,8 +597,8 @@ function apply(ctx) { } if (typeof document !== "undefined") { let ModalController = function() { - const [isOpen, setIsOpen] = (0, import_react3.useState)(false); - (0, import_react3.useEffect)(() => { + const [isOpen, setIsOpen] = useState2(false); + useEffect2(() => { const handleOpen = () => setIsOpen(true); const handleClose = () => setIsOpen(false); window.addEventListener("dsh:open-token-heatmap", handleOpen); @@ -620,57 +609,78 @@ function apply(ctx) { window.removeEventListener("dsh:open-token-heatmap", handleOpen); }; }, []); - return /* @__PURE__ */ import_react3.default.createElement(HeatmapModal, { open: isOpen, onClose: () => setIsOpen(false) }); + return /* @__PURE__ */ React3.createElement(HeatmapModal, { open: isOpen, onClose: () => setIsOpen(false) }); }; const modalContainer = document.createElement("div"); modalContainer.id = "dsh-token-heatmap-modal-root"; document.body.appendChild(modalContainer); - const root = (0, import_client.createRoot)(modalContainer); - root.render(/* @__PURE__ */ import_react3.default.createElement(ModalController, null)); + const root = createRoot(modalContainer); + root.render(/* @__PURE__ */ React3.createElement(ModalController, null)); const observer = new MutationObserver(() => { - const statDialogs = document.querySelectorAll( - 'div[role="dialog"][aria-label*="usage" i], div[role="dialog"][aria-label*="Token" i], [data-session-stats-usage]' - ); - statDialogs.forEach((dialog) => { - if (dialog.querySelector(".dsh-heatmap-trigger-btn")) return; - const button = document.createElement("button"); - button.className = "dsh-heatmap-trigger-btn"; - button.type = "button"; - button.innerHTML = "View 12-Month Activity Heatmap \u2192"; - button.style.cssText = ` - display: flex; - align-items: center; - justify-content: center; - width: 100%; - margin-top: 12px; - padding: 8px 12px; - background: var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.12)); - border: 1px solid var(--dsw-alias-border-l2, #494949); - border-radius: 8px; - color: var(--dsw-alias-brand-primary, #6366f1); - font-size: 12px; - font-weight: 550; - cursor: pointer; - transition: background 0.15s ease, border-color 0.15s ease; - `; - button.onmouseenter = () => { - button.style.background = "var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.2))"; - button.style.borderColor = "var(--dsw-alias-brand-primary, #6366f1)"; - }; - button.onmouseleave = () => { - button.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.12))"; - button.style.borderColor = "var(--dsw-alias-border-l2, #494949)"; - }; - button.onclick = (e) => { - e.preventDefault(); - e.stopPropagation(); + const usageList = document.querySelector("[data-session-stats-usage]"); + if (!usageList) return; + const dialogContainer = usageList.closest('div[role="dialog"]') || usageList.parentElement; + if (!dialogContainer) return; + if (dialogContainer.querySelector(".dsh-heatmap-trigger-btn") || document.querySelector(".dsh-heatmap-trigger-btn")) { + return; + } + const button = document.createElement("button"); + button.className = "dsh-heatmap-trigger-btn"; + button.type = "button"; + button.innerHTML = "View Token Usage Heatmap \u2192"; + button.style.cssText = ` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + margin-top: 12px; + padding: 8px 12px; + background: rgba(79, 70, 229, 0.12); + border: 1px solid rgba(79, 70, 229, 0.35); + border-radius: 8px; + color: var(--dsw-alias-brand-primary, #6366f1); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease; + `; + button.onmouseenter = () => { + button.style.background = "rgba(79, 70, 229, 0.22)"; + button.style.borderColor = "var(--dsw-alias-brand-primary, #6366f1)"; + }; + button.onmouseleave = () => { + button.style.background = "rgba(79, 70, 229, 0.12)"; + button.style.borderColor = "rgba(79, 70, 229, 0.35)"; + }; + button.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + const settingsTrigger = document.querySelector( + 'button[aria-label*="Setting" i], [data-sidebar-settings] button, button.settings' + ); + if (settingsTrigger) { + settingsTrigger.click(); + setTimeout(() => { + const heatmapTab = Array.from(document.querySelectorAll("*")).find( + (el) => el.textContent?.trim() === "Token Heatmap" && (el.tagName === "BUTTON" || el.tagName === "DIV" || el.getAttribute("role") === "tab" || el.classList.contains("ant-list-item")) + ); + if (heatmapTab) { + heatmapTab.click(); + } else { + window.dispatchEvent(new CustomEvent("dsh:open-token-heatmap")); + } + }, 120); + } else { window.dispatchEvent(new CustomEvent("dsh:open-token-heatmap")); - }; - dialog.appendChild(button); - }); + } + }; + dialogContainer.appendChild(button); }); observer.observe(document.body, { childList: true, subtree: true }); } } -return module.exports; } }); -//# sourceMappingURL=client.js.map +export { + apply, + inject, + name +}; diff --git a/lib/index.js b/lib/index.js index 274317c..e5317dc 100644 --- a/lib/index.js +++ b/lib/index.js @@ -42,4 +42,3 @@ export { inject, name }; -//# sourceMappingURL=index.js.map diff --git a/src/client/HeatmapView.tsx b/src/client/HeatmapView.tsx index 8e348bb..453566c 100644 --- a/src/client/HeatmapView.tsx +++ b/src/client/HeatmapView.tsx @@ -10,6 +10,18 @@ export interface DayStats { level?: number; } +export interface SessionSummary { + id: string; + title: string; + createdAt: number; + totalTokens: number; + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + turns: number; + days: Record; +} + export interface SummaryStats { totalTokens: number; totalInputTokens: number; @@ -26,6 +38,7 @@ export interface SummaryStats { export interface HeatmapResponse { summary: SummaryStats; days: Record; + sessions: SessionSummary[]; startDate: string; endDate: string; } @@ -44,35 +57,20 @@ function formatExact(num: number): string { const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; const DAY_LABELS = ["", "Mon", "", "Wed", "", "Fri", ""]; -// Themes: Green (GitHub classic) and DeepSeek Blue/Indigo -const COLOR_THEMES = { - green: { - name: "GitHub Emerald", - levels: [ - "var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.08))", - "#0e4429", - "#006d32", - "#26a641", - "#39d353", - ], - }, - deepseek: { - name: "DeepSeek Indigo", - levels: [ - "var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.08))", - "#1e1e4a", - "#2e3188", - "#434fc7", - "#6366f1", - ], - }, -}; +// DeepSeek Blue/Indigo theme levels (OpenCode & Codex style) +const DEEPSEEK_LEVELS = [ + "var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.08))", + "#1e2640", + "#2d3e8b", + "#3d52c4", + "#4f46e5", +]; export function HeatmapView() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [themeKey, setThemeKey] = useState<"green" | "deepseek">("green"); + const [selectedSessionId, setSelectedSessionId] = useState("all"); const [hoveredDay, setHoveredDay] = useState<{ date: string; stats: DayStats } | null>(null); const [tooltipPos, setTooltipPos] = useState<{ x: number; y: number } | null>(null); @@ -97,18 +95,97 @@ export function HeatmapView() { fetchStats(); }, []); + // Compute active days & stats based on session filter + const activeView = useMemo(() => { + if (!data) return null; + + if (selectedSessionId === "all") { + return { + summary: data.summary, + days: data.days, + sessionTitle: "All Sessions (Global)", + }; + } + + const session = data.sessions.find((s) => s.id === selectedSessionId); + if (!session) { + return { + summary: data.summary, + days: data.days, + sessionTitle: "All Sessions (Global)", + }; + } + + // Recalculate summary for this specific session + let totalTokens = session.totalTokens; + let totalInputTokens = session.inputTokens; + let totalOutputTokens = session.outputTokens; + let totalCacheReadTokens = session.cacheReadTokens; + let totalTurns = session.turns; + let activeDays = Object.values(session.days).filter((d) => d.totalTokens > 0).length; + let dailyAverage = activeDays > 0 ? Math.round(totalTokens / activeDays) : 0; + let peakDay: { date: string; tokens: number; turns: number } | null = null; + + for (const [date, stats] of Object.entries(session.days)) { + if (!peakDay || stats.totalTokens > peakDay.tokens) { + peakDay = { date, tokens: stats.totalTokens, turns: stats.turns }; + } + } + + // Re-level for this session + const positiveValues = Object.values(session.days) + .map((d) => d.totalTokens) + .filter((t) => t > 0) + .sort((a, b) => a - b); + + let q1 = 1_000_000; + let q2 = 5_000_000; + let q3 = 15_000_000; + + if (positiveValues.length >= 4) { + q1 = positiveValues[Math.floor(positiveValues.length * 0.25)]; + q2 = positiveValues[Math.floor(positiveValues.length * 0.50)]; + q3 = positiveValues[Math.floor(positiveValues.length * 0.75)]; + } + + const leveledDays: Record = {}; + for (const [date, stats] of Object.entries(session.days)) { + let lvl = 0; + if (stats.totalTokens === 0) lvl = 0; + else if (stats.totalTokens <= q1) lvl = 1; + else if (stats.totalTokens <= q2) lvl = 2; + else if (stats.totalTokens <= q3) lvl = 3; + else lvl = 4; + leveledDays[date] = { ...stats, level: lvl }; + } + + return { + summary: { + totalTokens, + totalInputTokens, + totalOutputTokens, + totalCacheReadTokens, + totalTurns, + activeDays, + currentStreak: 0, + longestStreak: 0, + peakDay, + dailyAverage, + }, + days: leveledDays, + sessionTitle: session.title, + }; + }, [data, selectedSessionId]); + // Compute 52-week calendar grid: 7 rows x 53 columns const calendarGrid = useMemo(() => { - if (!data) return null; + if (!activeView) return null; const weeks: Array> = []; const now = new Date(); - // End on current week Saturday const end = new Date(now); - // Start 52 weeks back on Sunday const start = new Date(now); start.setDate(start.getDate() - 364); - // Align start to nearest previous Sunday start.setDate(start.getDate() - start.getDay()); let current = new Date(start); @@ -116,7 +193,7 @@ export function HeatmapView() { while (current <= end || currentWeek.length > 0) { const dateKey = `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, "0")}-${String(current.getDate()).padStart(2, "0")}`; - const dayStats = data.days[dateKey] || { + const dayStats = activeView.days[dateKey] || { totalTokens: 0, inputTokens: 0, outputTokens: 0, @@ -141,7 +218,7 @@ export function HeatmapView() { } return weeks; - }, [data]); + }, [activeView]); // Compute month headers const monthHeaders = useMemo(() => { @@ -164,12 +241,10 @@ export function HeatmapView() { return headers; }, [calendarGrid]); - const activeTheme = COLOR_THEMES[themeKey]; - if (loading && !data) { return (
- Loading token usage heatmap... + Loading token usage statistics...
); } @@ -196,9 +271,9 @@ export function HeatmapView() { ); } - if (!data) return null; + if (!data || !activeView) return null; - const { summary } = data; + const { summary } = activeView; const cacheHitPercent = summary.totalTokens > 0 ? Math.round((summary.totalCacheReadTokens / summary.totalTokens) * 100) @@ -216,14 +291,14 @@ export function HeatmapView() { color: "var(--dsw-alias-label-primary, #eeeeee)", }} > - {/* Title Bar */} + {/* Top Header & Filter Controls (OpenCode / Codex style) */}
@@ -242,70 +317,56 @@ export function HeatmapView() { Token Usage Activity - Last 12 Months + 52 Weeks

- Daily token volume and request step intensity across all sessions + Daily token volume and request step intensity across sessions

- {/* Action Controls */} + {/* Right side controls: Session selector & Refresh */}
- {/* Theme Switcher */} -
-
- {/* Summary Cards */} + {/* Summary Cards Row (Codex / OpenCode style) */}
Total Tokens
-
+
{formatNumber(summary.totalTokens)}
- {formatExact(summary.totalTokens)} total + {formatExact(summary.totalTokens)} tokens
- {/* Card 2: Cache Efficiency */} + {/* Card 2: Cache Hit */}
Cache Hit Ratio
-
+
{cacheHitPercent}%
@@ -379,7 +440,7 @@ export function HeatmapView() {
- {/* Card 3: Daily Average */} + {/* Card 3: Daily Average / Turns */}
- Daily Average + {selectedSessionId === "all" ? "Daily Average" : "Total Steps"}
- {formatNumber(summary.dailyAverage)} + {selectedSessionId === "all" ? formatNumber(summary.dailyAverage) : summary.totalTurns}
- Across {summary.activeDays} active days + {selectedSessionId === "all" ? `Across ${summary.activeDays} active days` : `${formatNumber(summary.totalInputTokens + summary.totalOutputTokens)} billed`}
- {/* Card 4: Streaks */} + {/* Card 4: Streak / Activity */}
- Active Streak + {selectedSessionId === "all" ? "Active Streak" : "Active Days"}
- {summary.currentStreak} {summary.currentStreak === 1 ? "day" : "days"} + {selectedSessionId === "all" ? `${summary.currentStreak} days` : `${summary.activeDays} days`}
- Longest streak: {summary.longestStreak} days + {selectedSessionId === "all" ? `Longest streak: ${summary.longestStreak} days` : `Session: ${activeView.sessionTitle.slice(0, 24)}`}
@@ -502,7 +563,7 @@ export function HeatmapView() { return
; } const lvl = day.stats.level || 0; - const bg = activeTheme.levels[lvl]; + const bg = DEEPSEEK_LEVELS[lvl]; return (
Less - {activeTheme.levels.map((c, i) => ( + {DEEPSEEK_LEVELS.map((c, i) => (
No activity recorded
) : (
-
+
{formatExact(hoveredDay.stats.totalTokens)} tokens
diff --git a/src/client/index.tsx b/src/client/index.tsx index 8fc7aee..95da3cc 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -53,52 +53,78 @@ export function apply(ctx: any) { root.render(); - // 3. Observe DOM for in-session token usage dialog to attach "View Heatmap" button + // 3. Observe DOM for in-session token usage dialog: strictly single button const observer = new MutationObserver(() => { - // Find the open session stats dialog - const statDialogs = document.querySelectorAll( - 'div[role="dialog"][aria-label*="usage" i], div[role="dialog"][aria-label*="Token" i], [data-session-stats-usage]' - ); + // Find the dl with data-session-stats-usage + const usageList = document.querySelector('[data-session-stats-usage]'); + if (!usageList) return; - statDialogs.forEach((dialog) => { - if (dialog.querySelector(".dsh-heatmap-trigger-btn")) return; + const dialogContainer = usageList.closest('div[role="dialog"]') || usageList.parentElement; + if (!dialogContainer) return; - const button = document.createElement("button"); - button.className = "dsh-heatmap-trigger-btn"; - button.type = "button"; - button.innerHTML = "View 12-Month Activity Heatmap →"; - button.style.cssText = ` - display: flex; - align-items: center; - justify-content: center; - width: 100%; - margin-top: 12px; - padding: 8px 12px; - background: var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.12)); - border: 1px solid var(--dsw-alias-border-l2, #494949); - border-radius: 8px; - color: var(--dsw-alias-brand-primary, #6366f1); - font-size: 12px; - font-weight: 550; - cursor: pointer; - transition: background 0.15s ease, border-color 0.15s ease; - `; - button.onmouseenter = () => { - button.style.background = "var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.2))"; - button.style.borderColor = "var(--dsw-alias-brand-primary, #6366f1)"; - }; - button.onmouseleave = () => { - button.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.12))"; - button.style.borderColor = "var(--dsw-alias-border-l2, #494949)"; - }; - button.onclick = (e) => { - e.preventDefault(); - e.stopPropagation(); + // Strictly ensure only one button is ever attached + if (dialogContainer.querySelector(".dsh-heatmap-trigger-btn") || document.querySelector(".dsh-heatmap-trigger-btn")) { + return; + } + + const button = document.createElement("button"); + button.className = "dsh-heatmap-trigger-btn"; + button.type = "button"; + button.innerHTML = "View Token Usage Heatmap →"; + button.style.cssText = ` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + margin-top: 12px; + padding: 8px 12px; + background: rgba(79, 70, 229, 0.12); + border: 1px solid rgba(79, 70, 229, 0.35); + border-radius: 8px; + color: var(--dsw-alias-brand-primary, #6366f1); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease; + `; + button.onmouseenter = () => { + button.style.background = "rgba(79, 70, 229, 0.22)"; + button.style.borderColor = "var(--dsw-alias-brand-primary, #6366f1)"; + }; + button.onmouseleave = () => { + button.style.background = "rgba(79, 70, 229, 0.12)"; + button.style.borderColor = "rgba(79, 70, 229, 0.35)"; + }; + button.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + + // Try navigating to Settings -> Token Heatmap + const settingsTrigger = document.querySelector( + 'button[aria-label*="Setting" i], [data-sidebar-settings] button, button.settings' + ); + + if (settingsTrigger) { + (settingsTrigger as HTMLElement).click(); + setTimeout(() => { + const heatmapTab = Array.from(document.querySelectorAll("*")).find( + (el) => + el.textContent?.trim() === "Token Heatmap" && + (el.tagName === "BUTTON" || el.tagName === "DIV" || el.getAttribute("role") === "tab" || el.classList.contains("ant-list-item")) + ); + if (heatmapTab) { + (heatmapTab as HTMLElement).click(); + } else { + window.dispatchEvent(new CustomEvent("dsh:open-token-heatmap")); + } + }, 120); + } else { + // Open direct interactive modal window.dispatchEvent(new CustomEvent("dsh:open-token-heatmap")); - }; + } + }; - dialog.appendChild(button); - }); + dialogContainer.appendChild(button); }); observer.observe(document.body, { childList: true, subtree: true }); diff --git a/src/server/aggregator.ts b/src/server/aggregator.ts index 54a6e63..dacdc9a 100644 --- a/src/server/aggregator.ts +++ b/src/server/aggregator.ts @@ -13,6 +13,18 @@ export interface DayStats { level?: number; // 0..4 } +export interface SessionSummary { + id: string; + title: string; + createdAt: number; + totalTokens: number; + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + turns: number; + days: Record; +} + export interface SummaryStats { totalTokens: number; totalInputTokens: number; @@ -29,13 +41,14 @@ export interface SummaryStats { export interface HeatmapResponse { summary: SummaryStats; days: Record; + sessions: SessionSummary[]; startDate: string; endDate: string; } interface CacheEntry { mtimeMs: number; - days: Record; + session: SessionSummary; } const fileCache = new Map(); @@ -58,53 +71,87 @@ function findSessionFiles(dir: string): string[] { return results; } -async function parseSessionFile(filePath: string): Promise> { +async function parseSessionFile(filePath: string): Promise { try { const stat = fs.statSync(filePath); const cached = fileCache.get(filePath); if (cached && cached.mtimeMs === stat.mtimeMs) { - return cached.days; + return cached.session; } const isZstd = filePath.endsWith(".zstd"); const days: Record = {}; + let sessionId = path.basename(path.dirname(filePath)); + let title = ""; + let createdAt = stat.birthtimeMs || stat.mtimeMs; + let totalTokens = 0; + let totalInputTokens = 0; + let totalOutputTokens = 0; + let totalCacheReadTokens = 0; + let totalTurns = 0; await new Promise((resolve) => { let buffer = ""; const handleLine = (line: string) => { - if (!line.includes('"usage"')) return; - try { - const obj = JSON.parse(line); - const time = obj.time || (obj.data && obj.data.time); - const usage = obj.usage || (obj.data && obj.data.usage); - if (time && usage) { - const d = new Date(time); - const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; - const input = Number(usage.inputTokens) || 0; - const output = Number(usage.outputTokens) || 0; - const cacheRead = Number(usage.cacheReadTokens) || 0; - const cacheWrite = Number(usage.cacheWriteTokens) || 0; - const total = input + output + cacheRead; + if (line.includes('"type":"session"')) { + try { + const obj = JSON.parse(line); + if (obj.id) sessionId = obj.id; + if (obj.createdAt) createdAt = obj.createdAt; + } catch {} + } + if (!title && line.includes('"session/title"')) { + try { + const obj = JSON.parse(line); + if (obj.data?.title) title = obj.data.title; + } catch {} + } + if (!title && line.includes('"subagent/descriptor"')) { + try { + const obj = JSON.parse(line); + if (obj.data?.label) title = obj.data.label; + } catch {} + } + if (line.includes('"usage"')) { + try { + const obj = JSON.parse(line); + const time = obj.time || (obj.data && obj.data.time); + const usage = obj.usage || (obj.data && obj.data.usage); + if (time && usage) { + const d = new Date(time); + const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; + const input = Number(usage.inputTokens) || 0; + const output = Number(usage.outputTokens) || 0; + const cacheRead = Number(usage.cacheReadTokens) || 0; + const cacheWrite = Number(usage.cacheWriteTokens) || 0; + const total = input + output + cacheRead; - if (!days[dateKey]) { - days[dateKey] = { - totalTokens: 0, - inputTokens: 0, - outputTokens: 0, - cacheReadTokens: 0, - cacheWriteTokens: 0, - turns: 0, - }; + if (!days[dateKey]) { + days[dateKey] = { + totalTokens: 0, + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: 0, + cacheWriteTokens: 0, + turns: 0, + }; + } + days[dateKey].totalTokens += total; + days[dateKey].inputTokens += input; + days[dateKey].outputTokens += output; + days[dateKey].cacheReadTokens += cacheRead; + days[dateKey].cacheWriteTokens += cacheWrite; + days[dateKey].turns += 1; + + totalTokens += total; + totalInputTokens += input; + totalOutputTokens += output; + totalCacheReadTokens += cacheRead; + totalTurns += 1; } - days[dateKey].totalTokens += total; - days[dateKey].inputTokens += input; - days[dateKey].outputTokens += output; - days[dateKey].cacheReadTokens += cacheRead; - days[dateKey].cacheWriteTokens += cacheWrite; - days[dateKey].turns += 1; + } catch { + // Ignore malformed lines } - } catch { - // Ignore malformed lines } }; @@ -143,22 +190,37 @@ async function parseSessionFile(filePath: string): Promise { const sessionsRoot = customSessionsRoot || path.join(os.homedir(), ".dsh", "sessions"); const files = findSessionFiles(sessionsRoot); - const parsedResults = await Promise.all(files.map(parseSessionFile)); + const parsedSessions = (await Promise.all(files.map(parseSessionFile))).filter(Boolean) as SessionSummary[]; + + // Sort sessions newest first + parsedSessions.sort((a, b) => b.createdAt - a.createdAt); const days: Record = {}; - for (const fileDays of parsedResults) { - for (const [date, stats] of Object.entries(fileDays)) { + for (const session of parsedSessions) { + for (const [date, stats] of Object.entries(session.days)) { if (!days[date]) { days[date] = { totalTokens: 0, @@ -178,7 +240,7 @@ export async function aggregateTokens(customSessionsRoot?: string): Promise d.totalTokens) .filter((t) => t > 0) @@ -187,13 +249,11 @@ export async function aggregateTokens(customSessionsRoot?: string): Promise= 4) { q1 = positiveValues[Math.floor(positiveValues.length * 0.25)]; q2 = positiveValues[Math.floor(positiveValues.length * 0.50)]; q3 = positiveValues[Math.floor(positiveValues.length * 0.75)]; - q4 = positiveValues[Math.floor(positiveValues.length * 0.90)]; } for (const day of Object.values(days)) { @@ -238,7 +298,6 @@ export async function aggregateTokens(customSessionsRoot?: string): Promise 0 ? new Date(now) : (days[yesterdayKey]?.totalTokens ?? 0) > 0 ? new Date(cursor) : null; if (streakDate) { @@ -280,7 +339,7 @@ export async function aggregateTokens(customSessionsRoot?: string): Promise