Skip to content

Commit 23f4212

Browse files
committed
fix(settings): reload settings on system resume to prevent reset after sleep
When the computer goes to sleep and wakes up, some settings like MAX OUTPUT LINES PER RESPONSE were resetting to default values. This adds Electron's powerMonitor to detect system resume events and notify the renderer to reload settings from persistent storage: - Add powerMonitor.on('resume') listener in main process - Add onSystemResume IPC handler in preload API - Refactor loadSettings to useCallback for reuse - Register system resume listener in useSettings hook Closes #269
1 parent c1da171 commit 23f4212

File tree

6 files changed

+99
-6
lines changed

6 files changed

+99
-6
lines changed

src/__tests__/renderer/hooks/useSettings.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,6 +1295,61 @@ describe('useSettings', () => {
12951295
});
12961296
});
12971297

1298+
describe('system resume behavior', () => {
1299+
it('should register onSystemResume listener on mount', async () => {
1300+
renderHook(() => useSettings());
1301+
1302+
expect(window.maestro.app.onSystemResume).toHaveBeenCalled();
1303+
});
1304+
1305+
it('should reload settings when system resumes from sleep', async () => {
1306+
// Capture the callback passed to onSystemResume
1307+
let resumeCallback: (() => void) | undefined;
1308+
vi.mocked(window.maestro.app.onSystemResume).mockImplementation((cb) => {
1309+
resumeCallback = cb;
1310+
return () => {};
1311+
});
1312+
1313+
// Initial load with default settings
1314+
vi.mocked(window.maestro.settings.getAll).mockResolvedValue({
1315+
maxOutputLines: 25,
1316+
});
1317+
1318+
const { result } = renderHook(() => useSettings());
1319+
await waitForSettingsLoaded(result);
1320+
1321+
expect(result.current.maxOutputLines).toBe(25);
1322+
1323+
// Simulate settings change while asleep (user may have changed via another method)
1324+
// In the real bug, the setting was being reset, so simulate that by changing
1325+
// the mock to return a different value on next load
1326+
vi.mocked(window.maestro.settings.getAll).mockResolvedValue({
1327+
maxOutputLines: 0, // 0 means "ALL" in the UI
1328+
});
1329+
1330+
// Trigger system resume
1331+
await act(async () => {
1332+
resumeCallback?.();
1333+
// Allow async operations to complete
1334+
await new Promise((resolve) => setTimeout(resolve, 0));
1335+
});
1336+
1337+
// Settings should be reloaded with the new value
1338+
expect(result.current.maxOutputLines).toBe(0);
1339+
});
1340+
1341+
it('should cleanup onSystemResume listener on unmount', async () => {
1342+
const cleanupFn = vi.fn();
1343+
vi.mocked(window.maestro.app.onSystemResume).mockReturnValue(cleanupFn);
1344+
1345+
const { unmount } = renderHook(() => useSettings());
1346+
1347+
unmount();
1348+
1349+
expect(cleanupFn).toHaveBeenCalled();
1350+
});
1351+
});
1352+
12981353
describe('edge cases', () => {
12991354
it('should handle undefined values from settings.getAll gracefully', async () => {
13001355
// All settings return empty object (uses defaults)

src/__tests__/setup.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,12 @@ const mockMaestro = {
480480
onUpdated: vi.fn().mockReturnValue(() => {}),
481481
onContributionStarted: vi.fn().mockReturnValue(() => {}),
482482
},
483+
app: {
484+
onQuitConfirmationRequest: vi.fn().mockReturnValue(() => {}),
485+
confirmQuit: vi.fn(),
486+
cancelQuit: vi.fn(),
487+
onSystemResume: vi.fn().mockReturnValue(() => {}),
488+
},
483489
};
484490

485491
// Only mock window.maestro if window exists (jsdom environment)

src/main/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { app, BrowserWindow } from 'electron';
1+
import { app, BrowserWindow, powerMonitor } from 'electron';
22
import path from 'path';
33
import os from 'os';
44
import crypto from 'crypto';
@@ -358,6 +358,15 @@ app.whenReady().then(async () => {
358358
createWindow();
359359
}
360360
});
361+
362+
// Listen for system resume (after sleep/suspend) and notify renderer
363+
// This allows the renderer to refresh settings that may have been reset
364+
powerMonitor.on('resume', () => {
365+
logger.info('System resumed from sleep/suspend', 'PowerMonitor');
366+
if (isWebContentsAvailable(mainWindow)) {
367+
mainWindow.webContents.send('app:systemResume');
368+
}
369+
});
361370
});
362371

363372
app.on('window-all-closed', () => {

src/main/preload/system.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,15 @@ export function createAppApi() {
191191
cancelQuit: () => {
192192
ipcRenderer.send('app:quitCancelled');
193193
},
194+
/**
195+
* Listen for system resume event (after sleep/suspend)
196+
* Used to refresh settings that may have been reset during sleep
197+
*/
198+
onSystemResume: (callback: () => void) => {
199+
const handler = () => callback();
200+
ipcRenderer.on('app:systemResume', handler);
201+
return () => ipcRenderer.removeListener('app:systemResume', handler);
202+
},
194203
};
195204
}
196205

src/renderer/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,7 @@ interface MaestroAPI {
954954
onQuitConfirmationRequest: (callback: () => void) => () => void;
955955
confirmQuit: () => void;
956956
cancelQuit: () => void;
957+
onSystemResume: (callback: () => void) => () => void;
957958
};
958959
logger: {
959960
log: (

src/renderer/hooks/settings/useSettings.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,10 +1296,10 @@ export function useSettings(): UseSettingsReturn {
12961296
window.maestro.settings.set('sshRemoteHonorGitignore', value);
12971297
}, []);
12981298

1299-
// Load settings from electron-store on mount
1299+
// Load settings from electron-store
1300+
// This function is called on mount and on system resume (after sleep/suspend)
13001301
// PERF: Use batch loading to reduce IPC calls from ~60 to 3
1301-
useEffect(() => {
1302-
const loadSettings = async () => {
1302+
const loadSettings = useCallback(async () => {
13031303
try {
13041304
// Batch load all settings in a single IPC call
13051305
const allSettings = (await window.maestro.settings.getAll()) as Record<string, unknown>;
@@ -1725,9 +1725,22 @@ export function useSettings(): UseSettingsReturn {
17251725
// Mark settings as loaded even if there was an error (use defaults)
17261726
setSettingsLoaded(true);
17271727
}
1728-
};
1728+
}, []);
1729+
1730+
// Load settings on mount
1731+
useEffect(() => {
17291732
loadSettings();
1730-
}, []);
1733+
}, [loadSettings]);
1734+
1735+
// Reload settings when system resumes from sleep/suspend
1736+
// This ensures settings like maxOutputLines aren't reset to defaults
1737+
useEffect(() => {
1738+
const cleanup = window.maestro.app.onSystemResume(() => {
1739+
console.log('[Settings] System resumed from sleep, reloading settings');
1740+
loadSettings();
1741+
});
1742+
return cleanup;
1743+
}, [loadSettings]);
17311744

17321745
// Apply font size to HTML root element so rem-based Tailwind classes scale
17331746
// Only apply after settings are loaded to prevent layout shift from default->saved font size

0 commit comments

Comments
 (0)