flying-lobster 1.6.2 → 1.8.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/assets/icon.icns +0 -0
- package/package.json +8 -3
- package/src/main/index.js +499 -20
- package/src/main/preload-session.js +16 -0
- package/src/main/preload-settings.js +14 -0
- package/src/main/preload.js +17 -0
- package/src/main/store.js +39 -0
- package/src/main/updater.js +261 -0
- package/src/renderer/index.html +323 -1
- package/src/renderer/session.html +187 -0
- package/src/renderer/settings.html +190 -47
- package/src/renderer/settings.js +331 -12
package/assets/icon.icns
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flying-lobster",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Always-on-top chat window for OpenClaw gateways 🦞",
|
|
5
5
|
"author": "Rootlab.ai",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,15 +31,20 @@
|
|
|
31
31
|
"assets/**/*"
|
|
32
32
|
],
|
|
33
33
|
"mac": {
|
|
34
|
+
"icon": "assets/icon.icns",
|
|
34
35
|
"category": "public.app-category.productivity",
|
|
35
36
|
"target": [
|
|
36
37
|
{
|
|
37
38
|
"target": "dmg",
|
|
38
|
-
"arch": [
|
|
39
|
+
"arch": [
|
|
40
|
+
"universal"
|
|
41
|
+
]
|
|
39
42
|
},
|
|
40
43
|
{
|
|
41
44
|
"target": "dir",
|
|
42
|
-
"arch": [
|
|
45
|
+
"arch": [
|
|
46
|
+
"universal"
|
|
47
|
+
]
|
|
43
48
|
}
|
|
44
49
|
]
|
|
45
50
|
},
|
package/src/main/index.js
CHANGED
|
@@ -2,6 +2,7 @@ const { app, BrowserWindow, globalShortcut, Tray, Menu, nativeImage, ipcMain, we
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const store = require('./store');
|
|
4
4
|
const { randomUUID } = require('crypto');
|
|
5
|
+
const updater = require('./updater');
|
|
5
6
|
|
|
6
7
|
// CSS to inject into OpenClaw webview to create chat-only view
|
|
7
8
|
const OPENCLAW_CSS = `
|
|
@@ -36,6 +37,8 @@ const OPENCLAW_CSS = `
|
|
|
36
37
|
.chat-compose__actions .btn { padding: 0 10px !important; }
|
|
37
38
|
.chat-group-messages { max-width: 100% !important; }
|
|
38
39
|
.chat-group { margin-right: 4px !important; margin-left: 4px !important; }
|
|
40
|
+
/* Hide tool call/result cards */
|
|
41
|
+
.chat-tool-card { display: none !important; }
|
|
39
42
|
`;
|
|
40
43
|
|
|
41
44
|
const OPENCLAW_JS = `
|
|
@@ -74,6 +77,7 @@ const OPENCLAW_JS = `
|
|
|
74
77
|
|
|
75
78
|
let mainWindow = null;
|
|
76
79
|
let settingsWindow = null;
|
|
80
|
+
let sessionWindows = new Map(); // Map of sessionId -> BrowserWindow
|
|
77
81
|
let tray = null;
|
|
78
82
|
|
|
79
83
|
// ── IPC Handlers ──────────────────────────────────────────────
|
|
@@ -107,9 +111,17 @@ ipcMain.handle('delete-gateway', (_e, id) => {
|
|
|
107
111
|
return gateways;
|
|
108
112
|
});
|
|
109
113
|
|
|
110
|
-
ipcMain.handle('get-active-gateway', () =>
|
|
114
|
+
ipcMain.handle('get-active-gateway', () => {
|
|
115
|
+
const id = store.get('activeGateway');
|
|
116
|
+
console.log('[Main] get-active-gateway:', id);
|
|
117
|
+
return id;
|
|
118
|
+
});
|
|
111
119
|
|
|
112
120
|
ipcMain.handle('set-active-gateway', (_e, id) => {
|
|
121
|
+
console.log('[Main] set-active-gateway:', id);
|
|
122
|
+
const gateways = store.get('gateways');
|
|
123
|
+
const gw = gateways.find(g => g.id === id);
|
|
124
|
+
console.log('[Main] Gateway details:', gw?.name, gw?.url);
|
|
113
125
|
store.set('activeGateway', id);
|
|
114
126
|
return id;
|
|
115
127
|
});
|
|
@@ -148,6 +160,311 @@ ipcMain.handle('get-theme', () => {
|
|
|
148
160
|
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
|
149
161
|
});
|
|
150
162
|
|
|
163
|
+
// ── Session Management ────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
// Extract session name from session key
|
|
166
|
+
function extractSessionName(sessionKey) {
|
|
167
|
+
if (!sessionKey) return 'main';
|
|
168
|
+
|
|
169
|
+
// Handle patterns like "agent:main:main" -> "main"
|
|
170
|
+
const parts = sessionKey.split(':');
|
|
171
|
+
if (parts.length >= 3 && parts[0] === 'agent') {
|
|
172
|
+
return parts[parts.length - 1]; // Return last part
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Handle other patterns, default to last part after ':'
|
|
176
|
+
const lastPart = parts[parts.length - 1];
|
|
177
|
+
return lastPart || 'session';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Fetch sessions from a gateway
|
|
181
|
+
async function fetchGatewaySessions(gatewayUrl) {
|
|
182
|
+
try {
|
|
183
|
+
const ctrl = new AbortController();
|
|
184
|
+
const timeout = setTimeout(() => ctrl.abort(), 5000);
|
|
185
|
+
|
|
186
|
+
const response = await fetch(`${gatewayUrl}/api/sessions`, {
|
|
187
|
+
signal: ctrl.signal,
|
|
188
|
+
headers: { 'Accept': 'application/json' }
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
clearTimeout(timeout);
|
|
192
|
+
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
console.log(`[Sessions] Gateway ${gatewayUrl} sessions API returned ${response.status}`);
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const data = await response.json();
|
|
199
|
+
|
|
200
|
+
// Expect sessions to be an array of strings (session keys)
|
|
201
|
+
if (!Array.isArray(data)) {
|
|
202
|
+
console.log(`[Sessions] Gateway ${gatewayUrl} returned non-array:`, typeof data);
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return data.map(sessionKey => ({
|
|
207
|
+
id: sessionKey,
|
|
208
|
+
name: extractSessionName(sessionKey),
|
|
209
|
+
url: `${gatewayUrl}/chat?session=${sessionKey}`
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.log(`[Sessions] Failed to fetch sessions from ${gatewayUrl}:`, error.message);
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Update sessions for all gateways
|
|
219
|
+
async function refreshAllSessions() {
|
|
220
|
+
const gateways = store.get('gateways');
|
|
221
|
+
const allSessions = {};
|
|
222
|
+
|
|
223
|
+
for (const gateway of gateways) {
|
|
224
|
+
console.log(`[Sessions] Fetching sessions for gateway ${gateway.name}...`);
|
|
225
|
+
const sessions = await fetchGatewaySessions(gateway.url);
|
|
226
|
+
allSessions[gateway.id] = sessions;
|
|
227
|
+
console.log(`[Sessions] Found ${sessions.length} sessions for ${gateway.name}:`, sessions.map(s => s.name));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
store.set('sessions', allSessions);
|
|
231
|
+
return allSessions;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Create a session window
|
|
235
|
+
function createSessionWindow(gatewayId, session) {
|
|
236
|
+
const gateway = store.get('gateways').find(g => g.id === gatewayId);
|
|
237
|
+
if (!gateway) {
|
|
238
|
+
console.error(`[Sessions] Gateway ${gatewayId} not found`);
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check if window already exists
|
|
243
|
+
if (sessionWindows.has(session.id)) {
|
|
244
|
+
const existingWindow = sessionWindows.get(session.id);
|
|
245
|
+
if (!existingWindow.isDestroyed()) {
|
|
246
|
+
existingWindow.show();
|
|
247
|
+
existingWindow.focus();
|
|
248
|
+
return existingWindow;
|
|
249
|
+
} else {
|
|
250
|
+
sessionWindows.delete(session.id);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Get saved bounds or default offset from main window
|
|
255
|
+
const savedBounds = store.get('sessionWindowBounds')[session.id];
|
|
256
|
+
const mainBounds = store.get('windowBounds');
|
|
257
|
+
|
|
258
|
+
const bounds = savedBounds || {
|
|
259
|
+
x: (mainBounds.x || 100) + 50,
|
|
260
|
+
y: (mainBounds.y || 100) + 50,
|
|
261
|
+
width: mainBounds.width || 400,
|
|
262
|
+
height: mainBounds.height || 600
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const sessionWindow = new BrowserWindow({
|
|
266
|
+
width: bounds.width,
|
|
267
|
+
height: bounds.height,
|
|
268
|
+
x: bounds.x,
|
|
269
|
+
y: bounds.y,
|
|
270
|
+
frame: false,
|
|
271
|
+
alwaysOnTop: true,
|
|
272
|
+
skipTaskbar: true,
|
|
273
|
+
resizable: true,
|
|
274
|
+
show: false,
|
|
275
|
+
transparent: false,
|
|
276
|
+
visibleOnAllWorkspaces: true,
|
|
277
|
+
webPreferences: {
|
|
278
|
+
nodeIntegration: false,
|
|
279
|
+
contextIsolation: true,
|
|
280
|
+
preload: path.join(__dirname, 'preload-session.js'),
|
|
281
|
+
webviewTag: true,
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (process.platform === 'darwin') {
|
|
286
|
+
sessionWindow.setAlwaysOnTop(true, 'screen-saver');
|
|
287
|
+
sessionWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
288
|
+
} else {
|
|
289
|
+
sessionWindow.setAlwaysOnTop(true, 'screen-saver');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Load session-specific HTML (we'll create this)
|
|
293
|
+
sessionWindow.loadFile(path.join(__dirname, '..', 'renderer', 'session.html'));
|
|
294
|
+
|
|
295
|
+
// Store window reference
|
|
296
|
+
sessionWindows.set(session.id, sessionWindow);
|
|
297
|
+
|
|
298
|
+
// Track active session windows
|
|
299
|
+
const activeWindows = store.get('activeSessionWindows');
|
|
300
|
+
activeWindows[sessionWindow.id] = { gatewayId, sessionId: session.id };
|
|
301
|
+
store.set('activeSessionWindows', activeWindows);
|
|
302
|
+
|
|
303
|
+
// Inject CSS/JS into webview when attached
|
|
304
|
+
sessionWindow.webContents.on('did-attach-webview', (event, wvWebContents) => {
|
|
305
|
+
console.log(`[Sessions] Session ${session.name} webview attached`);
|
|
306
|
+
|
|
307
|
+
wvWebContents.on('dom-ready', () => {
|
|
308
|
+
// wvWebContents.insertCSS(OPENCLAW_CSS).catch(e => console.error('Session CSS injection failed:', e));
|
|
309
|
+
// wvWebContents.executeJavaScript(OPENCLAW_JS).catch(e => console.error('Session JS injection failed:', e));
|
|
310
|
+
console.log(`[Sessions] CSS/JS injection disabled for session ${session.name}`);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Save bounds on move/resize
|
|
315
|
+
const saveBounds = () => {
|
|
316
|
+
if (!sessionWindow.isDestroyed()) {
|
|
317
|
+
const allBounds = store.get('sessionWindowBounds');
|
|
318
|
+
allBounds[session.id] = sessionWindow.getBounds();
|
|
319
|
+
store.set('sessionWindowBounds', allBounds);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
sessionWindow.on('move', saveBounds);
|
|
323
|
+
sessionWindow.on('resize', saveBounds);
|
|
324
|
+
|
|
325
|
+
// Hide on blur (like main window)
|
|
326
|
+
sessionWindow.on('blur', () => {
|
|
327
|
+
if (sessionWindow.isVisible()) {
|
|
328
|
+
sessionWindow.hide();
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Handle keyboard shortcuts
|
|
333
|
+
sessionWindow.webContents.on('before-input-event', (event, input) => {
|
|
334
|
+
if (input.key === 'Escape') {
|
|
335
|
+
sessionWindow.hide();
|
|
336
|
+
}
|
|
337
|
+
// Cycling shortcuts
|
|
338
|
+
if (input.type === 'keyDown' && input.shift && (input.meta || input.control)) {
|
|
339
|
+
if (input.key === 'ArrowRight') {
|
|
340
|
+
event.preventDefault();
|
|
341
|
+
cycleWindows('next');
|
|
342
|
+
} else if (input.key === 'ArrowLeft') {
|
|
343
|
+
event.preventDefault();
|
|
344
|
+
cycleWindows('prev');
|
|
345
|
+
} else if (input.key === 'k' || input.key === 'K') {
|
|
346
|
+
event.preventDefault();
|
|
347
|
+
toggleTheme();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Clean up on close
|
|
353
|
+
sessionWindow.on('closed', () => {
|
|
354
|
+
sessionWindows.delete(session.id);
|
|
355
|
+
const activeWindows = store.get('activeSessionWindows');
|
|
356
|
+
delete activeWindows[sessionWindow.id];
|
|
357
|
+
store.set('activeSessionWindows', activeWindows);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
sessionWindow.once('ready-to-show', () => {
|
|
361
|
+
sessionWindow.show();
|
|
362
|
+
// Send session info to renderer
|
|
363
|
+
sessionWindow.webContents.send('load-session', {
|
|
364
|
+
gateway: gateway,
|
|
365
|
+
session: session
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return sessionWindow;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// IPC handlers for session management
|
|
373
|
+
ipcMain.handle('get-sessions', async (e, gatewayId) => {
|
|
374
|
+
if (gatewayId) {
|
|
375
|
+
const sessions = store.get('sessions')[gatewayId] || [];
|
|
376
|
+
return sessions;
|
|
377
|
+
}
|
|
378
|
+
return store.get('sessions');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
ipcMain.handle('refresh-sessions', async (e, gatewayId) => {
|
|
382
|
+
if (gatewayId) {
|
|
383
|
+
const gateway = store.get('gateways').find(g => g.id === gatewayId);
|
|
384
|
+
if (gateway) {
|
|
385
|
+
const sessions = await fetchGatewaySessions(gateway.url);
|
|
386
|
+
const allSessions = store.get('sessions');
|
|
387
|
+
allSessions[gatewayId] = sessions;
|
|
388
|
+
store.set('sessions', allSessions);
|
|
389
|
+
return sessions;
|
|
390
|
+
}
|
|
391
|
+
return [];
|
|
392
|
+
} else {
|
|
393
|
+
return await refreshAllSessions();
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
ipcMain.handle('open-session', (e, gatewayId, sessionData) => {
|
|
398
|
+
const window = createSessionWindow(gatewayId, sessionData);
|
|
399
|
+
return window !== null;
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
ipcMain.handle('close-session', (e, sessionId) => {
|
|
403
|
+
const window = sessionWindows.get(sessionId);
|
|
404
|
+
if (window && !window.isDestroyed()) {
|
|
405
|
+
window.close();
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// ── Visibility Management ─────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
ipcMain.handle('get-hidden-gateways', () => store.get('hiddenGateways'));
|
|
414
|
+
|
|
415
|
+
ipcMain.handle('set-gateway-visibility', (e, gatewayId, visible) => {
|
|
416
|
+
const hiddenGateways = store.get('hiddenGateways');
|
|
417
|
+
if (visible) {
|
|
418
|
+
// Remove from hidden list
|
|
419
|
+
const filtered = hiddenGateways.filter(id => id !== gatewayId);
|
|
420
|
+
store.set('hiddenGateways', filtered);
|
|
421
|
+
} else {
|
|
422
|
+
// Add to hidden list
|
|
423
|
+
if (!hiddenGateways.includes(gatewayId)) {
|
|
424
|
+
hiddenGateways.push(gatewayId);
|
|
425
|
+
store.set('hiddenGateways', hiddenGateways);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return store.get('hiddenGateways');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
ipcMain.handle('get-hidden-sessions', (e, gatewayId) => {
|
|
432
|
+
const hiddenSessions = store.get('hiddenSessions');
|
|
433
|
+
return hiddenSessions[gatewayId] || [];
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
ipcMain.handle('set-session-visibility', (e, gatewayId, sessionId, visible) => {
|
|
437
|
+
const hiddenSessions = store.get('hiddenSessions');
|
|
438
|
+
const gatewayHidden = hiddenSessions[gatewayId] || [];
|
|
439
|
+
|
|
440
|
+
if (visible) {
|
|
441
|
+
// Remove from hidden list
|
|
442
|
+
hiddenSessions[gatewayId] = gatewayHidden.filter(id => id !== sessionId);
|
|
443
|
+
} else {
|
|
444
|
+
// Add to hidden list
|
|
445
|
+
if (!gatewayHidden.includes(sessionId)) {
|
|
446
|
+
gatewayHidden.push(sessionId);
|
|
447
|
+
hiddenSessions[gatewayId] = gatewayHidden;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
store.set('hiddenSessions', hiddenSessions);
|
|
452
|
+
return hiddenSessions[gatewayId] || [];
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Toggle between dark and light theme
|
|
456
|
+
function toggleTheme() {
|
|
457
|
+
// Cycle: system -> dark -> light -> system
|
|
458
|
+
// Or simply toggle: dark <-> light
|
|
459
|
+
if (nativeTheme.themeSource === 'system') {
|
|
460
|
+
nativeTheme.themeSource = nativeTheme.shouldUseDarkColors ? 'light' : 'dark';
|
|
461
|
+
} else if (nativeTheme.themeSource === 'dark') {
|
|
462
|
+
nativeTheme.themeSource = 'light';
|
|
463
|
+
} else {
|
|
464
|
+
nativeTheme.themeSource = 'dark';
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
151
468
|
// Notify all windows when theme changes
|
|
152
469
|
nativeTheme.on('updated', () => {
|
|
153
470
|
const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
|
@@ -193,11 +510,29 @@ function createWindow() {
|
|
|
193
510
|
|
|
194
511
|
mainWindow.loadFile(path.join(__dirname, '..', 'renderer', 'index.html'));
|
|
195
512
|
|
|
513
|
+
// Open DevTools with Cmd+Option+I
|
|
514
|
+
mainWindow.webContents.on('before-input-event', (event, input) => {
|
|
515
|
+
if (input.meta && input.alt && input.key === 'i') {
|
|
516
|
+
mainWindow.webContents.openDevTools({ mode: 'detach' });
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
196
520
|
// Inject CSS/JS into any webview that loads inside this window
|
|
197
521
|
mainWindow.webContents.on('did-attach-webview', (event, wvWebContents) => {
|
|
522
|
+
console.log('[Main] Webview attached, URL:', wvWebContents.getURL());
|
|
523
|
+
|
|
524
|
+
wvWebContents.on('did-start-loading', () => console.log('[Main] Webview did-start-loading'));
|
|
525
|
+
wvWebContents.on('did-stop-loading', () => console.log('[Main] Webview did-stop-loading'));
|
|
526
|
+
wvWebContents.on('did-fail-load', (e, code, desc) => console.log('[Main] Webview did-fail-load:', code, desc));
|
|
527
|
+
|
|
198
528
|
wvWebContents.on('dom-ready', () => {
|
|
199
|
-
|
|
200
|
-
|
|
529
|
+
const url = wvWebContents.getURL();
|
|
530
|
+
console.log('[Main] Webview dom-ready, URL:', url);
|
|
531
|
+
|
|
532
|
+
// TEMPORARILY DISABLE INJECTION TO DEBUG
|
|
533
|
+
// wvWebContents.insertCSS(OPENCLAW_CSS).catch(e => console.error('Main insertCSS failed:', e));
|
|
534
|
+
// wvWebContents.executeJavaScript(OPENCLAW_JS).catch(e => console.error('Main executeJS failed:', e));
|
|
535
|
+
console.log('[Main] CSS/JS injection DISABLED for debugging');
|
|
201
536
|
});
|
|
202
537
|
});
|
|
203
538
|
|
|
@@ -223,6 +558,19 @@ function createWindow() {
|
|
|
223
558
|
if (input.key === 'Escape') {
|
|
224
559
|
mainWindow.hide();
|
|
225
560
|
}
|
|
561
|
+
// Catch window cycling shortcuts even when webview has focus
|
|
562
|
+
if (input.type === 'keyDown' && input.shift && (input.meta || input.control)) {
|
|
563
|
+
if (input.key === 'ArrowRight') {
|
|
564
|
+
event.preventDefault();
|
|
565
|
+
cycleWindows('next');
|
|
566
|
+
} else if (input.key === 'ArrowLeft') {
|
|
567
|
+
event.preventDefault();
|
|
568
|
+
cycleWindows('prev');
|
|
569
|
+
} else if (input.key === 'k' || input.key === 'K') {
|
|
570
|
+
event.preventDefault();
|
|
571
|
+
toggleTheme();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
226
574
|
});
|
|
227
575
|
|
|
228
576
|
mainWindow.on('closed', () => {
|
|
@@ -240,7 +588,7 @@ function openSettings() {
|
|
|
240
588
|
|
|
241
589
|
settingsWindow = new BrowserWindow({
|
|
242
590
|
width: 520,
|
|
243
|
-
height:
|
|
591
|
+
height: 640,
|
|
244
592
|
resizable: false,
|
|
245
593
|
frame: false,
|
|
246
594
|
webPreferences: {
|
|
@@ -261,36 +609,146 @@ function openSettings() {
|
|
|
261
609
|
});
|
|
262
610
|
}
|
|
263
611
|
|
|
264
|
-
// ──
|
|
612
|
+
// ── Enhanced Window Cycling (Gateway → Sessions → Next Gateway) ─
|
|
265
613
|
|
|
266
|
-
function
|
|
614
|
+
function getAllWindowsInOrder() {
|
|
267
615
|
const gateways = store.get('gateways');
|
|
268
|
-
|
|
616
|
+
const allSessions = store.get('sessions');
|
|
617
|
+
const hiddenGateways = store.get('hiddenGateways');
|
|
618
|
+
const hiddenSessions = store.get('hiddenSessions');
|
|
619
|
+
const windows = [];
|
|
620
|
+
|
|
621
|
+
for (const gateway of gateways) {
|
|
622
|
+
// Skip hidden gateways
|
|
623
|
+
if (hiddenGateways.includes(gateway.id)) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Add main gateway window
|
|
628
|
+
windows.push({
|
|
629
|
+
type: 'gateway',
|
|
630
|
+
gatewayId: gateway.id,
|
|
631
|
+
gateway: gateway,
|
|
632
|
+
window: mainWindow
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// Add session windows for this gateway
|
|
636
|
+
const sessions = allSessions[gateway.id] || [];
|
|
637
|
+
const hiddenSessionsForGateway = hiddenSessions[gateway.id] || [];
|
|
638
|
+
|
|
639
|
+
for (const session of sessions) {
|
|
640
|
+
// Skip hidden sessions
|
|
641
|
+
if (hiddenSessionsForGateway.includes(session.id)) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const sessionWindow = sessionWindows.get(session.id);
|
|
646
|
+
if (sessionWindow && !sessionWindow.isDestroyed()) {
|
|
647
|
+
windows.push({
|
|
648
|
+
type: 'session',
|
|
649
|
+
gatewayId: gateway.id,
|
|
650
|
+
sessionId: session.id,
|
|
651
|
+
gateway: gateway,
|
|
652
|
+
session: session,
|
|
653
|
+
window: sessionWindow
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return windows;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function getCurrentlyFocusedWindow() {
|
|
663
|
+
const focused = BrowserWindow.getFocusedWindow();
|
|
664
|
+
if (!focused) return null;
|
|
665
|
+
|
|
666
|
+
if (focused === mainWindow) {
|
|
667
|
+
return { type: 'gateway', window: mainWindow };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
for (const [sessionId, sessionWindow] of sessionWindows.entries()) {
|
|
671
|
+
if (sessionWindow === focused) {
|
|
672
|
+
return { type: 'session', sessionId, window: sessionWindow };
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function cycleWindows(direction) {
|
|
680
|
+
const allWindows = getAllWindowsInOrder();
|
|
681
|
+
if (allWindows.length === 0) return;
|
|
682
|
+
|
|
683
|
+
const currentWindow = getCurrentlyFocusedWindow();
|
|
684
|
+
if (!currentWindow) {
|
|
685
|
+
// No window focused, show main window
|
|
686
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
687
|
+
mainWindow.show();
|
|
688
|
+
mainWindow.focus();
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Find current window index
|
|
694
|
+
let currentIndex = -1;
|
|
695
|
+
for (let i = 0; i < allWindows.length; i++) {
|
|
696
|
+
const w = allWindows[i];
|
|
697
|
+
if (currentWindow.type === 'gateway' && w.type === 'gateway') {
|
|
698
|
+
currentIndex = i;
|
|
699
|
+
break;
|
|
700
|
+
} else if (currentWindow.type === 'session' && w.type === 'session' && w.sessionId === currentWindow.sessionId) {
|
|
701
|
+
currentIndex = i;
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
269
705
|
|
|
270
|
-
|
|
271
|
-
|
|
706
|
+
if (currentIndex === -1) {
|
|
707
|
+
currentIndex = 0; // Fallback
|
|
708
|
+
}
|
|
272
709
|
|
|
273
|
-
|
|
710
|
+
// Calculate next index
|
|
711
|
+
let nextIndex;
|
|
274
712
|
if (direction === 'next') {
|
|
275
|
-
|
|
713
|
+
nextIndex = (currentIndex + 1) % allWindows.length;
|
|
276
714
|
} else {
|
|
277
|
-
|
|
715
|
+
nextIndex = (currentIndex - 1 + allWindows.length) % allWindows.length;
|
|
278
716
|
}
|
|
279
717
|
|
|
280
|
-
const
|
|
281
|
-
store.set('activeGateway', newGateway.id);
|
|
718
|
+
const nextWindow = allWindows[nextIndex];
|
|
282
719
|
|
|
283
|
-
//
|
|
284
|
-
if (
|
|
285
|
-
|
|
720
|
+
// Hide current window
|
|
721
|
+
if (currentWindow.window && !currentWindow.window.isDestroyed()) {
|
|
722
|
+
currentWindow.window.hide();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Show and focus next window
|
|
726
|
+
if (nextWindow.window && !nextWindow.window.isDestroyed()) {
|
|
727
|
+
nextWindow.window.show();
|
|
728
|
+
nextWindow.window.focus();
|
|
729
|
+
|
|
730
|
+
// If switching to main window, update active gateway
|
|
731
|
+
if (nextWindow.type === 'gateway') {
|
|
732
|
+
store.set('activeGateway', nextWindow.gatewayId);
|
|
733
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
734
|
+
mainWindow.webContents.send('switch-gateway', nextWindow.gatewayId);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
286
737
|
}
|
|
287
738
|
}
|
|
288
739
|
|
|
740
|
+
// Legacy function for backward compatibility
|
|
741
|
+
function cycleGateway(direction) {
|
|
742
|
+
cycleWindows(direction);
|
|
743
|
+
}
|
|
744
|
+
|
|
289
745
|
// ── Toggle / Tray / Hotkey ────────────────────────────────────
|
|
290
746
|
|
|
291
747
|
function toggleWindow() {
|
|
292
748
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
|
293
749
|
createWindow();
|
|
750
|
+
// Update updater's main window reference
|
|
751
|
+
updater.setMainWindow(mainWindow);
|
|
294
752
|
return;
|
|
295
753
|
}
|
|
296
754
|
if (mainWindow.isVisible()) {
|
|
@@ -313,6 +771,7 @@ function createTray() {
|
|
|
313
771
|
{ label: 'Show/Hide', click: toggleWindow },
|
|
314
772
|
{ type: 'separator' },
|
|
315
773
|
{ label: 'Settings', click: openSettings },
|
|
774
|
+
{ label: 'Check for Updates...', click: () => updater.checkForUpdate(true) },
|
|
316
775
|
{ type: 'separator' },
|
|
317
776
|
{ label: 'Quit', click: () => app.quit() }
|
|
318
777
|
]);
|
|
@@ -331,12 +790,16 @@ function registerHotkey() {
|
|
|
331
790
|
console.error(`Failed to register hotkey: ${hotkey}`);
|
|
332
791
|
}
|
|
333
792
|
|
|
334
|
-
//
|
|
335
|
-
const nextRegistered = globalShortcut.register('CommandOrControl+Shift+Right', () =>
|
|
336
|
-
const prevRegistered = globalShortcut.register('CommandOrControl+Shift+Left', () =>
|
|
793
|
+
// Window cycling shortcuts (Cmd+Shift+Right/Left)
|
|
794
|
+
const nextRegistered = globalShortcut.register('CommandOrControl+Shift+Right', () => cycleWindows('next'));
|
|
795
|
+
const prevRegistered = globalShortcut.register('CommandOrControl+Shift+Left', () => cycleWindows('prev'));
|
|
337
796
|
|
|
338
797
|
if (!nextRegistered) console.error('Failed to register Cmd+Shift+Right');
|
|
339
798
|
if (!prevRegistered) console.error('Failed to register Cmd+Shift+Left');
|
|
799
|
+
|
|
800
|
+
// Theme toggle shortcut (Cmd+Shift+K)
|
|
801
|
+
const themeRegistered = globalShortcut.register('CommandOrControl+Shift+K', toggleTheme);
|
|
802
|
+
if (!themeRegistered) console.error('Failed to register Cmd+Shift+K for theme toggle');
|
|
340
803
|
}
|
|
341
804
|
|
|
342
805
|
if (process.platform === 'darwin') {
|
|
@@ -347,6 +810,22 @@ app.whenReady().then(() => {
|
|
|
347
810
|
createWindow();
|
|
348
811
|
createTray();
|
|
349
812
|
registerHotkey();
|
|
813
|
+
|
|
814
|
+
// Initialize auto-updater
|
|
815
|
+
updater.registerIpcHandlers();
|
|
816
|
+
updater.setMainWindow(mainWindow);
|
|
817
|
+
|
|
818
|
+
// Check for updates on launch (respects 24h cache)
|
|
819
|
+
updater.checkForUpdate(false);
|
|
820
|
+
|
|
821
|
+
// Refresh sessions for all gateways on startup
|
|
822
|
+
setTimeout(() => {
|
|
823
|
+
refreshAllSessions().then(() => {
|
|
824
|
+
console.log('[Sessions] Initial session refresh completed');
|
|
825
|
+
}).catch(err => {
|
|
826
|
+
console.log('[Sessions] Initial session refresh failed:', err.message);
|
|
827
|
+
});
|
|
828
|
+
}, 2000); // Give gateways time to start
|
|
350
829
|
});
|
|
351
830
|
|
|
352
831
|
app.on('will-quit', () => {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld('electronAPI', {
|
|
4
|
+
// Window management
|
|
5
|
+
closeWindow: () => {
|
|
6
|
+
const window = require('@electron/remote').getCurrentWindow();
|
|
7
|
+
window.close();
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
// Theme support
|
|
11
|
+
getTheme: () => ipcRenderer.invoke('get-theme'),
|
|
12
|
+
onThemeChanged: (cb) => ipcRenderer.on('theme-changed', (_event, theme) => cb(theme)),
|
|
13
|
+
|
|
14
|
+
// Session loading
|
|
15
|
+
onLoadSession: (cb) => ipcRenderer.on('load-session', (_event, sessionData) => cb(sessionData)),
|
|
16
|
+
});
|
|
@@ -10,4 +10,18 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
10
10
|
// Theme support
|
|
11
11
|
getTheme: () => ipcRenderer.invoke('get-theme'),
|
|
12
12
|
onThemeChanged: (cb) => ipcRenderer.on('theme-changed', (_event, theme) => cb(theme)),
|
|
13
|
+
// Session management
|
|
14
|
+
getSessions: (gatewayId) => ipcRenderer.invoke('get-sessions', gatewayId),
|
|
15
|
+
refreshSessions: (gatewayId) => ipcRenderer.invoke('refresh-sessions', gatewayId),
|
|
16
|
+
// Visibility management
|
|
17
|
+
getHiddenGateways: () => ipcRenderer.invoke('get-hidden-gateways'),
|
|
18
|
+
setGatewayVisibility: (gatewayId, visible) => ipcRenderer.invoke('set-gateway-visibility', gatewayId, visible),
|
|
19
|
+
getHiddenSessions: (gatewayId) => ipcRenderer.invoke('get-hidden-sessions', gatewayId),
|
|
20
|
+
setSessionVisibility: (gatewayId, sessionId, visible) => ipcRenderer.invoke('set-session-visibility', gatewayId, sessionId, visible),
|
|
21
|
+
// Auto-update support
|
|
22
|
+
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
|
23
|
+
checkForUpdate: (force) => ipcRenderer.invoke('check-for-update', force),
|
|
24
|
+
getUpdateState: () => ipcRenderer.invoke('get-update-state'),
|
|
25
|
+
startUpdate: () => ipcRenderer.invoke('start-update'),
|
|
26
|
+
onUpdateStateChanged: (cb) => ipcRenderer.on('update-state-changed', (_event, state) => cb(state)),
|
|
13
27
|
});
|