@zhangferry-dev/tokendash 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -40
- package/dist/client/popover.html +44 -17
- package/dist/electron-server.cjs +13 -0
- package/dist/electron-server.cjs.map +2 -2
- package/dist/server/codexParser.d.ts +4 -5
- package/dist/server/codexParser.js +18 -6
- package/electron/main.cjs +67 -12
- package/electron/preload.cjs +3 -0
- package/electron/trayBadge.cjs +3 -1
- package/electron/trayHelper +0 -0
- package/electron/trayHelper.swift +38 -16
- package/electron-builder.yml +4 -1
- package/package.json +1 -1
- package/resources/icon.icns +0 -0
- package/resources/product_menu.png +0 -0
- package/resources/cache_diagram.html +0 -456
- package/resources/cache_diagram.png +0 -0
- package/resources/pr1_preview.png +0 -0
- package/resources/test_single_agent.png +0 -0
package/README.md
CHANGED
|
@@ -4,22 +4,47 @@ A beautiful, local web dashboard for visualizing your Claude Code, Codex, OpenCl
|
|
|
4
4
|
|
|
5
5
|
It runs locally and parses token usage data directly from local session files, presenting it in a clean, interactive React dashboard. No external CLI dependencies required.
|
|
6
6
|
|
|
7
|
-

