@zhangferry-dev/tokendash 1.3.0 → 1.4.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 +31 -3
- package/bin/tokendash.js +5 -1
- package/dist/client/assets/{index-D-RErhSy.js → index-B4YgU_cb.js} +42 -42
- package/dist/client/assets/index-iYDpTV63.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/client/popover.html +1105 -0
- package/dist/electron-server.cjs +2162 -0
- package/dist/electron-server.cjs.map +7 -0
- package/dist/server/index.d.ts +4 -1
- package/dist/server/index.js +59 -22
- package/electron/main.cjs +450 -0
- package/electron/main.js +291 -0
- package/electron/preload.cjs +22 -0
- package/electron/trayBadge.cjs +25 -0
- package/electron/trayBadge.js +30 -0
- package/electron/trayHelper +0 -0
- package/electron/trayHelper.swift +130 -0
- package/electron-builder.yml +17 -0
- package/package.json +13 -4
- package/resources/cache_diagram.html +456 -0
- package/resources/cache_diagram.png +0 -0
- package/resources/entitlements.mac.plist +10 -0
- package/resources/icon.png +0 -0
- package/resources/pr1_preview.png +0 -0
- package/resources/product_screenshoot.png +0 -0
- package/resources/test_single_agent.png +0 -0
- package/dist/client/assets/index-x7K7fQX4.css +0 -1
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
const { app, BrowserWindow, ipcMain, screen, shell } = require('electron');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const http = require('node:http');
|
|
5
|
+
const https = require('node:https');
|
|
6
|
+
const { spawn } = require('node:child_process');
|
|
7
|
+
|
|
8
|
+
// Global debug logger (writes to file since stdout is lost in packaged apps)
|
|
9
|
+
const DEBUG_LOG = '/tmp/tokendash-debug.log';
|
|
10
|
+
try { fs.writeFileSync(DEBUG_LOG, 'main.js loaded\n'); } catch(_){}
|
|
11
|
+
|
|
12
|
+
// Import from bundled server (created by esbuild)
|
|
13
|
+
let createApp;
|
|
14
|
+
try {
|
|
15
|
+
createApp = require('../dist/electron-server.cjs').createApp;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.error('Failed to load bundled server. Did you run the build?', e.message);
|
|
18
|
+
app.quit();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { formatTokens } = require('./trayBadge.cjs');
|
|
22
|
+
|
|
23
|
+
// Resolve trayHelper binary: extract from asar if needed
|
|
24
|
+
function resolveTrayHelperPath() {
|
|
25
|
+
const srcPath = path.join(__dirname, 'trayHelper');
|
|
26
|
+
const isAsar = srcPath.includes('.asar');
|
|
27
|
+
const debugLog = (msg) => {
|
|
28
|
+
const logPath = '/tmp/tokendash-debug.log';
|
|
29
|
+
fs.appendFileSync(logPath, msg + '\n');
|
|
30
|
+
};
|
|
31
|
+
debugLog('[trayHelper] __dirname: ' + __dirname);
|
|
32
|
+
debugLog('[trayHelper] srcPath: ' + srcPath + ' isAsar: ' + isAsar);
|
|
33
|
+
if (isAsar) {
|
|
34
|
+
const destDir = path.join(app.getPath('userData'), 'helpers');
|
|
35
|
+
const destPath = path.join(destDir, 'trayHelper');
|
|
36
|
+
debugLog('[trayHelper] extracting to: ' + destPath);
|
|
37
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
38
|
+
fs.copyFileSync(srcPath, destPath);
|
|
39
|
+
fs.chmodSync(destPath, 0o755);
|
|
40
|
+
debugLog('[trayHelper] extracted OK');
|
|
41
|
+
return destPath;
|
|
42
|
+
}
|
|
43
|
+
return srcPath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// State
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
let popover = null;
|
|
51
|
+
let server = null;
|
|
52
|
+
let trayProcess = null;
|
|
53
|
+
let selectedAgents = null; // null = use all available agents
|
|
54
|
+
const serverPort = parseInt(process.env.TOKENDASH_PORT || '3456', 10);
|
|
55
|
+
const POPOVER_WIDTH = 380;
|
|
56
|
+
const POPOVER_HEIGHT = 540;
|
|
57
|
+
const PACKAGE_NAME = '@zhangferry-dev/tokendash';
|
|
58
|
+
const GITHUB_REPO = 'zhangferry/tokendash';
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function listenWithFallback(expressApp, port) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
let currentPort = port;
|
|
67
|
+
let attempts = 0;
|
|
68
|
+
|
|
69
|
+
function tryListen() {
|
|
70
|
+
const s = expressApp.listen(currentPort);
|
|
71
|
+
s.once('listening', () => resolve({ server: s, port: currentPort }));
|
|
72
|
+
s.once('error', (err) => {
|
|
73
|
+
if (err.code === 'EADDRINUSE' && attempts < 20) {
|
|
74
|
+
attempts++;
|
|
75
|
+
currentPort++;
|
|
76
|
+
tryListen();
|
|
77
|
+
} else {
|
|
78
|
+
reject(err);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
tryListen();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fetchJson(url) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
http.get(url, (res) => {
|
|
90
|
+
let data = '';
|
|
91
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
92
|
+
res.on('end', () => {
|
|
93
|
+
try { resolve(JSON.parse(data)); }
|
|
94
|
+
catch (e) { reject(e); }
|
|
95
|
+
});
|
|
96
|
+
}).on('error', reject);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function fetchHttpsJson(url) {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const opts = new URL(url);
|
|
103
|
+
const reqOpts = {
|
|
104
|
+
hostname: opts.hostname,
|
|
105
|
+
path: opts.pathname + opts.search,
|
|
106
|
+
method: 'GET',
|
|
107
|
+
headers: { 'User-Agent': 'TokenDash' },
|
|
108
|
+
};
|
|
109
|
+
https.get(reqOpts, (res) => {
|
|
110
|
+
let data = '';
|
|
111
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
112
|
+
res.on('end', () => {
|
|
113
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
114
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
try { resolve(JSON.parse(data)); }
|
|
118
|
+
catch (e) { reject(e); }
|
|
119
|
+
});
|
|
120
|
+
}).on('error', reject);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function compareVersions(a, b) {
|
|
125
|
+
const aParts = String(a).split('.').map((part) => parseInt(part, 10) || 0);
|
|
126
|
+
const bParts = String(b).split('.').map((part) => parseInt(part, 10) || 0);
|
|
127
|
+
const maxLen = Math.max(aParts.length, bParts.length);
|
|
128
|
+
for (let i = 0; i < maxLen; i++) {
|
|
129
|
+
const delta = (aParts[i] || 0) - (bParts[i] || 0);
|
|
130
|
+
if (delta !== 0) return delta;
|
|
131
|
+
}
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getAppInfo() {
|
|
136
|
+
// app.getVersion() returns Electron's version in dev mode (e.g. 41.5).
|
|
137
|
+
// Always read from package.json to get the app's own version.
|
|
138
|
+
let version = app.getVersion();
|
|
139
|
+
try {
|
|
140
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
141
|
+
if (pkg.version) version = pkg.version;
|
|
142
|
+
} catch (_) {}
|
|
143
|
+
return {
|
|
144
|
+
version,
|
|
145
|
+
launchAtLogin: app.getLoginItemSettings().openAtLogin,
|
|
146
|
+
platform: process.platform,
|
|
147
|
+
packageName: PACKAGE_NAME,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function positionPopoverAtClick(clickScreenX) {
|
|
152
|
+
if (!popover) return;
|
|
153
|
+
|
|
154
|
+
// Find which display the click is on
|
|
155
|
+
const allDisplays = screen.getAllDisplays();
|
|
156
|
+
const clickDisplay = allDisplays.find(d => {
|
|
157
|
+
const bounds = d.bounds;
|
|
158
|
+
return clickScreenX >= bounds.x && clickScreenX < bounds.x + bounds.width;
|
|
159
|
+
}) || screen.getPrimaryDisplay();
|
|
160
|
+
|
|
161
|
+
const { x: screenX, y: screenY, width: screenW, height: screenH } = clickDisplay.workArea;
|
|
162
|
+
const popoverWidth = POPOVER_WIDTH;
|
|
163
|
+
const popoverHeight = POPOVER_HEIGHT;
|
|
164
|
+
|
|
165
|
+
// Center horizontally on click position
|
|
166
|
+
let x = clickScreenX - popoverWidth / 2;
|
|
167
|
+
// Below menu bar, close to the icon
|
|
168
|
+
let y = screenY + 6;
|
|
169
|
+
|
|
170
|
+
// Clamp to screen bounds
|
|
171
|
+
if (x < screenX + 8) x = screenX + 8;
|
|
172
|
+
if (x + popoverWidth > screenX + screenW - 8) x = screenX + screenW - popoverWidth - 8;
|
|
173
|
+
if (y + popoverHeight > screenY + screenH - 8) y = screenY + screenH - popoverHeight - 8;
|
|
174
|
+
|
|
175
|
+
popover.setPosition(Math.round(x), Math.round(y), false);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function togglePopover(clickScreenX) {
|
|
179
|
+
if (!popover) return;
|
|
180
|
+
|
|
181
|
+
if (popover.isVisible()) {
|
|
182
|
+
popover.hide();
|
|
183
|
+
} else {
|
|
184
|
+
positionPopoverAtClick(clickScreenX || 0);
|
|
185
|
+
popover.show();
|
|
186
|
+
popover.focus();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Native tray helper (Swift binary for macOS 26+ compatibility)
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
function startTrayHelper() {
|
|
195
|
+
const helperPath = resolveTrayHelperPath();
|
|
196
|
+
trayProcess = spawn(helperPath, [], {
|
|
197
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
let buffer = '';
|
|
201
|
+
|
|
202
|
+
trayProcess.stdout.on('data', (data) => {
|
|
203
|
+
buffer += data.toString();
|
|
204
|
+
const lines = buffer.split('\n');
|
|
205
|
+
buffer = lines.pop(); // keep incomplete line in buffer
|
|
206
|
+
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
const event = line.trim();
|
|
209
|
+
if (event.startsWith('click:')) {
|
|
210
|
+
// Format: click:x,y (screen coordinates in macOS points)
|
|
211
|
+
const parts = event.split(':')[1];
|
|
212
|
+
const clickX = parseInt(parts.split(',')[0], 10) || 0;
|
|
213
|
+
// Convert macOS screen coords (origin bottom-left) to top-left for Electron
|
|
214
|
+
const primaryDisplay = screen.getPrimaryDisplay();
|
|
215
|
+
const screenH = primaryDisplay.size.height;
|
|
216
|
+
togglePopover(clickX);
|
|
217
|
+
} else if (event === 'ready') {
|
|
218
|
+
// Helper is ready, start badge updates
|
|
219
|
+
startBadgeUpdates();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
trayProcess.on('close', (code) => {
|
|
225
|
+
console.log('Tray helper exited with code', code);
|
|
226
|
+
trayProcess = null;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
trayProcess.on('error', (err) => {
|
|
230
|
+
console.error('Failed to start tray helper:', err.message);
|
|
231
|
+
trayProcess = null;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function sendTrayCommand(command) {
|
|
236
|
+
if (trayProcess && trayProcess.stdin && !trayProcess.stdin.destroyed) {
|
|
237
|
+
trayProcess.stdin.write(command + '\n');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function stopTrayHelper() {
|
|
242
|
+
if (trayProcess) {
|
|
243
|
+
sendTrayCommand('quit');
|
|
244
|
+
trayProcess = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Tray badge updater
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
let updateTimer = null;
|
|
253
|
+
|
|
254
|
+
function updateTrayBadge() {
|
|
255
|
+
const d = new Date(); const today = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2,"0") + "-" + String(d.getDate()).padStart(2,"0");
|
|
256
|
+
|
|
257
|
+
// Fetch agents list, then fetch daily data for each agent in parallel
|
|
258
|
+
fetchJson(`http://localhost:${serverPort}/api/agents`)
|
|
259
|
+
.then((agentData) => {
|
|
260
|
+
let agents = (agentData && agentData.available) ? agentData.available : ['claude'];
|
|
261
|
+
// Apply agent filter from popover settings
|
|
262
|
+
if (selectedAgents && selectedAgents.length > 0) {
|
|
263
|
+
agents = agents.filter(a => selectedAgents.includes(a));
|
|
264
|
+
if (agents.length === 0) agents = (agentData && agentData.available) ? agentData.available : ['claude'];
|
|
265
|
+
}
|
|
266
|
+
return Promise.all(
|
|
267
|
+
agents.map(agent =>
|
|
268
|
+
fetchJson(`http://localhost:${serverPort}/api/daily?agent=${agent}`)
|
|
269
|
+
.catch(() => null)
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
})
|
|
273
|
+
.then((results) => {
|
|
274
|
+
let totalTokens = 0;
|
|
275
|
+
let totalCost = 0;
|
|
276
|
+
let totalInput = 0;
|
|
277
|
+
let totalOutput = 0;
|
|
278
|
+
let totalCacheRead = 0;
|
|
279
|
+
|
|
280
|
+
for (const data of results) {
|
|
281
|
+
if (!data || !data.daily) continue;
|
|
282
|
+
const entry = data.daily.find(d => d.date === today);
|
|
283
|
+
if (!entry) continue;
|
|
284
|
+
totalTokens += entry.totalTokens || 0;
|
|
285
|
+
totalCost += entry.totalCost || 0;
|
|
286
|
+
totalInput += entry.inputTokens || 0;
|
|
287
|
+
totalOutput += entry.outputTokens || 0;
|
|
288
|
+
totalCacheRead += entry.cacheReadTokens || 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Show token count on tray icon
|
|
292
|
+
const tokenStr = formatTokens(totalTokens);
|
|
293
|
+
sendTrayCommand('title:' + tokenStr);
|
|
294
|
+
|
|
295
|
+
// Tooltip with breakdown
|
|
296
|
+
const cacheRate = totalTokens > 0 ? ((totalCacheRead / totalTokens) * 100).toFixed(1) : '0.0';
|
|
297
|
+
sendTrayCommand('tooltip:TokenDash - ' + tokenStr + ' tokens today ($' + totalCost.toFixed(2) + ') | cache: ' + cacheRate + '%');
|
|
298
|
+
})
|
|
299
|
+
.catch((err) => {
|
|
300
|
+
if (err.code !== 'ECONNREFUSED') {
|
|
301
|
+
console.error('Tray badge update error:', err.message);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function startBadgeUpdates() {
|
|
307
|
+
updateTrayBadge();
|
|
308
|
+
updateTimer = setInterval(updateTrayBadge, 5000);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function stopBadgeUpdates() {
|
|
312
|
+
if (updateTimer) {
|
|
313
|
+
clearInterval(updateTimer);
|
|
314
|
+
updateTimer = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Create popover window
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
function createPopoverWindow() {
|
|
323
|
+
popover = new BrowserWindow({
|
|
324
|
+
width: POPOVER_WIDTH,
|
|
325
|
+
height: POPOVER_HEIGHT,
|
|
326
|
+
frame: false,
|
|
327
|
+
resizable: false,
|
|
328
|
+
hasShadow: true,
|
|
329
|
+
alwaysOnTop: true,
|
|
330
|
+
skipTaskbar: true,
|
|
331
|
+
show: false,
|
|
332
|
+
fullscreenable: false,
|
|
333
|
+
transparent: false,
|
|
334
|
+
webPreferences: {
|
|
335
|
+
nodeIntegration: false,
|
|
336
|
+
contextIsolation: true,
|
|
337
|
+
preload: path.join(__dirname, 'preload.cjs'),
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
popover.loadURL(`http://localhost:${serverPort}/popover.html`);
|
|
342
|
+
|
|
343
|
+
popover.on('blur', () => {
|
|
344
|
+
popover.hide();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
popover.on('close', (e) => {
|
|
348
|
+
if (!app.isQuitting) {
|
|
349
|
+
e.preventDefault();
|
|
350
|
+
popover.hide();
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function registerIpcHandlers() {
|
|
356
|
+
ipcMain.handle('tokendash:open-dashboard', (_event, url) => {
|
|
357
|
+
const target = typeof url === 'string' && url.length > 0 ? url : `http://localhost:${serverPort}`;
|
|
358
|
+
return shell.openExternal(target);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
ipcMain.handle('tokendash:get-app-info', () => {
|
|
362
|
+
return getAppInfo();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
ipcMain.handle('tokendash:set-launch-at-login', (_event, enabled) => {
|
|
366
|
+
const openAtLogin = Boolean(enabled);
|
|
367
|
+
app.setLoginItemSettings({ openAtLogin });
|
|
368
|
+
return { launchAtLogin: app.getLoginItemSettings().openAtLogin };
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
ipcMain.handle('tokendash:check-for-updates', async () => {
|
|
372
|
+
const currentVersion = getAppInfo().version;
|
|
373
|
+
const releasesUrl = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
const latest = await fetchHttpsJson(releasesUrl);
|
|
377
|
+
// GitHub release tag may be "v1.3.0" or "1.3.0"
|
|
378
|
+
const tag = (latest.tag_name || '').replace(/^v/, '');
|
|
379
|
+
const latestVersion = tag || currentVersion;
|
|
380
|
+
return {
|
|
381
|
+
currentVersion,
|
|
382
|
+
latestVersion,
|
|
383
|
+
upToDate: compareVersions(currentVersion, latestVersion) >= 0,
|
|
384
|
+
releaseUrl: latest.html_url || null,
|
|
385
|
+
};
|
|
386
|
+
} catch (error) {
|
|
387
|
+
return {
|
|
388
|
+
currentVersion,
|
|
389
|
+
latestVersion: currentVersion,
|
|
390
|
+
upToDate: true,
|
|
391
|
+
error: error instanceof Error ? error.message : String(error),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
ipcMain.handle('tokendash:quit', () => {
|
|
397
|
+
app.isQuitting = true;
|
|
398
|
+
stopBadgeUpdates();
|
|
399
|
+
stopTrayHelper();
|
|
400
|
+
if (server) server.close();
|
|
401
|
+
app.quit();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
ipcMain.handle('tokendash:set-selected-agents', (_event, agents) => {
|
|
405
|
+
selectedAgents = Array.isArray(agents) ? agents : null;
|
|
406
|
+
// Immediately refresh badge with new filter
|
|
407
|
+
updateTrayBadge();
|
|
408
|
+
return { ok: true };
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
// App lifecycle
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
app.whenReady().then(async () => {
|
|
417
|
+
registerIpcHandlers();
|
|
418
|
+
|
|
419
|
+
app.on('before-quit', () => {
|
|
420
|
+
app.isQuitting = true;
|
|
421
|
+
stopBadgeUpdates();
|
|
422
|
+
stopTrayHelper();
|
|
423
|
+
if (server) server.close();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Create and bind Express server
|
|
427
|
+
// Pass dist/ directory so createApp resolves client assets correctly
|
|
428
|
+
const distDir = path.join(__dirname, '..', 'dist');
|
|
429
|
+
const expressApp = createApp(serverPort, distDir);
|
|
430
|
+
try {
|
|
431
|
+
const result = await listenWithFallback(expressApp, serverPort);
|
|
432
|
+
server = result.server;
|
|
433
|
+
console.log(`tokendash running on http://localhost:${result.port}`);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
console.error('Failed to start server:', err);
|
|
436
|
+
app.quit();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Start native tray helper
|
|
441
|
+
startTrayHelper();
|
|
442
|
+
|
|
443
|
+
// Create popover
|
|
444
|
+
createPopoverWindow();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
process.on('uncaughtException', (err) => {
|
|
448
|
+
console.error('Fatal error in Electron main:', err);
|
|
449
|
+
app.quit();
|
|
450
|
+
});
|