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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flying-lobster",
3
- "version": "1.6.2",
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": ["universal"]
39
+ "arch": [
40
+ "universal"
41
+ ]
39
42
  },
40
43
  {
41
44
  "target": "dir",
42
- "arch": ["universal"]
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', () => store.get('activeGateway'));
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
- wvWebContents.insertCSS(OPENCLAW_CSS).catch(e => console.error('Main insertCSS failed:', e));
200
- wvWebContents.executeJavaScript(OPENCLAW_JS).catch(e => console.error('Main executeJS failed:', e));
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: 560,
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
- // ── Agent Switching Shortcuts ─────────────────────────────────
612
+ // ── Enhanced Window Cycling (Gateway → Sessions → Next Gateway) ─
265
613
 
266
- function cycleGateway(direction) {
614
+ function getAllWindowsInOrder() {
267
615
  const gateways = store.get('gateways');
268
- if (gateways.length < 2) return; // Nothing to cycle
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
- const activeId = store.get('activeGateway');
271
- const currentIndex = gateways.findIndex(g => g.id === activeId);
706
+ if (currentIndex === -1) {
707
+ currentIndex = 0; // Fallback
708
+ }
272
709
 
273
- let newIndex;
710
+ // Calculate next index
711
+ let nextIndex;
274
712
  if (direction === 'next') {
275
- newIndex = (currentIndex + 1) % gateways.length;
713
+ nextIndex = (currentIndex + 1) % allWindows.length;
276
714
  } else {
277
- newIndex = (currentIndex - 1 + gateways.length) % gateways.length;
715
+ nextIndex = (currentIndex - 1 + allWindows.length) % allWindows.length;
278
716
  }
279
717
 
280
- const newGateway = gateways[newIndex];
281
- store.set('activeGateway', newGateway.id);
718
+ const nextWindow = allWindows[nextIndex];
282
719
 
283
- // Notify main window to switch gateway
284
- if (mainWindow && !mainWindow.isDestroyed()) {
285
- mainWindow.webContents.send('switch-gateway', newGateway.id);
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
- // Agent switching shortcuts (Cmd+Shift+Right/Left)
335
- const nextRegistered = globalShortcut.register('CommandOrControl+Shift+Right', () => cycleGateway('next'));
336
- const prevRegistered = globalShortcut.register('CommandOrControl+Shift+Left', () => cycleGateway('prev'));
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
  });