refactor: DeepSeek blue theme, single session meter button, session filter dropdown
This commit is contained in:
parent
a53865caf6
commit
63cddbf117
6 changed files with 639 additions and 437 deletions
|
|
@ -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
|
|
||||||
|
|
|
||||||
440
lib/client.js
440
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
|
// 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
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -42,4 +42,3 @@ export {
|
||||||
inject,
|
inject,
|
||||||
name
|
name
|
||||||
};
|
};
|
||||||
//# sourceMappingURL=index.js.map
|
|
||||||
|
|
|
||||||
|
|
@ -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 }}>
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue