vibe-monitor 1.0.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 +281 -0
- package/assets/generate-icons.js +86 -0
- package/assets/icon-128.png +0 -0
- package/assets/icon-16.png +0 -0
- package/assets/icon-256.png +0 -0
- package/assets/icon-32.png +0 -0
- package/assets/icon-64.png +0 -0
- package/assets/icon-generator.html +221 -0
- package/assets/icon.icns +0 -0
- package/assets/icon.ico +0 -0
- package/assets/icon.png +0 -0
- package/bin/cli.js +11 -0
- package/index.html +56 -0
- package/main.js +504 -0
- package/package.json +81 -0
- package/preload.js +9 -0
- package/renderer.js +222 -0
- package/styles.css +66 -0
package/main.js
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, screen } = require('electron');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const { createCanvas } = require('canvas');
|
|
5
|
+
|
|
6
|
+
let mainWindow;
|
|
7
|
+
let tray;
|
|
8
|
+
let isAlwaysOnTop = true;
|
|
9
|
+
let currentState = 'start';
|
|
10
|
+
let currentCharacter = 'clawd'; // 'clawd' or 'kiro'
|
|
11
|
+
let currentProject = '';
|
|
12
|
+
let currentTool = '';
|
|
13
|
+
let currentModel = '';
|
|
14
|
+
let currentMemory = '';
|
|
15
|
+
|
|
16
|
+
// State timeout management
|
|
17
|
+
let stateTimeoutTimer = null;
|
|
18
|
+
const DONE_TO_IDLE_TIMEOUT = 60 * 1000; // 1 minute
|
|
19
|
+
const IDLE_TO_SLEEP_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
20
|
+
|
|
21
|
+
// HTTP server for receiving status updates
|
|
22
|
+
let httpServer;
|
|
23
|
+
const HTTP_PORT = 19280;
|
|
24
|
+
const MAX_PAYLOAD_SIZE = 10 * 1024; // 10KB limit for security
|
|
25
|
+
|
|
26
|
+
// Valid states for validation
|
|
27
|
+
const VALID_STATES = ['start', 'idle', 'thinking', 'working', 'notification', 'done', 'sleep'];
|
|
28
|
+
|
|
29
|
+
// State colors for tray icon (must match shared/config.js states.bgColor)
|
|
30
|
+
// NOTE: Cannot import ESM from CommonJS, keep in sync manually
|
|
31
|
+
const STATE_COLORS = {
|
|
32
|
+
start: '#00CCCC',
|
|
33
|
+
idle: '#00AA00',
|
|
34
|
+
thinking: '#6633CC',
|
|
35
|
+
working: '#0066CC',
|
|
36
|
+
notification: '#FFCC00',
|
|
37
|
+
done: '#00AA00',
|
|
38
|
+
sleep: '#1a1a4e'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Character configurations for tray icon (must match shared/config.js CHARACTER_CONFIG)
|
|
42
|
+
// NOTE: Cannot import ESM from CommonJS, keep in sync manually
|
|
43
|
+
const CHARACTER_CONFIG = {
|
|
44
|
+
clawd: {
|
|
45
|
+
name: 'clawd',
|
|
46
|
+
displayName: 'Clawd',
|
|
47
|
+
color: '#D97757',
|
|
48
|
+
isGhost: false
|
|
49
|
+
},
|
|
50
|
+
kiro: {
|
|
51
|
+
name: 'kiro',
|
|
52
|
+
displayName: 'Kiro',
|
|
53
|
+
color: '#FFFFFF',
|
|
54
|
+
isGhost: true
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const CHARACTER_NAMES = Object.keys(CHARACTER_CONFIG);
|
|
59
|
+
const DEFAULT_CHARACTER = 'clawd';
|
|
60
|
+
const COLOR_EYE = '#000000';
|
|
61
|
+
|
|
62
|
+
// Tray icon cache for performance
|
|
63
|
+
const trayIconCache = new Map();
|
|
64
|
+
|
|
65
|
+
// Create tray icon with state-based background color using canvas for proper PNG encoding
|
|
66
|
+
function createTrayIcon(state, character = 'clawd') {
|
|
67
|
+
const cacheKey = `${state}-${character}`;
|
|
68
|
+
|
|
69
|
+
// Return cached icon if available
|
|
70
|
+
if (trayIconCache.has(cacheKey)) {
|
|
71
|
+
return trayIconCache.get(cacheKey);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const size = 22;
|
|
75
|
+
const canvas = createCanvas(size, size);
|
|
76
|
+
const ctx = canvas.getContext('2d');
|
|
77
|
+
|
|
78
|
+
const bgColor = STATE_COLORS[state] || STATE_COLORS.idle;
|
|
79
|
+
const charConfig = CHARACTER_CONFIG[character] || CHARACTER_CONFIG[DEFAULT_CHARACTER];
|
|
80
|
+
const charColor = charConfig.color;
|
|
81
|
+
const isGhost = charConfig.isGhost;
|
|
82
|
+
|
|
83
|
+
// Helper to draw filled rectangle
|
|
84
|
+
function rect(x, y, w, h, color) {
|
|
85
|
+
ctx.fillStyle = color;
|
|
86
|
+
ctx.fillRect(x, y, w, h);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Clear canvas (transparent)
|
|
90
|
+
ctx.clearRect(0, 0, size, size);
|
|
91
|
+
|
|
92
|
+
// Draw rounded background
|
|
93
|
+
const radius = 4;
|
|
94
|
+
ctx.fillStyle = bgColor;
|
|
95
|
+
ctx.beginPath();
|
|
96
|
+
ctx.roundRect(0, 0, size, size, radius);
|
|
97
|
+
ctx.fill();
|
|
98
|
+
|
|
99
|
+
if (isGhost) {
|
|
100
|
+
// Draw ghost character for kiro
|
|
101
|
+
// Body (rounded top effect)
|
|
102
|
+
rect(6, 4, 10, 2, charColor); // Rounded top
|
|
103
|
+
rect(5, 6, 12, 8, charColor); // Main body
|
|
104
|
+
|
|
105
|
+
// Wavy tail (instead of legs)
|
|
106
|
+
rect(5, 14, 4, 3, charColor); // Left wave
|
|
107
|
+
rect(9, 15, 4, 2, charColor); // Middle wave
|
|
108
|
+
rect(13, 14, 4, 3, charColor); // Right wave
|
|
109
|
+
|
|
110
|
+
// Eyes (2x2 each)
|
|
111
|
+
rect(7, 8, 2, 2, COLOR_EYE); // Left eye
|
|
112
|
+
rect(13, 8, 2, 2, COLOR_EYE); // Right eye
|
|
113
|
+
} else {
|
|
114
|
+
// Draw clawd character (default)
|
|
115
|
+
// Body (centered, 14x8)
|
|
116
|
+
rect(4, 6, 14, 8, charColor);
|
|
117
|
+
|
|
118
|
+
// Arms (2x3 each)
|
|
119
|
+
rect(2, 8, 2, 3, charColor); // Left arm
|
|
120
|
+
rect(18, 8, 2, 3, charColor); // Right arm
|
|
121
|
+
|
|
122
|
+
// Legs (2x4 each, 4 legs)
|
|
123
|
+
rect(5, 14, 2, 4, charColor); // Left outer
|
|
124
|
+
rect(8, 14, 2, 4, charColor); // Left inner
|
|
125
|
+
rect(12, 14, 2, 4, charColor); // Right inner
|
|
126
|
+
rect(15, 14, 2, 4, charColor); // Right outer
|
|
127
|
+
|
|
128
|
+
// Eyes (2x2 each)
|
|
129
|
+
rect(6, 9, 2, 2, COLOR_EYE); // Left eye
|
|
130
|
+
rect(14, 9, 2, 2, COLOR_EYE); // Right eye
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Convert canvas to PNG buffer and create nativeImage
|
|
134
|
+
const pngBuffer = canvas.toBuffer('image/png');
|
|
135
|
+
const icon = nativeImage.createFromBuffer(pngBuffer);
|
|
136
|
+
|
|
137
|
+
// Cache the icon for future use
|
|
138
|
+
trayIconCache.set(cacheKey, icon);
|
|
139
|
+
|
|
140
|
+
return icon;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createWindow() {
|
|
144
|
+
const { workArea } = screen.getPrimaryDisplay();
|
|
145
|
+
|
|
146
|
+
mainWindow = new BrowserWindow({
|
|
147
|
+
width: 172,
|
|
148
|
+
height: 348,
|
|
149
|
+
x: workArea.x + workArea.width - 172,
|
|
150
|
+
y: workArea.y,
|
|
151
|
+
frame: false,
|
|
152
|
+
transparent: true,
|
|
153
|
+
alwaysOnTop: isAlwaysOnTop,
|
|
154
|
+
resizable: false,
|
|
155
|
+
skipTaskbar: false,
|
|
156
|
+
hasShadow: true,
|
|
157
|
+
show: false,
|
|
158
|
+
icon: path.join(__dirname, 'assets', 'icon.png'),
|
|
159
|
+
webPreferences: {
|
|
160
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
161
|
+
contextIsolation: true,
|
|
162
|
+
nodeIntegration: false
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
mainWindow.loadFile('index.html');
|
|
167
|
+
|
|
168
|
+
// Allow window to be dragged
|
|
169
|
+
mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
170
|
+
|
|
171
|
+
// Show window without stealing focus
|
|
172
|
+
mainWindow.once('ready-to-show', () => {
|
|
173
|
+
// Set alwaysOnTop with level for better Windows support
|
|
174
|
+
if (isAlwaysOnTop) {
|
|
175
|
+
mainWindow.setAlwaysOnTop(true, 'screen-saver');
|
|
176
|
+
}
|
|
177
|
+
mainWindow.showInactive();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
mainWindow.on('closed', () => {
|
|
181
|
+
mainWindow = null;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function createTray() {
|
|
186
|
+
const icon = createTrayIcon(currentState, currentCharacter);
|
|
187
|
+
tray = new Tray(icon);
|
|
188
|
+
tray.setToolTip('Vibe Monitor');
|
|
189
|
+
updateTrayMenu();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function updateTrayIcon() {
|
|
193
|
+
if (tray) {
|
|
194
|
+
const icon = createTrayIcon(currentState, currentCharacter);
|
|
195
|
+
tray.setImage(icon);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function updateTrayMenu() {
|
|
200
|
+
const contextMenu = Menu.buildFromTemplate([
|
|
201
|
+
{
|
|
202
|
+
label: `State: ${currentState}`,
|
|
203
|
+
enabled: false
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
label: `Character: ${currentCharacter}`,
|
|
207
|
+
enabled: false
|
|
208
|
+
},
|
|
209
|
+
{ type: 'separator' },
|
|
210
|
+
{
|
|
211
|
+
label: 'Set State',
|
|
212
|
+
submenu: [
|
|
213
|
+
{ label: 'Start', click: () => updateState({ state: 'start' }) },
|
|
214
|
+
{ label: 'Idle', click: () => updateState({ state: 'idle' }) },
|
|
215
|
+
{ label: 'Working', click: () => updateState({ state: 'working' }) },
|
|
216
|
+
{ label: 'Notification', click: () => updateState({ state: 'notification' }) },
|
|
217
|
+
{ label: 'Done', click: () => updateState({ state: 'done' }) },
|
|
218
|
+
{ label: 'Sleep', click: () => updateState({ state: 'sleep' }) }
|
|
219
|
+
]
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
label: 'Set Character',
|
|
223
|
+
submenu: CHARACTER_NAMES.map(name => {
|
|
224
|
+
const char = CHARACTER_CONFIG[name];
|
|
225
|
+
return {
|
|
226
|
+
label: char.displayName,
|
|
227
|
+
type: 'radio',
|
|
228
|
+
checked: currentCharacter === name,
|
|
229
|
+
click: () => updateState({ state: currentState, character: name })
|
|
230
|
+
};
|
|
231
|
+
})
|
|
232
|
+
},
|
|
233
|
+
{ type: 'separator' },
|
|
234
|
+
{
|
|
235
|
+
label: 'Always on Top',
|
|
236
|
+
type: 'checkbox',
|
|
237
|
+
checked: isAlwaysOnTop,
|
|
238
|
+
click: () => {
|
|
239
|
+
isAlwaysOnTop = !isAlwaysOnTop;
|
|
240
|
+
if (mainWindow) {
|
|
241
|
+
mainWindow.setAlwaysOnTop(isAlwaysOnTop, 'screen-saver');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
label: 'Show Window',
|
|
247
|
+
click: () => {
|
|
248
|
+
if (mainWindow) {
|
|
249
|
+
mainWindow.show();
|
|
250
|
+
mainWindow.focus();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
{ type: 'separator' },
|
|
255
|
+
{
|
|
256
|
+
label: `HTTP Server: localhost:${HTTP_PORT}`,
|
|
257
|
+
enabled: false
|
|
258
|
+
},
|
|
259
|
+
{ type: 'separator' },
|
|
260
|
+
{
|
|
261
|
+
label: 'Quit',
|
|
262
|
+
click: () => {
|
|
263
|
+
app.quit();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
tray.setContextMenu(contextMenu);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Clear any existing state timeout timer
|
|
272
|
+
function clearStateTimeout() {
|
|
273
|
+
if (stateTimeoutTimer) {
|
|
274
|
+
clearTimeout(stateTimeoutTimer);
|
|
275
|
+
stateTimeoutTimer = null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Set up state timeout based on current state
|
|
280
|
+
function setupStateTimeout() {
|
|
281
|
+
clearStateTimeout();
|
|
282
|
+
|
|
283
|
+
if (currentState === 'done') {
|
|
284
|
+
// done -> idle after 1 minute
|
|
285
|
+
stateTimeoutTimer = setTimeout(() => {
|
|
286
|
+
updateState({ state: 'idle' });
|
|
287
|
+
}, DONE_TO_IDLE_TIMEOUT);
|
|
288
|
+
} else if (currentState === 'idle' || currentState === 'start') {
|
|
289
|
+
// idle/start -> sleep after 10 minutes
|
|
290
|
+
stateTimeoutTimer = setTimeout(() => {
|
|
291
|
+
updateState({ state: 'sleep' });
|
|
292
|
+
}, IDLE_TO_SLEEP_TIMEOUT);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function updateState(data) {
|
|
297
|
+
// If state is provided (from vibe-monitor.sh), update all fields
|
|
298
|
+
if (data.state !== undefined) {
|
|
299
|
+
currentState = data.state;
|
|
300
|
+
if (data.character !== undefined) {
|
|
301
|
+
currentCharacter = CHARACTER_CONFIG[data.character] ? data.character : DEFAULT_CHARACTER;
|
|
302
|
+
}
|
|
303
|
+
// Clear model and memory when project changes
|
|
304
|
+
if (data.project !== undefined && data.project !== currentProject) {
|
|
305
|
+
currentModel = '';
|
|
306
|
+
currentMemory = '';
|
|
307
|
+
data.model = '';
|
|
308
|
+
data.memory = '';
|
|
309
|
+
}
|
|
310
|
+
if (data.project !== undefined) currentProject = data.project;
|
|
311
|
+
if (data.tool !== undefined) currentTool = data.tool;
|
|
312
|
+
if (data.model !== undefined) currentModel = data.model;
|
|
313
|
+
if (data.memory !== undefined) currentMemory = data.memory;
|
|
314
|
+
|
|
315
|
+
// Set up state timeout for auto-transitions
|
|
316
|
+
setupStateTimeout();
|
|
317
|
+
} else {
|
|
318
|
+
// If no state (from statusline.sh), only update model/memory if project matches
|
|
319
|
+
if (data.project !== undefined && data.project === currentProject) {
|
|
320
|
+
if (data.model !== undefined) currentModel = data.model;
|
|
321
|
+
if (data.memory !== undefined) currentMemory = data.memory;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (mainWindow) {
|
|
325
|
+
mainWindow.webContents.send('state-update', data);
|
|
326
|
+
}
|
|
327
|
+
updateTrayIcon();
|
|
328
|
+
updateTrayMenu();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Show window and position to top-right corner
|
|
332
|
+
function showAndPositionWindow() {
|
|
333
|
+
if (mainWindow) {
|
|
334
|
+
const { workArea } = screen.getPrimaryDisplay();
|
|
335
|
+
const x = workArea.x + workArea.width - 172;
|
|
336
|
+
const y = workArea.y;
|
|
337
|
+
// Use setBounds for better Windows compatibility
|
|
338
|
+
mainWindow.setBounds({ x, y, width: 172, height: 348 });
|
|
339
|
+
mainWindow.showInactive();
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function startHttpServer() {
|
|
346
|
+
httpServer = http.createServer((req, res) => {
|
|
347
|
+
// CORS headers
|
|
348
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
349
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
350
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
351
|
+
|
|
352
|
+
if (req.method === 'OPTIONS') {
|
|
353
|
+
res.writeHead(200);
|
|
354
|
+
res.end();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (req.method === 'POST' && req.url === '/status') {
|
|
359
|
+
const chunks = [];
|
|
360
|
+
let bodySize = 0;
|
|
361
|
+
let aborted = false;
|
|
362
|
+
|
|
363
|
+
req.on('data', chunk => {
|
|
364
|
+
if (aborted) return;
|
|
365
|
+
bodySize += chunk.length;
|
|
366
|
+
if (bodySize > MAX_PAYLOAD_SIZE) {
|
|
367
|
+
aborted = true;
|
|
368
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
369
|
+
res.end(JSON.stringify({ error: 'Payload too large' }));
|
|
370
|
+
req.destroy();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
chunks.push(chunk);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
req.on('end', () => {
|
|
377
|
+
if (aborted) return;
|
|
378
|
+
try {
|
|
379
|
+
const body = Buffer.concat(chunks).toString('utf-8');
|
|
380
|
+
const data = JSON.parse(body);
|
|
381
|
+
|
|
382
|
+
// Validate state if provided
|
|
383
|
+
if (data.state !== undefined && !VALID_STATES.includes(data.state)) {
|
|
384
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
385
|
+
res.end(JSON.stringify({ error: `Invalid state: ${data.state}` }));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Validate character if provided
|
|
390
|
+
if (data.character !== undefined && !CHARACTER_NAMES.includes(data.character)) {
|
|
391
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
392
|
+
res.end(JSON.stringify({ error: `Invalid character: ${data.character}` }));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
updateState(data);
|
|
397
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
398
|
+
res.end(JSON.stringify({ success: true, state: currentState }));
|
|
399
|
+
} catch (e) {
|
|
400
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
401
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
req.on('error', () => {
|
|
406
|
+
if (!aborted) {
|
|
407
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
408
|
+
res.end(JSON.stringify({ error: 'Request error' }));
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
} else if (req.method === 'GET' && req.url === '/status') {
|
|
412
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
413
|
+
res.end(JSON.stringify({
|
|
414
|
+
state: currentState,
|
|
415
|
+
project: currentProject,
|
|
416
|
+
tool: currentTool,
|
|
417
|
+
model: currentModel,
|
|
418
|
+
memory: currentMemory
|
|
419
|
+
}));
|
|
420
|
+
} else if (req.method === 'GET' && req.url === '/health') {
|
|
421
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
422
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
423
|
+
} else if (req.method === 'POST' && req.url === '/show') {
|
|
424
|
+
const shown = showAndPositionWindow();
|
|
425
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
426
|
+
res.end(JSON.stringify({ success: shown }));
|
|
427
|
+
} else if (req.method === 'GET' && req.url === '/debug') {
|
|
428
|
+
const displays = screen.getAllDisplays();
|
|
429
|
+
const primary = screen.getPrimaryDisplay();
|
|
430
|
+
const windowBounds = mainWindow ? mainWindow.getBounds() : null;
|
|
431
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
432
|
+
res.end(JSON.stringify({
|
|
433
|
+
primaryDisplay: {
|
|
434
|
+
bounds: primary.bounds,
|
|
435
|
+
workArea: primary.workArea,
|
|
436
|
+
workAreaSize: primary.workAreaSize,
|
|
437
|
+
scaleFactor: primary.scaleFactor
|
|
438
|
+
},
|
|
439
|
+
allDisplays: displays.map(d => ({
|
|
440
|
+
id: d.id,
|
|
441
|
+
bounds: d.bounds,
|
|
442
|
+
workArea: d.workArea,
|
|
443
|
+
scaleFactor: d.scaleFactor
|
|
444
|
+
})),
|
|
445
|
+
window: windowBounds,
|
|
446
|
+
platform: process.platform
|
|
447
|
+
}, null, 2));
|
|
448
|
+
} else {
|
|
449
|
+
res.writeHead(404);
|
|
450
|
+
res.end('Not Found');
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
httpServer.on('error', (err) => {
|
|
455
|
+
console.error('HTTP Server error:', err.message);
|
|
456
|
+
if (err.code === 'EADDRINUSE') {
|
|
457
|
+
console.error(`Port ${HTTP_PORT} is already in use`);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
httpServer.listen(HTTP_PORT, '127.0.0.1', () => {
|
|
462
|
+
console.log(`Vibe Monitor HTTP server running on http://127.0.0.1:${HTTP_PORT}`);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// IPC handlers
|
|
467
|
+
ipcMain.on('close-window', () => {
|
|
468
|
+
if (mainWindow) {
|
|
469
|
+
mainWindow.hide();
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
ipcMain.on('minimize-window', () => {
|
|
474
|
+
if (mainWindow) {
|
|
475
|
+
mainWindow.minimize();
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
app.whenReady().then(() => {
|
|
480
|
+
createWindow();
|
|
481
|
+
createTray();
|
|
482
|
+
startHttpServer();
|
|
483
|
+
setupStateTimeout(); // Start timeout timer for initial state
|
|
484
|
+
|
|
485
|
+
app.on('activate', () => {
|
|
486
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
487
|
+
createWindow();
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
app.on('window-all-closed', () => {
|
|
493
|
+
// Keep app running in tray on macOS
|
|
494
|
+
if (process.platform !== 'darwin') {
|
|
495
|
+
app.quit();
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
app.on('before-quit', () => {
|
|
500
|
+
clearStateTimeout();
|
|
501
|
+
if (httpServer) {
|
|
502
|
+
httpServer.close();
|
|
503
|
+
}
|
|
504
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibe-monitor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Claude Code Status Monitor - Frameless Desktop App",
|
|
5
|
+
"main": "main.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vibe-monitor": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "electron .",
|
|
11
|
+
"build": "electron-builder",
|
|
12
|
+
"build:mac": "electron-builder --mac",
|
|
13
|
+
"build:dmg": "electron-builder --mac dmg",
|
|
14
|
+
"build:win": "electron-builder --win",
|
|
15
|
+
"build:linux": "electron-builder --linux",
|
|
16
|
+
"build:all": "electron-builder --mac --win --linux"
|
|
17
|
+
},
|
|
18
|
+
"author": "nalbam",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"electron": "^33.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"canvas": "^3.2.1",
|
|
25
|
+
"electron-builder": "^25.0.0"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin/",
|
|
29
|
+
"main.js",
|
|
30
|
+
"preload.js",
|
|
31
|
+
"index.html",
|
|
32
|
+
"renderer.js",
|
|
33
|
+
"styles.css",
|
|
34
|
+
"assets/"
|
|
35
|
+
],
|
|
36
|
+
"build": {
|
|
37
|
+
"appId": "com.nalbam.vibe-monitor",
|
|
38
|
+
"productName": "Vibe Monitor",
|
|
39
|
+
"mac": {
|
|
40
|
+
"category": "public.app-category.developer-tools",
|
|
41
|
+
"icon": "assets/icon.icns",
|
|
42
|
+
"target": [
|
|
43
|
+
"dmg",
|
|
44
|
+
"zip"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"win": {
|
|
48
|
+
"icon": "assets/icon.ico",
|
|
49
|
+
"target": [
|
|
50
|
+
{
|
|
51
|
+
"target": "nsis",
|
|
52
|
+
"arch": [
|
|
53
|
+
"x64",
|
|
54
|
+
"arm64"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"target": "portable",
|
|
59
|
+
"arch": [
|
|
60
|
+
"x64"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
"nsis": {
|
|
66
|
+
"oneClick": false,
|
|
67
|
+
"perMachine": false,
|
|
68
|
+
"allowToChangeInstallationDirectory": true,
|
|
69
|
+
"createDesktopShortcut": true,
|
|
70
|
+
"createStartMenuShortcut": true
|
|
71
|
+
},
|
|
72
|
+
"linux": {
|
|
73
|
+
"icon": "assets/icon.png",
|
|
74
|
+
"target": [
|
|
75
|
+
"AppImage",
|
|
76
|
+
"deb"
|
|
77
|
+
],
|
|
78
|
+
"category": "Development"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
package/preload.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld('electronAPI', {
|
|
4
|
+
closeWindow: () => ipcRenderer.send('close-window'),
|
|
5
|
+
minimizeWindow: () => ipcRenderer.send('minimize-window'),
|
|
6
|
+
onStateUpdate: (callback) => {
|
|
7
|
+
ipcRenderer.on('state-update', (event, data) => callback(data));
|
|
8
|
+
}
|
|
9
|
+
});
|