@zhangferry-dev/tokendash 1.4.0 → 1.5.0
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/assets/{index-B4YgU_cb.js → index-BPWY9q0y.js} +45 -45
- package/dist/client/index.html +1 -1
- package/dist/client/popover.html +44 -17
- package/dist/electron-server.cjs +180 -53
- package/dist/electron-server.cjs.map +3 -3
- package/dist/server/analyticsParser.js +66 -2
- package/dist/server/claudeBlocksParser.js +48 -5
- package/dist/server/claudeJsonlParser.d.ts +6 -0
- package/dist/server/claudeJsonlParser.js +53 -4
- package/dist/server/codexParser.d.ts +10 -7
- package/dist/server/codexParser.js +83 -57
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +25 -9
- 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/electron/main.cjs
CHANGED
|
@@ -51,7 +51,7 @@ let popover = null;
|
|
|
51
51
|
let server = null;
|
|
52
52
|
let trayProcess = null;
|
|
53
53
|
let selectedAgents = null; // null = use all available agents
|
|
54
|
-
|
|
54
|
+
let serverPort = parseInt(process.env.TOKENDASH_PORT || '3456', 10);
|
|
55
55
|
const POPOVER_WIDTH = 380;
|
|
56
56
|
const POPOVER_HEIGHT = 540;
|
|
57
57
|
const PACKAGE_NAME = '@zhangferry-dev/tokendash';
|
|
@@ -250,6 +250,27 @@ function stopTrayHelper() {
|
|
|
250
250
|
// ---------------------------------------------------------------------------
|
|
251
251
|
|
|
252
252
|
let updateTimer = null;
|
|
253
|
+
let lastTraySnapshot = null;
|
|
254
|
+
|
|
255
|
+
function getTrayAgentKey(agents) {
|
|
256
|
+
return agents.slice().sort().join(',');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function applyTraySnapshot(snapshot) {
|
|
260
|
+
const totalTokens = Number(snapshot && snapshot.totalTokens) || 0;
|
|
261
|
+
const totalCost = Number(snapshot && snapshot.totalCost) || 0;
|
|
262
|
+
const totalCacheRead = Number(snapshot && snapshot.totalCacheRead) || 0;
|
|
263
|
+
const today = snapshot && snapshot.today;
|
|
264
|
+
const agentKey = snapshot && snapshot.agentKey;
|
|
265
|
+
|
|
266
|
+
lastTraySnapshot = { today, agentKey, totalTokens, totalCost, totalCacheRead };
|
|
267
|
+
|
|
268
|
+
const tokenStr = formatTokens(totalTokens);
|
|
269
|
+
sendTrayCommand('title:' + tokenStr);
|
|
270
|
+
|
|
271
|
+
const cacheRate = totalTokens > 0 ? ((totalCacheRead / totalTokens) * 100).toFixed(1) : '0.0';
|
|
272
|
+
sendTrayCommand('tooltip:TokenDash - ' + tokenStr + ' tokens today ($' + totalCost.toFixed(2) + ') | cache: ' + cacheRate + '%');
|
|
273
|
+
}
|
|
253
274
|
|
|
254
275
|
function updateTrayBadge() {
|
|
255
276
|
const d = new Date(); const today = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2,"0") + "-" + String(d.getDate()).padStart(2,"0");
|
|
@@ -257,20 +278,35 @@ function updateTrayBadge() {
|
|
|
257
278
|
// Fetch agents list, then fetch daily data for each agent in parallel
|
|
258
279
|
fetchJson(`http://localhost:${serverPort}/api/agents`)
|
|
259
280
|
.then((agentData) => {
|
|
260
|
-
let agents = (agentData && agentData.available) ? agentData.available : ['claude'];
|
|
281
|
+
let agents = (agentData && Array.isArray(agentData.available)) ? agentData.available : ['claude'];
|
|
282
|
+
if (agents.length === 0) {
|
|
283
|
+
// Transient agent detection failures should not clear a previously valid tray badge.
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
261
287
|
// Apply agent filter from popover settings
|
|
262
288
|
if (selectedAgents && selectedAgents.length > 0) {
|
|
263
|
-
|
|
264
|
-
if (
|
|
289
|
+
const filtered = agents.filter(a => selectedAgents.includes(a));
|
|
290
|
+
if (filtered.length > 0) agents = filtered;
|
|
265
291
|
}
|
|
292
|
+
|
|
293
|
+
const agentKey = getTrayAgentKey(agents);
|
|
266
294
|
return Promise.all(
|
|
267
295
|
agents.map(agent =>
|
|
268
296
|
fetchJson(`http://localhost:${serverPort}/api/daily?agent=${agent}`)
|
|
269
297
|
.catch(() => null)
|
|
270
298
|
)
|
|
271
|
-
);
|
|
299
|
+
).then(results => ({ agentKey, results }));
|
|
272
300
|
})
|
|
273
|
-
.then((
|
|
301
|
+
.then((payload) => {
|
|
302
|
+
if (!payload) return;
|
|
303
|
+
const { agentKey, results } = payload;
|
|
304
|
+
const successfulResults = results.filter(data => data && data.daily);
|
|
305
|
+
if (successfulResults.length === 0) {
|
|
306
|
+
// Keep the last good value when every daily request failed.
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
274
310
|
let totalTokens = 0;
|
|
275
311
|
let totalCost = 0;
|
|
276
312
|
let totalInput = 0;
|
|
@@ -288,13 +324,20 @@ function updateTrayBadge() {
|
|
|
288
324
|
totalCacheRead += entry.cacheReadTokens || 0;
|
|
289
325
|
}
|
|
290
326
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
327
|
+
const shouldPreserveLastPositive =
|
|
328
|
+
totalTokens === 0 &&
|
|
329
|
+
lastTraySnapshot &&
|
|
330
|
+
lastTraySnapshot.today === today &&
|
|
331
|
+
lastTraySnapshot.agentKey === agentKey &&
|
|
332
|
+
lastTraySnapshot.totalTokens > 0;
|
|
333
|
+
|
|
334
|
+
if (shouldPreserveLastPositive) {
|
|
335
|
+
// Daily usage should not drop to zero during the same day for the same agent filter.
|
|
336
|
+
// Treat a zero refresh after a positive value as transient empty data and keep the badge stable.
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
294
339
|
|
|
295
|
-
|
|
296
|
-
const cacheRate = totalTokens > 0 ? ((totalCacheRead / totalTokens) * 100).toFixed(1) : '0.0';
|
|
297
|
-
sendTrayCommand('tooltip:TokenDash - ' + tokenStr + ' tokens today ($' + totalCost.toFixed(2) + ') | cache: ' + cacheRate + '%');
|
|
340
|
+
applyTraySnapshot({ today, agentKey, totalTokens, totalCost, totalCacheRead });
|
|
298
341
|
})
|
|
299
342
|
.catch((err) => {
|
|
300
343
|
if (err.code !== 'ECONNREFUSED') {
|
|
@@ -403,10 +446,17 @@ function registerIpcHandlers() {
|
|
|
403
446
|
|
|
404
447
|
ipcMain.handle('tokendash:set-selected-agents', (_event, agents) => {
|
|
405
448
|
selectedAgents = Array.isArray(agents) ? agents : null;
|
|
449
|
+
lastTraySnapshot = null;
|
|
406
450
|
// Immediately refresh badge with new filter
|
|
407
451
|
updateTrayBadge();
|
|
408
452
|
return { ok: true };
|
|
409
453
|
});
|
|
454
|
+
|
|
455
|
+
ipcMain.handle('tokendash:update-tray-snapshot', (_event, snapshot) => {
|
|
456
|
+
if (!snapshot || typeof snapshot !== 'object') return { ok: false };
|
|
457
|
+
applyTraySnapshot(snapshot);
|
|
458
|
+
return { ok: true };
|
|
459
|
+
});
|
|
410
460
|
}
|
|
411
461
|
|
|
412
462
|
// ---------------------------------------------------------------------------
|
|
@@ -414,6 +464,10 @@ function registerIpcHandlers() {
|
|
|
414
464
|
// ---------------------------------------------------------------------------
|
|
415
465
|
|
|
416
466
|
app.whenReady().then(async () => {
|
|
467
|
+
if (process.platform === 'darwin' && app.dock) {
|
|
468
|
+
app.dock.hide();
|
|
469
|
+
}
|
|
470
|
+
|
|
417
471
|
registerIpcHandlers();
|
|
418
472
|
|
|
419
473
|
app.on('before-quit', () => {
|
|
@@ -430,6 +484,7 @@ app.whenReady().then(async () => {
|
|
|
430
484
|
try {
|
|
431
485
|
const result = await listenWithFallback(expressApp, serverPort);
|
|
432
486
|
server = result.server;
|
|
487
|
+
serverPort = result.port;
|
|
433
488
|
console.log(`tokendash running on http://localhost:${result.port}`);
|
|
434
489
|
} catch (err) {
|
|
435
490
|
console.error('Failed to start server:', err);
|
package/electron/preload.cjs
CHANGED
|
@@ -19,4 +19,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|
|
19
19
|
setSelectedAgents(agents) {
|
|
20
20
|
return ipcRenderer.invoke('tokendash:set-selected-agents', agents);
|
|
21
21
|
},
|
|
22
|
+
updateTraySnapshot(snapshot) {
|
|
23
|
+
return ipcRenderer.invoke('tokendash:update-tray-snapshot', snapshot);
|
|
24
|
+
},
|
|
22
25
|
});
|
package/electron/trayBadge.cjs
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* Examples: 1234 -> "1.2K", 567890 -> "567.9K", 1500000 -> "1.5M"
|
|
6
6
|
*/
|
|
7
7
|
function formatTokens(tokens) {
|
|
8
|
+
tokens = Number(tokens) || 0;
|
|
9
|
+
|
|
8
10
|
if (tokens >= 1e6) return (tokens / 1e6).toFixed(1) + 'M';
|
|
9
11
|
if (tokens >= 1e3) return (tokens / 1e3).toFixed(1) + 'K';
|
|
10
12
|
if (tokens > 0) return String(tokens);
|
|
11
|
-
return '0
|
|
13
|
+
return '0';
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/**
|
package/electron/trayHelper
CHANGED
|
Binary file
|
|
@@ -9,18 +9,17 @@ import Cocoa
|
|
|
9
9
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
|
10
10
|
var statusItem: NSStatusItem!
|
|
11
11
|
var readHandle: FileHandle?
|
|
12
|
+
var currentTitle = "0"
|
|
12
13
|
|
|
13
14
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
14
15
|
// Create status bar item
|
|
15
16
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
statusItem.button?.image = icon
|
|
20
|
-
statusItem.button?.imagePosition = .imageLeft
|
|
18
|
+
statusItem.button?.image = renderCombinedImage(title: currentTitle)
|
|
19
|
+
statusItem.button?.imagePosition = .imageOnly
|
|
21
20
|
statusItem.button?.imageScaling = .scaleProportionallyDown
|
|
22
|
-
|
|
23
|
-
statusItem.button?.title = "
|
|
21
|
+
statusItem.button?.isBordered = false
|
|
22
|
+
statusItem.button?.title = ""
|
|
24
23
|
statusItem.button?.toolTip = "TokenDash"
|
|
25
24
|
|
|
26
25
|
// Set up click actions — both left and right click
|
|
@@ -42,9 +41,39 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
42
41
|
sendEvent("ready")
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
/// Render icon + title text into a single template image for the status bar.
|
|
45
|
+
func renderCombinedImage(title: String) -> NSImage {
|
|
46
|
+
let iconW: CGFloat = 18
|
|
47
|
+
let iconH: CGFloat = 18
|
|
48
|
+
let fontSize: CGFloat = 13
|
|
49
|
+
let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .medium)
|
|
50
|
+
let textAttrs: [NSAttributedString.Key: Any] = [.font: font]
|
|
51
|
+
let textWidth = (title as NSString).size(withAttributes: textAttrs).width
|
|
52
|
+
let padding: CGFloat = 4 // gap between icon and text
|
|
53
|
+
|
|
54
|
+
let totalWidth = iconW + padding + textWidth
|
|
55
|
+
// Status bar height is ~22pt; center vertically
|
|
56
|
+
let totalHeight: CGFloat = 20
|
|
57
|
+
|
|
58
|
+
let image = NSImage(size: NSSize(width: totalWidth, height: totalHeight))
|
|
59
|
+
image.lockFocus()
|
|
60
|
+
|
|
61
|
+
// Draw icon centered vertically
|
|
62
|
+
let icon = createTemplateIcon(size: NSSize(width: iconW, height: iconH))
|
|
63
|
+
let iconY = (totalHeight - iconH) / 2.0
|
|
64
|
+
icon.draw(in: NSRect(x: 0, y: iconY, width: iconW, height: iconH))
|
|
65
|
+
|
|
66
|
+
// Draw text centered vertically (baseline-adjusted)
|
|
67
|
+
let textY = (totalHeight - fontSize) / 2.0 - 1
|
|
68
|
+
(title as NSString).draw(at: NSPoint(x: iconW + padding, y: textY), withAttributes: textAttrs)
|
|
69
|
+
|
|
70
|
+
image.unlockFocus()
|
|
71
|
+
image.isTemplate = true
|
|
72
|
+
return image
|
|
73
|
+
}
|
|
74
|
+
|
|
45
75
|
@objc func handleClick(_ sender: Any?) {
|
|
46
76
|
guard let _ = NSApp.currentEvent else { return }
|
|
47
|
-
// Send click with screen coordinates so Electron can position the popover
|
|
48
77
|
let loc = NSEvent.mouseLocation
|
|
49
78
|
sendEvent("click:\(Int(loc.x)),\(Int(loc.y))")
|
|
50
79
|
}
|
|
@@ -60,7 +89,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
60
89
|
let cmd = command.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
61
90
|
if cmd.hasPrefix("title:") {
|
|
62
91
|
let title = String(cmd.dropFirst(6))
|
|
63
|
-
|
|
92
|
+
currentTitle = title
|
|
93
|
+
statusItem.button?.image = renderCombinedImage(title: title)
|
|
64
94
|
} else if cmd.hasPrefix("tooltip:") {
|
|
65
95
|
let tooltip = String(cmd.dropFirst(8))
|
|
66
96
|
statusItem.button?.toolTip = tooltip
|
|
@@ -83,32 +113,24 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
83
113
|
let image = NSImage(size: size)
|
|
84
114
|
image.lockFocus()
|
|
85
115
|
|
|
86
|
-
// Draw the TokenDash wavy line icon (from SVG, 64x64 viewBox scaled to size)
|
|
87
|
-
// SVG coords use top-left origin; AppKit uses bottom-left, so flip Y: y = (64 - svgY)
|
|
88
116
|
let sx = size.width / 64.0
|
|
89
117
|
let sy = size.height / 64.0
|
|
90
118
|
|
|
91
119
|
let path = NSBezierPath()
|
|
92
|
-
// M6,32 H18
|
|
93
120
|
path.move(to: NSPoint(x: 6 * sx, y: (64 - 32) * sy))
|
|
94
121
|
path.line(to: NSPoint(x: 18 * sx, y: (64 - 32) * sy))
|
|
95
|
-
// C21,32 22.5,34 24.5,39
|
|
96
122
|
path.curve(to: NSPoint(x: 24.5 * sx, y: (64 - 39) * sy),
|
|
97
123
|
controlPoint1: NSPoint(x: 21 * sx, y: (64 - 32) * sy),
|
|
98
124
|
controlPoint2: NSPoint(x: 22.5 * sx, y: (64 - 34) * sy))
|
|
99
|
-
// C27,45.5 30,50 34,50
|
|
100
125
|
path.curve(to: NSPoint(x: 34 * sx, y: (64 - 50) * sy),
|
|
101
126
|
controlPoint1: NSPoint(x: 27 * sx, y: (64 - 45.5) * sy),
|
|
102
127
|
controlPoint2: NSPoint(x: 30 * sx, y: (64 - 50) * sy))
|
|
103
|
-
// C38,50 40.5,42 44,22
|
|
104
128
|
path.curve(to: NSPoint(x: 44 * sx, y: (64 - 22) * sy),
|
|
105
129
|
controlPoint1: NSPoint(x: 38 * sx, y: (64 - 50) * sy),
|
|
106
130
|
controlPoint2: NSPoint(x: 40.5 * sx, y: (64 - 42) * sy))
|
|
107
|
-
// C46,11 49,8 52,8
|
|
108
131
|
path.curve(to: NSPoint(x: 52 * sx, y: (64 - 8) * sy),
|
|
109
132
|
controlPoint1: NSPoint(x: 46 * sx, y: (64 - 11) * sy),
|
|
110
133
|
controlPoint2: NSPoint(x: 49 * sx, y: (64 - 8) * sy))
|
|
111
|
-
// C55,8 57.5,13 60,22
|
|
112
134
|
path.curve(to: NSPoint(x: 60 * sx, y: (64 - 22) * sy),
|
|
113
135
|
controlPoint1: NSPoint(x: 55 * sx, y: (64 - 8) * sy),
|
|
114
136
|
controlPoint2: NSPoint(x: 57.5 * sx, y: (64 - 13) * sy))
|
package/electron-builder.yml
CHANGED
|
@@ -10,8 +10,11 @@ files:
|
|
|
10
10
|
mac:
|
|
11
11
|
target: dmg
|
|
12
12
|
category: public.app-category.developer-tools
|
|
13
|
-
icon: resources/icon.
|
|
13
|
+
icon: resources/icon.icns
|
|
14
14
|
identity: null
|
|
15
15
|
darkModeSupport: true
|
|
16
|
+
minimumSystemVersion: '14.0'
|
|
17
|
+
extendInfo:
|
|
18
|
+
LSUIElement: true
|
|
16
19
|
extraMetadata:
|
|
17
20
|
main: electron/main.cjs
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|