@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 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
- ![Product Screenshoot](resources/product_screenshoot.png)
7
+ ![Web Dashboard](resources/product_screenshoot.png)
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 `~/.claude/projects/` JSONL files directly — no `ccusage` CLI dependency, 100x faster data loading.
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 (Claude Code & OpenClaw only).
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
- - **macOS Menu Bar App:** Native tray icon with real-time token usage, popover dashboard, and agent filtering — runs silently in the background.
22
- - **Test Coverage:** Unit tests (Vitest) and E2E tests (Playwright) for reliability.
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
+ ![macOS Menu Bar](resources/product_menu.png)
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)
@@ -177,9 +177,10 @@
177
177
  }
178
178
 
179
179
  .y-axis {
180
- display: grid;
181
- grid-template-rows: repeat(4, 1fr);
182
- justify-items: end;
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) + 10px);
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: rgba(255, 255, 255, 0.9);
265
- border: 1px solid var(--border);
266
- border-radius: 999px;
267
- padding: 4px 7px;
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, maxValue, maxEntry);
828
+ renderBars(selected, axisInfo.top, maxEntry);
817
829
  }
818
830
 
819
- function renderAxis(maxValue) {
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 top = maxValue;
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(midHigh) + '</span>',
827
- '<span>' + formatNumber(midLow) + '</span>',
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) {
@@ -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,