|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- **Multi-Agent Support:** View usage for Claude Code, Codex, OpenClaw, and OpenCode.
|
|
12
|
-
- **Direct JSONL Parsing:** Reads
|
|
12
|
+
- **Direct JSONL Parsing:** Reads local session files directly — 100x faster data loading.
|
|
13
13
|
- **Detailed Metrics:** Track total tokens, cost (USD), active days, cache hit rates, and output/input ratio.
|
|
14
14
|
- **Today by Hour:** 24-hour token consumption panel showing hourly breakdown for the current day.
|
|
15
|
-
- **Code Analytics:** Visualize code change trends, tool call frequency, and productivity KPIs
|
|
16
|
-
- **Pricing Transparency:** Toggle Cost metric to see per-model pricing formula and rates.
|
|
15
|
+
- **Code Analytics:** Visualize code change trends, tool call frequency, and productivity KPIs.
|
|
17
16
|
- **Interactive Charts:** Bar/line/area charts with tooltips, model breakdowns, and time range filtering.
|
|
18
17
|
- **24-Hour Heatmap:** Activity distribution by hour and day of week, with timezone awareness.
|
|
19
18
|
- **Model & Project Distribution:** See which models and projects drive your usage.
|
|
20
19
|
- **Persistent Filters:** Your selected time range, project, and metric mode are saved automatically.
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
|
|
21
|
+
## macOS Menu Bar App
|
|
22
|
+
|
|
23
|
+
TokenDash ships as a native macOS menu-bar-only application — no Dock icon, no window clutter. It lives quietly in your status bar and gives you instant access to your AI token usage at a glance.
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
**Status Bar Badge**
|
|
28
|
+
- Displays real-time token count (e.g. `1.2K`, `32.0M`) directly in the macOS status bar
|
|
29
|
+
- Updates every 5 seconds automatically
|
|
30
|
+
- Shows cost and cache hit rate in the tooltip on hover
|
|
31
|
+
- Resilient badge: transient network or data errors won't clear your existing badge value
|
|
32
|
+
|
|
33
|
+
**Popover Dashboard**
|
|
34
|
+
- Click the status bar icon to open a compact popover with today's full breakdown:
|
|
35
|
+
- Total tokens, input/output/cache metrics at a glance
|
|
36
|
+
- Hourly consumption bar chart with peak value highlight
|
|
37
|
+
- Agent filter dropdown (show only Claude Code, Codex, etc.)
|
|
38
|
+
- Settings: launch at login, check for updates, quit
|
|
39
|
+
- The popover syncs rendered totals back to the status bar badge for maximum accuracy
|
|
40
|
+
|
|
41
|
+
**Native Integration**
|
|
42
|
+
- Written in Swift (`trayHelper.swift`) for macOS 14+ compatibility
|
|
43
|
+
- Menu-bar-only (`LSUIElement: true`) — no Dock icon, runs silently in the background
|
|
44
|
+
- Dark mode support with automatic theme switching
|
|
45
|
+
- Custom app icon
|
|
46
|
+
- Distributed as a standard macOS DMG installer
|
|
47
|
+
- Port auto-detection with fallback — works even if port 3456 is already in use
|
|
23
48
|
|
|
24
49
|
## Requirements
|
|
25
50
|
|
|
@@ -47,13 +72,7 @@ During development (`npm run dev`), Vite starts a separate development server on
|
|
|
47
72
|
|
|
48
73
|
### macOS Menu Bar App
|
|
49
74
|
|
|
50
|
-
Download the latest `TokenDash.dmg` from [GitHub Releases](https://github.com/zhangferry/tokendash/releases). Open the DMG and drag TokenDash to your Applications folder.
|
|
51
|
-
|
|
52
|
-
The menu bar app shows real-time token usage in the status bar. Click the icon to open a compact popover dashboard with:
|
|
53
|
-
- Today's token count and cost breakdown
|
|
54
|
-
- Hourly consumption chart
|
|
55
|
-
- Agent filter (show only Claude Code, Codex, etc.)
|
|
56
|
-
- Settings: launch at login, check for updates, quit
|
|
75
|
+
Download the latest `TokenDash-<version>-arm64.dmg` from [GitHub Releases](https://github.com/zhangferry/tokendash/releases). Open the DMG and drag TokenDash to your Applications folder. Launch it and the status bar icon appears immediately — no setup needed.
|
|
57
76
|
|
|
58
77
|
### Command Line Options
|
|
59
78
|
|
|
@@ -104,33 +123,6 @@ If you want to contribute or modify the dashboard locally:
|
|
|
104
123
|
- **Testing:** Vitest (unit), Playwright (E2E). Run with `npm test` and `npm run test:e2e`.
|
|
105
124
|
- **CI:** GitHub Actions pipeline for automated testing on every push and PR.
|
|
106
125
|
|
|
107
|
-
## Changelog
|
|
108
|
-
|
|
109
|
-
### v1.4.0
|
|
110
|
-
- **macOS Menu Bar App** — native tray icon with real-time token count and cost in the status bar
|
|
111
|
-
- **Popover Dashboard** — click the tray icon for a compact dashboard with hourly chart, agent filter, and settings
|
|
112
|
-
- **Native Swift Tray Helper** — lightweight binary for macOS 26+ compatibility, drawn SVG icon
|
|
113
|
-
- **Agent Filter** — choose which agents to display in both popover and tray badge, persisted across sessions
|
|
114
|
-
- **Settings Panel** — launch at login, check for updates (via GitHub Releases), quit
|
|
115
|
-
- **Update Checker** — automatic version check against GitHub Releases
|
|
116
|
-
|
|
117
|
-
### v1.3.0
|
|
118
|
-
- **Added OpenCode agent support** — parses SQLite database at `~/.local/share/opencode/opencode.db`
|
|
119
|
-
- **Added Today by Hour panel** — 24-hour token consumption breakdown for the current day
|
|
120
|
-
- **E2E test overhaul** — comprehensive Playwright test suite with fixture-based test data
|
|
121
|
-
- **Added CI pipeline** — GitHub Actions for automated testing on push and PR
|
|
122
|
-
- **Fixed model trend chart** — included `cacheReadTokens` in model trend calculations
|
|
123
|
-
|
|
124
|
-
### v1.2.0
|
|
125
|
-
- **Replaced `ccusage` CLI** with direct JSONL parser — data loads in 1-2ms instead of 12-30s
|
|
126
|
-
- **Added code analytics** — code change trend, tool call trend, daily KPIs
|
|
127
|
-
- **Added persistent disk cache** with stale-while-revalidate pattern
|
|
128
|
-
- **Fixed heatmap** — cost metric now shows real data (was always $0)
|
|
129
|
-
- **Fixed timezone handling** — correct date/hour grouping for non-UTC users
|
|
130
|
-
- **Added pricing info popup** — shows per-model pricing formula in Cost mode
|
|
131
|
-
- **Added test suite** — 49 unit tests + 6 E2E tests
|
|
132
|
-
- **Layout improvements** — model trend bar chart, side-by-side analytics panels
|
|
133
|
-
|
|
134
126
|
## License
|
|
135
127
|
|
|
136
128
|
[MIT](./LICENSE)
|
package/dist/client/popover.html
CHANGED
|
@@ -177,9 +177,10 @@
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
.y-axis {
|
|
180
|
-
display:
|
|
181
|
-
|
|
182
|
-
justify-
|
|
180
|
+
display: flex;
|
|
181
|
+
flex-direction: column;
|
|
182
|
+
justify-content: space-between;
|
|
183
|
+
align-items: flex-end;
|
|
183
184
|
width: max-content;
|
|
184
185
|
padding-block: 6px 24px;
|
|
185
186
|
color: var(--muted);
|
|
@@ -257,14 +258,14 @@
|
|
|
257
258
|
|
|
258
259
|
.bar-tip {
|
|
259
260
|
position: absolute;
|
|
260
|
-
inset-block-end: calc(var(--height) +
|
|
261
|
+
inset-block-end: calc(var(--height) + 4px);
|
|
261
262
|
font-size: 10px;
|
|
262
263
|
color: var(--fg);
|
|
263
264
|
font-family: var(--font-mono);
|
|
264
|
-
background:
|
|
265
|
-
border:
|
|
266
|
-
border-radius:
|
|
267
|
-
padding:
|
|
265
|
+
background: transparent;
|
|
266
|
+
border: none;
|
|
267
|
+
border-radius: 0;
|
|
268
|
+
padding: 0;
|
|
268
269
|
white-space: nowrap;
|
|
269
270
|
}
|
|
270
271
|
|
|
@@ -742,6 +743,7 @@
|
|
|
742
743
|
var totalOutput = 0;
|
|
743
744
|
var totalCacheRead = 0;
|
|
744
745
|
var totalTokens = 0;
|
|
746
|
+
var totalCost = 0;
|
|
745
747
|
|
|
746
748
|
(allDaily || []).forEach(function(daily) {
|
|
747
749
|
if (!daily || !daily.daily) return;
|
|
@@ -751,6 +753,7 @@
|
|
|
751
753
|
totalOutput += entry.outputTokens || 0;
|
|
752
754
|
totalCacheRead += entry.cacheReadTokens || 0;
|
|
753
755
|
totalTokens += entry.totalTokens || 0;
|
|
756
|
+
totalCost += entry.totalCost || 0;
|
|
754
757
|
});
|
|
755
758
|
|
|
756
759
|
var denominator = totalInput + totalCacheRead;
|
|
@@ -760,6 +763,14 @@
|
|
|
760
763
|
setCardValue('input', formatNumber(totalInput), totalInput === 0);
|
|
761
764
|
setCardValue('output', formatNumber(totalOutput), totalOutput === 0);
|
|
762
765
|
setCardValue('cache-rate', formatPercent(cacheRate), cacheRate === 0);
|
|
766
|
+
|
|
767
|
+
return {
|
|
768
|
+
today: todayStr,
|
|
769
|
+
agentKey: selectedAgents.slice().sort().join(','),
|
|
770
|
+
totalTokens: totalTokens,
|
|
771
|
+
totalCost: totalCost,
|
|
772
|
+
totalCacheRead: totalCacheRead
|
|
773
|
+
};
|
|
763
774
|
}
|
|
764
775
|
|
|
765
776
|
function setCardValue(id, text, isEmpty) {
|
|
@@ -809,22 +820,34 @@
|
|
|
809
820
|
return;
|
|
810
821
|
}
|
|
811
822
|
|
|
823
|
+
var axisInfo = niceStep(maxValue);
|
|
812
824
|
renderAxis(maxValue);
|
|
813
825
|
var maxEntry = selected.reduce(function(max, entry) {
|
|
814
826
|
return !max || entry.value > max.value ? entry : max;
|
|
815
827
|
}, null);
|
|
816
|
-
renderBars(selected,
|
|
828
|
+
renderBars(selected, axisInfo.top, maxEntry);
|
|
817
829
|
}
|
|
818
830
|
|
|
819
|
-
function
|
|
831
|
+
function niceStep(value) {
|
|
832
|
+
// Always divide into 3 intervals, pick a step that gives round numbers
|
|
833
|
+
if (value <= 0) return { top: 3, step: 1 };
|
|
834
|
+
var rawStep = value / 3;
|
|
835
|
+
var exp = Math.floor(Math.log10(rawStep));
|
|
836
|
+
var base = Math.pow(10, exp);
|
|
837
|
+
var frac = rawStep / base;
|
|
838
|
+
// Round step up to nearest 1, 2, or 5 × 10^n
|
|
839
|
+
var nice = frac <= 1 ? base : frac <= 2 ? 2 * base : frac <= 5 ? 5 * base : 10 * base;
|
|
840
|
+
var top = nice * 3;
|
|
841
|
+
return { top: top, step: nice };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function renderAxis(axisMax) {
|
|
820
845
|
var axis = document.getElementById('y-axis');
|
|
821
|
-
var
|
|
822
|
-
var midHigh = top * (2 / 3);
|
|
823
|
-
var midLow = top * (1 / 3);
|
|
846
|
+
var info = niceStep(axisMax);
|
|
824
847
|
axis.innerHTML = [
|
|
825
|
-
'<span>' + formatNumber(top) + '</span>',
|
|
826
|
-
'<span>' + formatNumber(
|
|
827
|
-
'<span>' + formatNumber(
|
|
848
|
+
'<span>' + formatNumber(info.top) + '</span>',
|
|
849
|
+
'<span>' + formatNumber(info.top - info.step) + '</span>',
|
|
850
|
+
'<span>' + formatNumber(info.top - 2 * info.step) + '</span>',
|
|
828
851
|
'<span>0</span>'
|
|
829
852
|
].join('');
|
|
830
853
|
}
|
|
@@ -969,6 +992,7 @@
|
|
|
969
992
|
selectedAgents = agents.slice(); // default: all selected
|
|
970
993
|
}
|
|
971
994
|
renderAgentDropdown();
|
|
995
|
+
saveSelectedAgents();
|
|
972
996
|
}
|
|
973
997
|
|
|
974
998
|
// --- Data fetching ---
|
|
@@ -1003,8 +1027,11 @@
|
|
|
1003
1027
|
});
|
|
1004
1028
|
})
|
|
1005
1029
|
.then(function(data) {
|
|
1006
|
-
renderMetrics(data.dailyResults);
|
|
1030
|
+
var traySnapshot = renderMetrics(data.dailyResults);
|
|
1007
1031
|
renderChart(data.blockResults);
|
|
1032
|
+
if (window.electronAPI && window.electronAPI.updateTraySnapshot && traySnapshot.totalTokens > 0) {
|
|
1033
|
+
window.electronAPI.updateTraySnapshot(traySnapshot).catch(function() {});
|
|
1034
|
+
}
|
|
1008
1035
|
hideError();
|
|
1009
1036
|
})
|
|
1010
1037
|
.catch(function(error) {
|
package/dist/electron-server.cjs
CHANGED
|
@@ -260,6 +260,15 @@ var TokenCountPayloadSchema = import_zod2.z.object({
|
|
|
260
260
|
type: import_zod2.z.literal("token_count"),
|
|
261
261
|
info: TokenCountInfoSchema
|
|
262
262
|
});
|
|
263
|
+
function tokenUsageKey(usage) {
|
|
264
|
+
return [
|
|
265
|
+
usage.input_tokens,
|
|
266
|
+
usage.cached_input_tokens,
|
|
267
|
+
usage.output_tokens,
|
|
268
|
+
usage.reasoning_output_tokens,
|
|
269
|
+
usage.total_tokens
|
|
270
|
+
].join(":");
|
|
271
|
+
}
|
|
263
272
|
function getSessionsDir() {
|
|
264
273
|
return (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".codex", "sessions");
|
|
265
274
|
}
|
|
@@ -304,6 +313,7 @@ function parseCodexSession(filepath) {
|
|
|
304
313
|
let model = "";
|
|
305
314
|
let createdAt = "";
|
|
306
315
|
const tokenEvents = [];
|
|
316
|
+
const seenTotalUsageSnapshots = /* @__PURE__ */ new Set();
|
|
307
317
|
for (const line of lines) {
|
|
308
318
|
const trimmed = line.trim();
|
|
309
319
|
if (!trimmed) continue;
|
|
@@ -337,6 +347,9 @@ function parseCodexSession(filepath) {
|
|
|
337
347
|
}
|
|
338
348
|
const info = parseResult.data.info;
|
|
339
349
|
if (!info) continue;
|
|
350
|
+
const totalUsageKey = tokenUsageKey(info.total_token_usage);
|
|
351
|
+
if (seenTotalUsageSnapshots.has(totalUsageKey)) continue;
|
|
352
|
+
seenTotalUsageSnapshots.add(totalUsageKey);
|
|
340
353
|
const last = info.last_token_usage;
|
|
341
354
|
tokenEvents.push({
|
|
342
355
|
timestamp,
|