refactor: DeepSeek blue theme, single session meter button, session filter dropdown

This commit is contained in:
katR 2026-10-09 12:02:55 +05:30
parent a53865caf6
commit 63cddbf117
6 changed files with 639 additions and 437 deletions

View file

@ -24,44 +24,80 @@ async function parseSessionFile(filePath) {
const stat = fs.statSync(filePath); const stat = fs.statSync(filePath);
const cached = fileCache.get(filePath); const cached = fileCache.get(filePath);
if (cached && cached.mtimeMs === stat.mtimeMs) { if (cached && cached.mtimeMs === stat.mtimeMs) {
return cached.days; return cached.session;
} }
const isZstd = filePath.endsWith(".zstd"); const isZstd = filePath.endsWith(".zstd");
const days = {}; 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) => { await new Promise((resolve) => {
let buffer = ""; let buffer = "";
const handleLine = (line) => { const handleLine = (line) => {
if (!line.includes('"usage"')) return; if (line.includes('"type":"session"')) {
try { try {
const obj = JSON.parse(line); const obj = JSON.parse(line);
const time = obj.time || obj.data && obj.data.time; if (obj.id) sessionId = obj.id;
const usage = obj.usage || obj.data && obj.data.usage; if (obj.createdAt) createdAt = obj.createdAt;
if (time && usage) { } catch {
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; if (!title && line.includes('"session/title"')) {
const output = Number(usage.outputTokens) || 0; try {
const cacheRead = Number(usage.cacheReadTokens) || 0; const obj = JSON.parse(line);
const cacheWrite = Number(usage.cacheWriteTokens) || 0; if (obj.data?.title) title = obj.data.title;
const total = input + output + cacheRead; } catch {
if (!days[dateKey]) { }
days[dateKey] = { }
totalTokens: 0, if (!title && line.includes('"subagent/descriptor"')) {
inputTokens: 0, try {
outputTokens: 0, const obj = JSON.parse(line);
cacheReadTokens: 0, if (obj.data?.label) title = obj.data.label;
cacheWriteTokens: 0, } catch {
turns: 0 }
}; }
} if (line.includes('"usage"')) {
days[dateKey].totalTokens += total; try {
days[dateKey].inputTokens += input; const obj = JSON.parse(line);
days[dateKey].outputTokens += output; const time = obj.time || obj.data && obj.data.time;
days[dateKey].cacheReadTokens += cacheRead; const usage = obj.usage || obj.data && obj.data.usage;
days[dateKey].cacheWriteTokens += cacheWrite; if (time && usage) {
days[dateKey].turns += 1; 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) { if (isZstd) {
@ -98,19 +134,31 @@ async function parseSessionFile(filePath) {
stream.on("error", () => resolve()); stream.on("error", () => resolve());
} }
}); });
fileCache.set(filePath, { mtimeMs: stat.mtimeMs, days }); const session = {
return days; 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 { } catch {
return {}; return null;
} }
} }
async function aggregateTokens(customSessionsRoot) { async function aggregateTokens(customSessionsRoot) {
const sessionsRoot = customSessionsRoot || path.join(os.homedir(), ".dsh", "sessions"); const sessionsRoot = customSessionsRoot || path.join(os.homedir(), ".dsh", "sessions");
const files = findSessionFiles(sessionsRoot); 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 = {}; const days = {};
for (const fileDays of parsedResults) { for (const session of parsedSessions) {
for (const [date, stats] of Object.entries(fileDays)) { for (const [date, stats] of Object.entries(session.days)) {
if (!days[date]) { if (!days[date]) {
days[date] = { days[date] = {
totalTokens: 0, totalTokens: 0,
@ -133,12 +181,10 @@ async function aggregateTokens(customSessionsRoot) {
let q1 = 5e6; let q1 = 5e6;
let q2 = 25e6; let q2 = 25e6;
let q3 = 75e6; let q3 = 75e6;
let q4 = 15e7;
if (positiveValues.length >= 4) { if (positiveValues.length >= 4) {
q1 = positiveValues[Math.floor(positiveValues.length * 0.25)]; q1 = positiveValues[Math.floor(positiveValues.length * 0.25)];
q2 = positiveValues[Math.floor(positiveValues.length * 0.5)]; q2 = positiveValues[Math.floor(positiveValues.length * 0.5)];
q3 = positiveValues[Math.floor(positiveValues.length * 0.75)]; q3 = positiveValues[Math.floor(positiveValues.length * 0.75)];
q4 = positiveValues[Math.floor(positiveValues.length * 0.9)];
} }
for (const day of Object.values(days)) { for (const day of Object.values(days)) {
if (day.totalTokens === 0) day.level = 0; if (day.totalTokens === 0) day.level = 0;
@ -221,6 +267,7 @@ async function aggregateTokens(customSessionsRoot) {
dailyAverage dailyAverage
}, },
days, days,
sessions: parsedSessions,
startDate, startDate,
endDate endDate
}; };
@ -228,4 +275,3 @@ async function aggregateTokens(customSessionsRoot) {
export { export {
aggregateTokens aggregateTokens
}; };
//# sourceMappingURL=aggregator.js.map

View file

@ -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 // src/client/index.tsx
var index_exports = {}; import React3, { useState as useState2, useEffect as useEffect2 } from "react";
__export(index_exports, { import { createRoot } from "react-dom/client";
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");
// src/client/HeatmapView.tsx // src/client/HeatmapView.tsx
var import_react = __toESM(require("react"), 1); import React, { useEffect, useState, useMemo } from "react";
function formatNumber(num) { function formatNumber(num) {
if (num >= 1e9) return (num / 1e9).toFixed(2) + "B"; if (num >= 1e9) return (num / 1e9).toFixed(2) + "B";
if (num >= 1e6) return (num / 1e6).toFixed(1) + "M"; 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 MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var DAY_LABELS = ["", "Mon", "", "Wed", "", "Fri", ""]; var DAY_LABELS = ["", "Mon", "", "Wed", "", "Fri", ""];
var COLOR_THEMES = { var DEEPSEEK_LEVELS = [
green: { "var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.08))",
name: "GitHub Emerald", "#1e2640",
levels: [ "#2d3e8b",
"var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.08))", "#3d52c4",
"#0e4429", "#4f46e5"
"#006d32", ];
"#26a641",
"#39d353"
]
},
deepseek: {
name: "DeepSeek Indigo",
levels: [
"var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.08))",
"#1e1e4a",
"#2e3188",
"#434fc7",
"#6366f1"
]
}
};
function HeatmapView() { function HeatmapView() {
const [data, setData] = (0, import_react.useState)(null); const [data, setData] = useState(null);
const [loading, setLoading] = (0, import_react.useState)(true); const [loading, setLoading] = useState(true);
const [error, setError] = (0, import_react.useState)(null); const [error, setError] = useState(null);
const [themeKey, setThemeKey] = (0, import_react.useState)("green"); const [selectedSessionId, setSelectedSessionId] = useState("all");
const [hoveredDay, setHoveredDay] = (0, import_react.useState)(null); const [hoveredDay, setHoveredDay] = useState(null);
const [tooltipPos, setTooltipPos] = (0, import_react.useState)(null); const [tooltipPos, setTooltipPos] = useState(null);
const fetchStats = () => { const fetchStats = () => {
setLoading(true); setLoading(true);
fetch("/api/dsh-token-heatmap/stats").then((r) => { fetch("/api/dsh-token-heatmap/stats").then((r) => {
@ -93,11 +42,77 @@ function HeatmapView() {
setLoading(false); setLoading(false);
}); });
}; };
(0, import_react.useEffect)(() => { useEffect(() => {
fetchStats(); fetchStats();
}, []); }, []);
const calendarGrid = (0, import_react.useMemo)(() => { const activeView = useMemo(() => {
if (!data) return null; 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 weeks = [];
const now = /* @__PURE__ */ new Date(); const now = /* @__PURE__ */ new Date();
const end = new Date(now); const end = new Date(now);
@ -108,7 +123,7 @@ function HeatmapView() {
let currentWeek = []; let currentWeek = [];
while (current <= end || currentWeek.length > 0) { while (current <= end || currentWeek.length > 0) {
const dateKey = `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, "0")}-${String(current.getDate()).padStart(2, "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, totalTokens: 0,
inputTokens: 0, inputTokens: 0,
outputTokens: 0, outputTokens: 0,
@ -130,8 +145,8 @@ function HeatmapView() {
if (current > end && currentWeek.length === 0) break; if (current > end && currentWeek.length === 0) break;
} }
return weeks; return weeks;
}, [data]); }, [activeView]);
const monthHeaders = (0, import_react.useMemo)(() => { const monthHeaders = useMemo(() => {
if (!calendarGrid) return []; if (!calendarGrid) return [];
const headers = []; const headers = [];
let lastMonth = -1; let lastMonth = -1;
@ -148,12 +163,11 @@ function HeatmapView() {
}); });
return headers; return headers;
}, [calendarGrid]); }, [calendarGrid]);
const activeTheme = COLOR_THEMES[themeKey];
if (loading && !data) { 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) { 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", "button",
{ {
onClick: fetchStats, onClick: fetchStats,
@ -170,10 +184,10 @@ function HeatmapView() {
"Retry" "Retry"
)); ));
} }
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) : 0; const cacheHitPercent = summary.totalTokens > 0 ? Math.round(summary.totalCacheReadTokens / summary.totalTokens * 100) : 0;
return /* @__PURE__ */ import_react.default.createElement( return /* @__PURE__ */ React.createElement(
"div", "div",
{ {
style: { style: {
@ -186,7 +200,7 @@ function HeatmapView() {
color: "var(--dsw-alias-label-primary, #eeeeee)" color: "var(--dsw-alias-label-primary, #eeeeee)"
} }
}, },
/* @__PURE__ */ import_react.default.createElement( /* @__PURE__ */ React.createElement(
"div", "div",
{ {
style: { style: {
@ -194,11 +208,11 @@ function HeatmapView() {
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
flexWrap: "wrap", flexWrap: "wrap",
gap: 12, gap: 14,
marginBottom: 20 marginBottom: 20
} }
}, },
/* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement( /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(
"h2", "h2",
{ {
style: { style: {
@ -212,77 +226,52 @@ function HeatmapView() {
} }
}, },
"Token Usage Activity", "Token Usage Activity",
/* @__PURE__ */ import_react.default.createElement( /* @__PURE__ */ React.createElement(
"span", "span",
{ {
style: { style: {
fontSize: 12, fontSize: 11,
fontWeight: 500, fontWeight: 600,
padding: "2px 8px", padding: "2px 8px",
borderRadius: 12, borderRadius: 12,
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.15))", background: "rgba(79, 70, 229, 0.15)",
color: "var(--dsw-alias-label-secondary)" 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", "p",
{ {
style: { style: {
margin: "6px 0 0", margin: "5px 0 0",
fontSize: 13, fontSize: 13,
color: "var(--dsw-alias-label-tertiary, #a0a0a0)" 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( /* @__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(
"div", "select",
{ {
value: selectedSessionId,
onChange: (e) => setSelectedSessionId(e.target.value),
style: { style: {
display: "inline-flex",
background: "var(--dsw-alias-bg-layer-2, #2c2c2c)", background: "var(--dsw-alias-bg-layer-2, #2c2c2c)",
border: "1px solid var(--dsw-alias-border-l2, #494949)", border: "1px solid var(--dsw-alias-border-l2, #494949)",
color: "var(--dsw-alias-label-primary, #eeeeee)",
borderRadius: 8, borderRadius: 8,
padding: 2 padding: "6px 12px",
fontSize: 12,
outline: "none",
cursor: "pointer",
maxWidth: 240
} }
}, },
/* @__PURE__ */ import_react.default.createElement( /* @__PURE__ */ React.createElement("option", { value: "all" }, "All Sessions (", data.sessions.length, " total)"),
"button", 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(
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(
"button", "button",
{ {
onClick: fetchStats, onClick: fetchStats,
@ -304,7 +293,7 @@ function HeatmapView() {
"\u21BB Refresh" "\u21BB Refresh"
)) ))
), ),
/* @__PURE__ */ import_react.default.createElement( /* @__PURE__ */ React.createElement(
"div", "div",
{ {
style: { style: {
@ -314,7 +303,7 @@ function HeatmapView() {
marginBottom: 20 marginBottom: 20
} }
}, },
/* @__PURE__ */ import_react.default.createElement( /* @__PURE__ */ React.createElement(
"div", "div",
{ {
style: { style: {
@ -324,11 +313,11 @@ function HeatmapView() {
padding: "14px 16px" 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__ */ React.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__ */ React.createElement("div", { style: { fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "var(--dsw-alias-brand-primary, #6366f1)" } }, 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-secondary, #b9b9b9)" } }, formatExact(summary.totalTokens), " tokens")
), ),
/* @__PURE__ */ import_react.default.createElement( /* @__PURE__ */ React.createElement(
"div", "div",
{ {
style: { style: {
@ -338,11 +327,11 @@ function HeatmapView() {
padding: "14px 16px" 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__ */ React.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__ */ React.createElement("div", { style: { fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "#38bdf8" } }, 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-secondary, #b9b9b9)" } }, formatNumber(summary.totalCacheReadTokens), " cached reads")
), ),
/* @__PURE__ */ import_react.default.createElement( /* @__PURE__ */ React.createElement(
"div", "div",
{ {
style: { style: {
@ -352,11 +341,11 @@ function HeatmapView() {
padding: "14px 16px" 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__ */ 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__ */ 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__ */ 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__ */ 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-secondary, #b9b9b9)" } }, selectedSessionId === "all" ? `Across ${summary.activeDays} active days` : `${formatNumber(summary.totalInputTokens + summary.totalOutputTokens)} billed`)
), ),
/* @__PURE__ */ import_react.default.createElement( /* @__PURE__ */ React.createElement(
"div", "div",
{ {
style: { style: {
@ -366,12 +355,12 @@ function HeatmapView() {
padding: "14px 16px" 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__ */ 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__ */ 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__ */ 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__ */ 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-secondary, #b9b9b9)" } }, selectedSessionId === "all" ? `Longest streak: ${summary.longestStreak} days` : `Session: ${activeView.sessionTitle.slice(0, 24)}`)
) )
), ),
/* @__PURE__ */ import_react.default.createElement( /* @__PURE__ */ React.createElement(
"div", "div",
{ {
style: { style: {
@ -383,7 +372,7 @@ function HeatmapView() {
overflowX: "auto" overflowX: "auto"
} }
}, },
/* @__PURE__ */ import_react.default.createElement( /* @__PURE__ */ React.createElement(
"div", "div",
{ {
style: { style: {
@ -395,10 +384,10 @@ function HeatmapView() {
fontWeight: 600 fontWeight: 600
} }
}, },
/* @__PURE__ */ import_react.default.createElement("span", null, "Contribution Calendar"), /* @__PURE__ */ React.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)") 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", "div",
{ {
key: i, key: i,
@ -412,7 +401,7 @@ function HeatmapView() {
} }
}, },
m.label 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", "div",
{ {
style: { style: {
@ -426,14 +415,14 @@ function HeatmapView() {
paddingTop: 1 paddingTop: 1
} }
}, },
DAY_LABELS.map((lbl, idx) => /* @__PURE__ */ import_react.default.createElement("div", { key: idx, style: { height: 11, lineHeight: "11px" } }, lbl)) DAY_LABELS.map((lbl, idx) => /* @__PURE__ */ React.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) => { ), /* @__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) { 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 lvl = day.stats.level || 0;
const bg = activeTheme.levels[lvl]; const bg = DEEPSEEK_LEVELS[lvl];
return /* @__PURE__ */ import_react.default.createElement( return /* @__PURE__ */ React.createElement(
"div", "div",
{ {
key: rowIdx, key: rowIdx,
@ -457,7 +446,7 @@ function HeatmapView() {
} }
} }
); );
}))))), /* @__PURE__ */ import_react.default.createElement( }))))), /* @__PURE__ */ React.createElement(
"div", "div",
{ {
style: { style: {
@ -470,8 +459,8 @@ function HeatmapView() {
color: "var(--dsw-alias-label-tertiary, #a0a0a0)" color: "var(--dsw-alias-label-tertiary, #a0a0a0)"
} }
}, },
/* @__PURE__ */ import_react.default.createElement("span", null, "Less"), /* @__PURE__ */ React.createElement("span", null, "Less"),
activeTheme.levels.map((c, i) => /* @__PURE__ */ import_react.default.createElement( DEEPSEEK_LEVELS.map((c, i) => /* @__PURE__ */ React.createElement(
"div", "div",
{ {
key: i, 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", "div",
{ {
style: { style: {
@ -507,22 +496,22 @@ function HeatmapView() {
whiteSpace: "nowrap" 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", weekday: "short",
year: "numeric", year: "numeric",
month: "short", month: "short",
day: "numeric" 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 // src/client/HeatmapModal.tsx
var import_react2 = __toESM(require("react"), 1); import React2 from "react";
function HeatmapModal({ open, onClose }) { function HeatmapModal({ open, onClose }) {
if (!open) return null; if (!open) return null;
return /* @__PURE__ */ import_react2.default.createElement( return /* @__PURE__ */ React2.createElement(
"div", "div",
{ {
role: "dialog", role: "dialog",
@ -542,7 +531,7 @@ function HeatmapModal({ open, onClose }) {
overflowY: "auto" overflowY: "auto"
} }
}, },
/* @__PURE__ */ import_react2.default.createElement( /* @__PURE__ */ React2.createElement(
"div", "div",
{ {
onClick: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(),
@ -559,7 +548,7 @@ function HeatmapModal({ open, onClose }) {
padding: "24px 28px" padding: "24px 28px"
} }
}, },
/* @__PURE__ */ import_react2.default.createElement( /* @__PURE__ */ React2.createElement(
"button", "button",
{ {
onClick: onClose, onClick: onClose,
@ -583,7 +572,7 @@ function HeatmapModal({ open, onClose }) {
}, },
"\u2715" "\u2715"
), ),
/* @__PURE__ */ import_react2.default.createElement(HeatmapView, null) /* @__PURE__ */ React2.createElement(HeatmapView, null)
) )
); );
} }
@ -608,8 +597,8 @@ function apply(ctx) {
} }
if (typeof document !== "undefined") { if (typeof document !== "undefined") {
let ModalController = function() { let ModalController = function() {
const [isOpen, setIsOpen] = (0, import_react3.useState)(false); const [isOpen, setIsOpen] = useState2(false);
(0, import_react3.useEffect)(() => { useEffect2(() => {
const handleOpen = () => setIsOpen(true); const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false); const handleClose = () => setIsOpen(false);
window.addEventListener("dsh:open-token-heatmap", handleOpen); window.addEventListener("dsh:open-token-heatmap", handleOpen);
@ -620,57 +609,78 @@ function apply(ctx) {
window.removeEventListener("dsh:open-token-heatmap", handleOpen); 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"); const modalContainer = document.createElement("div");
modalContainer.id = "dsh-token-heatmap-modal-root"; modalContainer.id = "dsh-token-heatmap-modal-root";
document.body.appendChild(modalContainer); document.body.appendChild(modalContainer);
const root = (0, import_client.createRoot)(modalContainer); const root = createRoot(modalContainer);
root.render(/* @__PURE__ */ import_react3.default.createElement(ModalController, null)); root.render(/* @__PURE__ */ React3.createElement(ModalController, null));
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
const statDialogs = document.querySelectorAll( const usageList = document.querySelector("[data-session-stats-usage]");
'div[role="dialog"][aria-label*="usage" i], div[role="dialog"][aria-label*="Token" i], [data-session-stats-usage]' if (!usageList) return;
); const dialogContainer = usageList.closest('div[role="dialog"]') || usageList.parentElement;
statDialogs.forEach((dialog) => { if (!dialogContainer) return;
if (dialog.querySelector(".dsh-heatmap-trigger-btn")) return; if (dialogContainer.querySelector(".dsh-heatmap-trigger-btn") || document.querySelector(".dsh-heatmap-trigger-btn")) {
const button = document.createElement("button"); return;
button.className = "dsh-heatmap-trigger-btn"; }
button.type = "button"; const button = document.createElement("button");
button.innerHTML = "<span>View 12-Month Activity Heatmap \u2192</span>"; button.className = "dsh-heatmap-trigger-btn";
button.style.cssText = ` button.type = "button";
display: flex; button.innerHTML = "<span>View Token Usage Heatmap \u2192</span>";
align-items: center; button.style.cssText = `
justify-content: center; display: flex;
width: 100%; align-items: center;
margin-top: 12px; justify-content: center;
padding: 8px 12px; width: 100%;
background: var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.12)); margin-top: 12px;
border: 1px solid var(--dsw-alias-border-l2, #494949); padding: 8px 12px;
border-radius: 8px; background: rgba(79, 70, 229, 0.12);
color: var(--dsw-alias-brand-primary, #6366f1); border: 1px solid rgba(79, 70, 229, 0.35);
font-size: 12px; border-radius: 8px;
font-weight: 550; color: var(--dsw-alias-brand-primary, #6366f1);
cursor: pointer; font-size: 12px;
transition: background 0.15s ease, border-color 0.15s ease; font-weight: 600;
`; cursor: pointer;
button.onmouseenter = () => { transition: background 0.15s ease, border-color 0.15s ease;
button.style.background = "var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.2))"; `;
button.style.borderColor = "var(--dsw-alias-brand-primary, #6366f1)"; button.onmouseenter = () => {
}; button.style.background = "rgba(79, 70, 229, 0.22)";
button.onmouseleave = () => { button.style.borderColor = "var(--dsw-alias-brand-primary, #6366f1)";
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.onmouseleave = () => {
}; button.style.background = "rgba(79, 70, 229, 0.12)";
button.onclick = (e) => { button.style.borderColor = "rgba(79, 70, 229, 0.35)";
e.preventDefault(); };
e.stopPropagation(); 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")); window.dispatchEvent(new CustomEvent("dsh:open-token-heatmap"));
}; }
dialog.appendChild(button); };
}); dialogContainer.appendChild(button);
}); });
observer.observe(document.body, { childList: true, subtree: true }); observer.observe(document.body, { childList: true, subtree: true });
} }
} }
return module.exports; } }); export {
//# sourceMappingURL=client.js.map apply,
inject,
name
};

View file

@ -42,4 +42,3 @@ export {
inject, inject,
name name
}; };
//# sourceMappingURL=index.js.map

View file

@ -10,6 +10,18 @@ export interface DayStats {
level?: number; level?: number;
} }
export interface SessionSummary {
id: string;
title: string;
createdAt: number;
totalTokens: number;
inputTokens: number;
outputTokens: number;
cacheReadTokens: number;
turns: number;
days: Record<string, DayStats>;
}
export interface SummaryStats { export interface SummaryStats {
totalTokens: number; totalTokens: number;
totalInputTokens: number; totalInputTokens: number;
@ -26,6 +38,7 @@ export interface SummaryStats {
export interface HeatmapResponse { export interface HeatmapResponse {
summary: SummaryStats; summary: SummaryStats;
days: Record<string, DayStats>; days: Record<string, DayStats>;
sessions: SessionSummary[];
startDate: string; startDate: string;
endDate: 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 MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const DAY_LABELS = ["", "Mon", "", "Wed", "", "Fri", ""]; const DAY_LABELS = ["", "Mon", "", "Wed", "", "Fri", ""];
// Themes: Green (GitHub classic) and DeepSeek Blue/Indigo // DeepSeek Blue/Indigo theme levels (OpenCode & Codex style)
const COLOR_THEMES = { const DEEPSEEK_LEVELS = [
green: { "var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.08))",
name: "GitHub Emerald", "#1e2640",
levels: [ "#2d3e8b",
"var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.08))", "#3d52c4",
"#0e4429", "#4f46e5",
"#006d32", ];
"#26a641",
"#39d353",
],
},
deepseek: {
name: "DeepSeek Indigo",
levels: [
"var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.08))",
"#1e1e4a",
"#2e3188",
"#434fc7",
"#6366f1",
],
},
};
export function HeatmapView() { export function HeatmapView() {
const [data, setData] = useState<HeatmapResponse | null>(null); const [data, setData] = useState<HeatmapResponse | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [themeKey, setThemeKey] = useState<"green" | "deepseek">("green"); const [selectedSessionId, setSelectedSessionId] = useState<string>("all");
const [hoveredDay, setHoveredDay] = useState<{ date: string; stats: DayStats } | null>(null); const [hoveredDay, setHoveredDay] = useState<{ date: string; stats: DayStats } | null>(null);
const [tooltipPos, setTooltipPos] = useState<{ x: number; y: number } | null>(null); const [tooltipPos, setTooltipPos] = useState<{ x: number; y: number } | null>(null);
@ -97,18 +95,97 @@ export function HeatmapView() {
fetchStats(); 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<string, DayStats> = {};
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 // Compute 52-week calendar grid: 7 rows x 53 columns
const calendarGrid = useMemo(() => { const calendarGrid = useMemo(() => {
if (!data) return null; if (!activeView) return null;
const weeks: Array<Array<{ date: string; stats: DayStats } | null>> = []; const weeks: Array<Array<{ date: string; stats: DayStats } | null>> = [];
const now = new Date(); const now = new Date();
// End on current week Saturday
const end = new Date(now); const end = new Date(now);
// Start 52 weeks back on Sunday
const start = new Date(now); const start = new Date(now);
start.setDate(start.getDate() - 364); start.setDate(start.getDate() - 364);
// Align start to nearest previous Sunday
start.setDate(start.getDate() - start.getDay()); start.setDate(start.getDate() - start.getDay());
let current = new Date(start); let current = new Date(start);
@ -116,7 +193,7 @@ export function HeatmapView() {
while (current <= end || currentWeek.length > 0) { while (current <= end || currentWeek.length > 0) {
const dateKey = `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, "0")}-${String(current.getDate()).padStart(2, "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, totalTokens: 0,
inputTokens: 0, inputTokens: 0,
outputTokens: 0, outputTokens: 0,
@ -141,7 +218,7 @@ export function HeatmapView() {
} }
return weeks; return weeks;
}, [data]); }, [activeView]);
// Compute month headers // Compute month headers
const monthHeaders = useMemo(() => { const monthHeaders = useMemo(() => {
@ -164,12 +241,10 @@ export function HeatmapView() {
return headers; return headers;
}, [calendarGrid]); }, [calendarGrid]);
const activeTheme = COLOR_THEMES[themeKey];
if (loading && !data) { if (loading && !data) {
return ( return (
<div style={{ padding: "40px 20px", textAlign: "center", color: "var(--dsw-alias-label-secondary)" }}> <div style={{ padding: "40px 20px", textAlign: "center", color: "var(--dsw-alias-label-secondary)" }}>
Loading token usage heatmap... Loading token usage statistics...
</div> </div>
); );
} }
@ -196,9 +271,9 @@ export function HeatmapView() {
); );
} }
if (!data) return null; if (!data || !activeView) return null;
const { summary } = data; const { summary } = activeView;
const cacheHitPercent = const cacheHitPercent =
summary.totalTokens > 0 summary.totalTokens > 0
? Math.round((summary.totalCacheReadTokens / summary.totalTokens) * 100) ? Math.round((summary.totalCacheReadTokens / summary.totalTokens) * 100)
@ -216,14 +291,14 @@ export function HeatmapView() {
color: "var(--dsw-alias-label-primary, #eeeeee)", color: "var(--dsw-alias-label-primary, #eeeeee)",
}} }}
> >
{/* Title Bar */} {/* Top Header & Filter Controls (OpenCode / Codex style) */}
<div <div
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
flexWrap: "wrap", flexWrap: "wrap",
gap: 12, gap: 14,
marginBottom: 20, marginBottom: 20,
}} }}
> >
@ -242,70 +317,56 @@ export function HeatmapView() {
Token Usage Activity Token Usage Activity
<span <span
style={{ style={{
fontSize: 12, fontSize: 11,
fontWeight: 500, fontWeight: 600,
padding: "2px 8px", padding: "2px 8px",
borderRadius: 12, borderRadius: 12,
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.15))", background: "rgba(79, 70, 229, 0.15)",
color: "var(--dsw-alias-label-secondary)", color: "var(--dsw-alias-brand-primary, #6366f1)",
border: "1px solid rgba(79, 70, 229, 0.3)",
}} }}
> >
Last 12 Months 52 Weeks
</span> </span>
</h2> </h2>
<p <p
style={{ style={{
margin: "6px 0 0", margin: "5px 0 0",
fontSize: 13, fontSize: 13,
color: "var(--dsw-alias-label-tertiary, #a0a0a0)", 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
</p> </p>
</div> </div>
{/* Action Controls */} {/* Right side controls: Session selector & Refresh */}
<div style={{ display: "flex", alignItems: "center", gap: 10 }}> <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
{/* Theme Switcher */} {/* Session Filter Dropdown */}
<div <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
style={{ <span style={{ fontSize: 12, color: "var(--dsw-alias-label-tertiary)" }}>Session:</span>
display: "inline-flex", <select
background: "var(--dsw-alias-bg-layer-2, #2c2c2c)", value={selectedSessionId}
border: "1px solid var(--dsw-alias-border-l2, #494949)", onChange={(e) => setSelectedSessionId(e.target.value)}
borderRadius: 8,
padding: 2,
}}
>
<button
onClick={() => setThemeKey("green")}
style={{ style={{
border: 0, background: "var(--dsw-alias-bg-layer-2, #2c2c2c)",
background: themeKey === "green" ? "var(--dsw-alias-bg-layer-1, #1e1e1e)" : "transparent", border: "1px solid var(--dsw-alias-border-l2, #494949)",
color: themeKey === "green" ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-tertiary)", color: "var(--dsw-alias-label-primary, #eeeeee)",
borderRadius: 8,
padding: "6px 12px",
fontSize: 12, fontSize: 12,
padding: "4px 10px", outline: "none",
borderRadius: 6,
cursor: "pointer", cursor: "pointer",
fontWeight: themeKey === "green" ? 600 : 400, maxWidth: 240,
}} }}
> >
Emerald <option value="all">All Sessions ({data.sessions.length} total)</option>
</button> {data.sessions.map((s) => (
<button <option key={s.id} value={s.id}>
onClick={() => setThemeKey("deepseek")} {new Date(s.createdAt).toLocaleDateString()} · {s.title.slice(0, 32)}
style={{ </option>
border: 0, ))}
background: themeKey === "deepseek" ? "var(--dsw-alias-bg-layer-1, #1e1e1e)" : "transparent", </select>
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
</button>
</div> </div>
<button <button
@ -330,7 +391,7 @@ export function HeatmapView() {
</div> </div>
</div> </div>
{/* Summary Cards */} {/* Summary Cards Row (Codex / OpenCode style) */}
<div <div
style={{ style={{
display: "grid", display: "grid",
@ -351,15 +412,15 @@ export function HeatmapView() {
<div style={{ fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}> <div style={{ fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}>
Total Tokens Total Tokens
</div> </div>
<div style={{ fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "var(--dsw-alias-label-primary, #ffffff)" }}> <div style={{ fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "var(--dsw-alias-brand-primary, #6366f1)" }}>
{formatNumber(summary.totalTokens)} {formatNumber(summary.totalTokens)}
</div> </div>
<div style={{ fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" }}> <div style={{ fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" }}>
{formatExact(summary.totalTokens)} total {formatExact(summary.totalTokens)} tokens
</div> </div>
</div> </div>
{/* Card 2: Cache Efficiency */} {/* Card 2: Cache Hit */}
<div <div
style={{ style={{
background: "var(--dsw-alias-bg-layer-2, #2c2c2c)", background: "var(--dsw-alias-bg-layer-2, #2c2c2c)",
@ -371,7 +432,7 @@ export function HeatmapView() {
<div style={{ fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}> <div style={{ fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}>
Cache Hit Ratio Cache Hit Ratio
</div> </div>
<div style={{ fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "#10b981" }}> <div style={{ fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "#38bdf8" }}>
{cacheHitPercent}% {cacheHitPercent}%
</div> </div>
<div style={{ fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" }}> <div style={{ fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" }}>
@ -379,7 +440,7 @@ export function HeatmapView() {
</div> </div>
</div> </div>
{/* Card 3: Daily Average */} {/* Card 3: Daily Average / Turns */}
<div <div
style={{ style={{
background: "var(--dsw-alias-bg-layer-2, #2c2c2c)", background: "var(--dsw-alias-bg-layer-2, #2c2c2c)",
@ -389,17 +450,17 @@ export function HeatmapView() {
}} }}
> >
<div style={{ fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}> <div style={{ fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}>
Daily Average {selectedSessionId === "all" ? "Daily Average" : "Total Steps"}
</div> </div>
<div style={{ fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "var(--dsw-alias-label-primary, #ffffff)" }}> <div style={{ fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "var(--dsw-alias-label-primary, #ffffff)" }}>
{formatNumber(summary.dailyAverage)} {selectedSessionId === "all" ? formatNumber(summary.dailyAverage) : summary.totalTurns}
</div> </div>
<div style={{ fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" }}> <div style={{ fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" }}>
Across {summary.activeDays} active days {selectedSessionId === "all" ? `Across ${summary.activeDays} active days` : `${formatNumber(summary.totalInputTokens + summary.totalOutputTokens)} billed`}
</div> </div>
</div> </div>
{/* Card 4: Streaks */} {/* Card 4: Streak / Activity */}
<div <div
style={{ style={{
background: "var(--dsw-alias-bg-layer-2, #2c2c2c)", background: "var(--dsw-alias-bg-layer-2, #2c2c2c)",
@ -409,13 +470,13 @@ export function HeatmapView() {
}} }}
> >
<div style={{ fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}> <div style={{ fontSize: 11, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}>
Active Streak {selectedSessionId === "all" ? "Active Streak" : "Active Days"}
</div> </div>
<div style={{ fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "var(--dsw-alias-label-primary, #ffffff)" }}> <div style={{ fontSize: 24, fontWeight: 700, margin: "6px 0 2px", color: "var(--dsw-alias-label-primary, #ffffff)" }}>
{summary.currentStreak} {summary.currentStreak === 1 ? "day" : "days"} {selectedSessionId === "all" ? `${summary.currentStreak} days` : `${summary.activeDays} days`}
</div> </div>
<div style={{ fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" }}> <div style={{ fontSize: 11, color: "var(--dsw-alias-label-secondary, #b9b9b9)" }}>
Longest streak: {summary.longestStreak} days {selectedSessionId === "all" ? `Longest streak: ${summary.longestStreak} days` : `Session: ${activeView.sessionTitle.slice(0, 24)}`}
</div> </div>
</div> </div>
</div> </div>
@ -502,7 +563,7 @@ export function HeatmapView() {
return <div key={rowIdx} style={{ width: 11, height: 11 }} />; return <div key={rowIdx} style={{ width: 11, height: 11 }} />;
} }
const lvl = day.stats.level || 0; const lvl = day.stats.level || 0;
const bg = activeTheme.levels[lvl]; const bg = DEEPSEEK_LEVELS[lvl];
return ( return (
<div <div
@ -549,7 +610,7 @@ export function HeatmapView() {
}} }}
> >
<span>Less</span> <span>Less</span>
{activeTheme.levels.map((c, i) => ( {DEEPSEEK_LEVELS.map((c, i) => (
<div <div
key={i} key={i}
style={{ style={{
@ -599,7 +660,7 @@ export function HeatmapView() {
<div style={{ color: "var(--dsw-alias-label-tertiary)" }}>No activity recorded</div> <div style={{ color: "var(--dsw-alias-label-tertiary)" }}>No activity recorded</div>
) : ( ) : (
<div> <div>
<div style={{ color: "#34d399", fontWeight: 600 }}> <div style={{ color: "var(--dsw-alias-brand-primary, #818cf8)", fontWeight: 600 }}>
{formatExact(hoveredDay.stats.totalTokens)} tokens {formatExact(hoveredDay.stats.totalTokens)} tokens
</div> </div>
<div style={{ fontSize: 11, color: "var(--dsw-alias-label-secondary)", marginTop: 2 }}> <div style={{ fontSize: 11, color: "var(--dsw-alias-label-secondary)", marginTop: 2 }}>

View file

@ -53,52 +53,78 @@ export function apply(ctx: any) {
root.render(<ModalController />); root.render(<ModalController />);
// 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(() => { const observer = new MutationObserver(() => {
// Find the open session stats dialog // Find the dl with data-session-stats-usage
const statDialogs = document.querySelectorAll( const usageList = document.querySelector('[data-session-stats-usage]');
'div[role="dialog"][aria-label*="usage" i], div[role="dialog"][aria-label*="Token" i], [data-session-stats-usage]' if (!usageList) return;
);
statDialogs.forEach((dialog) => { const dialogContainer = usageList.closest('div[role="dialog"]') || usageList.parentElement;
if (dialog.querySelector(".dsh-heatmap-trigger-btn")) return; if (!dialogContainer) return;
const button = document.createElement("button"); // Strictly ensure only one button is ever attached
button.className = "dsh-heatmap-trigger-btn"; if (dialogContainer.querySelector(".dsh-heatmap-trigger-btn") || document.querySelector(".dsh-heatmap-trigger-btn")) {
button.type = "button"; return;
button.innerHTML = "<span>View 12-Month Activity Heatmap →</span>"; }
button.style.cssText = `
display: flex; const button = document.createElement("button");
align-items: center; button.className = "dsh-heatmap-trigger-btn";
justify-content: center; button.type = "button";
width: 100%; button.innerHTML = "<span>View Token Usage Heatmap →</span>";
margin-top: 12px; button.style.cssText = `
padding: 8px 12px; display: flex;
background: var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.12)); align-items: center;
border: 1px solid var(--dsw-alias-border-l2, #494949); justify-content: center;
border-radius: 8px; width: 100%;
color: var(--dsw-alias-brand-primary, #6366f1); margin-top: 12px;
font-size: 12px; padding: 8px 12px;
font-weight: 550; background: rgba(79, 70, 229, 0.12);
cursor: pointer; border: 1px solid rgba(79, 70, 229, 0.35);
transition: background 0.15s ease, border-color 0.15s ease; border-radius: 8px;
`; color: var(--dsw-alias-brand-primary, #6366f1);
button.onmouseenter = () => { font-size: 12px;
button.style.background = "var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.2))"; font-weight: 600;
button.style.borderColor = "var(--dsw-alias-brand-primary, #6366f1)"; cursor: pointer;
}; transition: background 0.15s ease, border-color 0.15s ease;
button.onmouseleave = () => { `;
button.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.12))"; button.onmouseenter = () => {
button.style.borderColor = "var(--dsw-alias-border-l2, #494949)"; button.style.background = "rgba(79, 70, 229, 0.22)";
}; button.style.borderColor = "var(--dsw-alias-brand-primary, #6366f1)";
button.onclick = (e) => { };
e.preventDefault(); button.onmouseleave = () => {
e.stopPropagation(); 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")); window.dispatchEvent(new CustomEvent("dsh:open-token-heatmap"));
}; }
};
dialog.appendChild(button); dialogContainer.appendChild(button);
});
}); });
observer.observe(document.body, { childList: true, subtree: true }); observer.observe(document.body, { childList: true, subtree: true });

View file

@ -13,6 +13,18 @@ export interface DayStats {
level?: number; // 0..4 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<string, DayStats>;
}
export interface SummaryStats { export interface SummaryStats {
totalTokens: number; totalTokens: number;
totalInputTokens: number; totalInputTokens: number;
@ -29,13 +41,14 @@ export interface SummaryStats {
export interface HeatmapResponse { export interface HeatmapResponse {
summary: SummaryStats; summary: SummaryStats;
days: Record<string, DayStats>; days: Record<string, DayStats>;
sessions: SessionSummary[];
startDate: string; startDate: string;
endDate: string; endDate: string;
} }
interface CacheEntry { interface CacheEntry {
mtimeMs: number; mtimeMs: number;
days: Record<string, DayStats>; session: SessionSummary;
} }
const fileCache = new Map<string, CacheEntry>(); const fileCache = new Map<string, CacheEntry>();
@ -58,53 +71,87 @@ function findSessionFiles(dir: string): string[] {
return results; return results;
} }
async function parseSessionFile(filePath: string): Promise<Record<string, DayStats>> { async function parseSessionFile(filePath: string): Promise<SessionSummary | null> {
try { try {
const stat = fs.statSync(filePath); const stat = fs.statSync(filePath);
const cached = fileCache.get(filePath); const cached = fileCache.get(filePath);
if (cached && cached.mtimeMs === stat.mtimeMs) { if (cached && cached.mtimeMs === stat.mtimeMs) {
return cached.days; return cached.session;
} }
const isZstd = filePath.endsWith(".zstd"); const isZstd = filePath.endsWith(".zstd");
const days: Record<string, DayStats> = {}; const days: Record<string, DayStats> = {};
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<void>((resolve) => { await new Promise<void>((resolve) => {
let buffer = ""; let buffer = "";
const handleLine = (line: string) => { const handleLine = (line: string) => {
if (!line.includes('"usage"')) return; if (line.includes('"type":"session"')) {
try { try {
const obj = JSON.parse(line); const obj = JSON.parse(line);
const time = obj.time || (obj.data && obj.data.time); if (obj.id) sessionId = obj.id;
const usage = obj.usage || (obj.data && obj.data.usage); if (obj.createdAt) createdAt = obj.createdAt;
if (time && usage) { } catch {}
const d = new Date(time); }
const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; if (!title && line.includes('"session/title"')) {
const input = Number(usage.inputTokens) || 0; try {
const output = Number(usage.outputTokens) || 0; const obj = JSON.parse(line);
const cacheRead = Number(usage.cacheReadTokens) || 0; if (obj.data?.title) title = obj.data.title;
const cacheWrite = Number(usage.cacheWriteTokens) || 0; } catch {}
const total = input + output + cacheRead; }
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]) { if (!days[dateKey]) {
days[dateKey] = { days[dateKey] = {
totalTokens: 0, totalTokens: 0,
inputTokens: 0, inputTokens: 0,
outputTokens: 0, outputTokens: 0,
cacheReadTokens: 0, cacheReadTokens: 0,
cacheWriteTokens: 0, cacheWriteTokens: 0,
turns: 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; } catch {
days[dateKey].inputTokens += input; // Ignore malformed lines
days[dateKey].outputTokens += output;
days[dateKey].cacheReadTokens += cacheRead;
days[dateKey].cacheWriteTokens += cacheWrite;
days[dateKey].turns += 1;
} }
} catch {
// Ignore malformed lines
} }
}; };
@ -143,22 +190,37 @@ async function parseSessionFile(filePath: string): Promise<Record<string, DaySta
} }
}); });
fileCache.set(filePath, { mtimeMs: stat.mtimeMs, days }); const session: SessionSummary = {
return days; 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 { } catch {
return {}; return null;
} }
} }
export async function aggregateTokens(customSessionsRoot?: string): Promise<HeatmapResponse> { export async function aggregateTokens(customSessionsRoot?: string): Promise<HeatmapResponse> {
const sessionsRoot = customSessionsRoot || path.join(os.homedir(), ".dsh", "sessions"); const sessionsRoot = customSessionsRoot || path.join(os.homedir(), ".dsh", "sessions");
const files = findSessionFiles(sessionsRoot); 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<string, DayStats> = {}; const days: Record<string, DayStats> = {};
for (const fileDays of parsedResults) { for (const session of parsedSessions) {
for (const [date, stats] of Object.entries(fileDays)) { for (const [date, stats] of Object.entries(session.days)) {
if (!days[date]) { if (!days[date]) {
days[date] = { days[date] = {
totalTokens: 0, totalTokens: 0,
@ -178,7 +240,7 @@ export async function aggregateTokens(customSessionsRoot?: string): Promise<Heat
} }
} }
// Calculate Levels (GitHub 0..4 intensity scale based on positive token days) // Calculate Levels (0..4 intensity scale based on positive token days)
const positiveValues = Object.values(days) const positiveValues = Object.values(days)
.map((d) => d.totalTokens) .map((d) => d.totalTokens)
.filter((t) => t > 0) .filter((t) => t > 0)
@ -187,13 +249,11 @@ export async function aggregateTokens(customSessionsRoot?: string): Promise<Heat
let q1 = 5_000_000; let q1 = 5_000_000;
let q2 = 25_000_000; let q2 = 25_000_000;
let q3 = 75_000_000; let q3 = 75_000_000;
let q4 = 150_000_000;
if (positiveValues.length >= 4) { if (positiveValues.length >= 4) {
q1 = positiveValues[Math.floor(positiveValues.length * 0.25)]; q1 = positiveValues[Math.floor(positiveValues.length * 0.25)];
q2 = positiveValues[Math.floor(positiveValues.length * 0.50)]; q2 = positiveValues[Math.floor(positiveValues.length * 0.50)];
q3 = positiveValues[Math.floor(positiveValues.length * 0.75)]; q3 = positiveValues[Math.floor(positiveValues.length * 0.75)];
q4 = positiveValues[Math.floor(positiveValues.length * 0.90)];
} }
for (const day of Object.values(days)) { for (const day of Object.values(days)) {
@ -238,7 +298,6 @@ export async function aggregateTokens(customSessionsRoot?: string): Promise<Heat
cursor.setDate(cursor.getDate() - 1); cursor.setDate(cursor.getDate() - 1);
const yesterdayKey = formatDate(cursor); const yesterdayKey = formatDate(cursor);
// If active today, start from today; else if active yesterday, start from yesterday
let streakDate = (days[todayKey]?.totalTokens ?? 0) > 0 ? new Date(now) : (days[yesterdayKey]?.totalTokens ?? 0) > 0 ? new Date(cursor) : null; let streakDate = (days[todayKey]?.totalTokens ?? 0) > 0 ? new Date(now) : (days[yesterdayKey]?.totalTokens ?? 0) > 0 ? new Date(cursor) : null;
if (streakDate) { if (streakDate) {
@ -280,7 +339,7 @@ export async function aggregateTokens(customSessionsRoot?: string): Promise<Heat
const endDate = formatDate(now); const endDate = formatDate(now);
const start = new Date(now); const start = new Date(now);
start.setDate(start.getDate() - 364); // 52 weeks = 364 days start.setDate(start.getDate() - 364);
const startDate = formatDate(start); const startDate = formatDate(start);
return { return {
@ -297,6 +356,7 @@ export async function aggregateTokens(customSessionsRoot?: string): Promise<Heat
dailyAverage, dailyAverage,
}, },
days, days,
sessions: parsedSessions,
startDate, startDate,
endDate, endDate,
}; };