Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2bad280
feat: add window state persistence to user preferences
miya Sep 23, 2025
54c08fb
feat: implement window state persistence and validation
miya Sep 23, 2025
ad278b4
fix: add optional chaining to validate window bounds
miya Sep 24, 2025
6924373
fix: replace hardcoded debounce delay with constant for save operations
miya Sep 24, 2025
8d13821
fix: rename saveTimeout variable for clarity in window state persistence
miya Sep 24, 2025
ebeede5
fix: reset saveTimeoutId after saving window bounds to prevent memory…
miya Sep 24, 2025
d198c68
fix: improve validation logic for window bounds to handle null values
miya Sep 28, 2025
1b40f27
fix: Remove unnecessary validation
miya Sep 28, 2025
65171d9
fix: enhance showWindowWhenReady to handle maximized state and simpli…
miya Sep 28, 2025
54f712f
fix: rm validateWindowBounds (utilize the validation used internally)
miya Sep 28, 2025
02836d7
fix: remove getSavedWindowBounds function and directly access prefere…
miya Sep 28, 2025
df9f359
fix: update error handling in saveWindowBounds to log errors to console
miya Sep 28, 2025
32f90ca
fix: refactor getSavedWindowBounds to streamline window bounds retrieval
miya Sep 28, 2025
3dff406
Merge branch 'main' into feat/window-bounds-persistence
miya Sep 28, 2025
8ed328c
fix: remove unused electron imports from window-manager
miya Sep 28, 2025
c7d854f
fix: remove unnecessary line break
miya Sep 28, 2025
ed70691
Merge branch 'main' into feat/window-bounds-persistence
miya Oct 1, 2025
2e4b687
Update packages/compass/src/main/window-manager.ts
miya Oct 2, 2025
2750310
assign log Id
miya Oct 2, 2025
8bdb6c6
Merge branch 'main' into feat/window-bounds-persistence
miya Oct 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/compass-preferences-model/src/preferences-schema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ export type InternalUserPreferences = {
// TODO: Remove this as part of COMPASS-8970.
enableConnectInNewWindow: boolean;
showEndOfLifeConnectionModal: boolean;
// Window state persistence
windowBounds?: {
x?: number;
y?: number;
width?: number;
height?: number;
isMaximized?: boolean;
};
};

// UserPreferences contains all preferences stored to disk.
Expand Down Expand Up @@ -459,6 +467,25 @@ export const storedUserPreferencesProps: Required<{
),
type: 'boolean',
},
/**
* Window bounds for restoring window size and position.
*/
windowBounds: {
ui: false,
cli: false,
global: false,
description: null,
validator: z
.object({
x: z.number().optional(),
y: z.number().optional(),
width: z.number().optional(),
height: z.number().optional(),
isMaximized: z.boolean().optional(),
})
.optional(),
type: 'object',
},
/**
* Enable/disable the AI services. This is currently set
* in the atlas-service initialization where we make a request to the
Expand Down
131 changes: 129 additions & 2 deletions packages/compass/src/main/window-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
registerConnectionIdForBrowserWindow,
} from './auto-connect';

import { screen, type Display } from 'electron';

const { debug } = createLogger('COMPASS-WINDOW-MANAGER');

const earlyOpenUrls: string[] = [];
Expand Down Expand Up @@ -68,6 +70,7 @@ const DEFAULT_HEIGHT = (() => {
// change significantly at widths of 1024 and less.
const MIN_WIDTH = process.env.COMPASS_MIN_WIDTH ?? 1025;
const MIN_HEIGHT = process.env.COMPASS_MIN_HEIGHT ?? 640;
const SAVE_DEBOUNCE_DELAY = 500; // 500ms delay for save operations

/**
* The app's HTML shell which is the output of `./src/index.html`
Expand All @@ -82,6 +85,95 @@ async function showWindowWhenReady(bw: BrowserWindow) {
bw.show();
}

/**
* Save window bounds to preferences
*/
async function saveWindowBounds(
window: BrowserWindow,
compassApp: typeof CompassApplication
) {
try {
const bounds = window.getBounds();
const isMaximized = window.isMaximized();

await compassApp.preferences.savePreferences({
windowBounds: {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
isMaximized,
},
});
} catch (error) {
debug('Failed to save window bounds:', error);
}
}

/**
* Get saved window bounds from preferences
*/
function getSavedWindowBounds(compassApp: typeof CompassApplication) {
try {
const preferences = compassApp.preferences.getPreferences();
return preferences.windowBounds;
} catch (error) {
debug('Failed to get saved window bounds:', error);
return undefined;
}
}

/**
* Validate and adjust window bounds to ensure they're visible on screen
*/
function validateWindowBounds(bounds: {
x?: number;
y?: number;
width?: number;
height?: number;
}) {
if (bounds?.width == null || bounds?.height == null) {
return {
width: Number(DEFAULT_WIDTH),
height: Number(DEFAULT_HEIGHT),
};
}

// Ensure minimum size
const width = Math.max(bounds.width, Number(MIN_WIDTH));
const height = Math.max(bounds.height, Number(MIN_HEIGHT));
Copy link
Preview

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The Math.max calls enforce minimum dimensions but this validation is duplicated with the minWidth/minHeight properties set on the BrowserWindow. Consider removing this duplication since Electron will enforce the minimums automatically.

Suggested change
// Ensure minimum size
const width = Math.max(bounds.width, Number(MIN_WIDTH));
const height = Math.max(bounds.height, Number(MIN_HEIGHT));
// Use provided size; Electron will enforce minimums
const width = bounds.width;
const height = bounds.height;

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed: 1b40f27


// If no position specified, let Electron handle it
if (bounds?.x == null || bounds?.y == null) {
return { width, height };
}

// Check if window would be visible on any display
const windowRect = {
x: bounds.x,
y: bounds.y,
width,
height,
};

const displays = screen.getAllDisplays();
const isVisible = displays.some((display: Display) => {
const { bounds: displayBounds } = display;
return (
windowRect.x < displayBounds.x + displayBounds.width &&
windowRect.x + windowRect.width > displayBounds.x &&
windowRect.y < displayBounds.y + displayBounds.height &&
windowRect.y + windowRect.height > displayBounds.y
);
});

if (isVisible) {
return { ...windowRect };
}

return { width, height };
}

/**
* Call me instead of using `new BrowserWindow()` directly because i'll:
*
Expand Down Expand Up @@ -109,9 +201,12 @@ function showConnectWindow(
}
> = {}
): BrowserWindow {
// Get saved window bounds
const savedBounds = getSavedWindowBounds(compassApp);
const validatedBounds = validateWindowBounds(savedBounds);

const windowOpts = {
width: Number(DEFAULT_WIDTH),
height: Number(DEFAULT_HEIGHT),
...validatedBounds,
minWidth: Number(MIN_WIDTH),
minHeight: Number(MIN_HEIGHT),
Comment on lines 167 to 170
Copy link
Preview

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spread operator ...validatedBounds may override the minWidth and minHeight properties if validatedBounds contains width and height properties with smaller values than the minimums. The spread should come after the min properties to ensure minimums are enforced.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...validatedBounds has been removed.

Additionally, since the ...bounds return value does not include minWidth and minHeight, they are not overwritten.

/**
Expand Down Expand Up @@ -156,8 +251,36 @@ function showConnectWindow(

compassApp.emit('new-window', window);

// Set up window state persistence
let saveTimeoutId: NodeJS.Timeout | null = null;
const debouncedSaveWindowBounds = () => {
if (saveTimeoutId) {
clearTimeout(saveTimeoutId);
}
saveTimeoutId = setTimeout(() => {
if (window && !window.isDestroyed()) {
void saveWindowBounds(window, compassApp);
}
saveTimeoutId = null;
}, SAVE_DEBOUNCE_DELAY); // Debounce to avoid too frequent saves
};

// Save window bounds when moved or resized
window.on('moved', debouncedSaveWindowBounds);
window.on('resized', debouncedSaveWindowBounds);
window.on('maximize', debouncedSaveWindowBounds);
window.on('unmaximize', debouncedSaveWindowBounds);

// Restore maximized state if it was saved
if (savedBounds?.isMaximized) {
window.maximize();
}
Copy link
Preview

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The maximized state restoration should happen after the window is shown to avoid potential visual glitches. Consider moving this logic after the showWindowWhenReady(window) call or inside the showWindowWhenReady function.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed: 65171d9


const onWindowClosed = () => {
debug('Window closed. Dereferencing.');
if (saveTimeoutId) {
clearTimeout(saveTimeoutId);
}
window = null;
void unsubscribeProxyListenerPromise.then((unsubscribe) => unsubscribe());
};
Expand Down Expand Up @@ -243,6 +366,8 @@ class CompassWindowManager {
if (first) {
debug('sending `app:quit` msg');
first.webContents.send('app:quit');
// Save window bounds before quitting
void saveWindowBounds(first, compassApp);
}
});

Expand Down Expand Up @@ -284,6 +409,8 @@ class CompassWindowManager {
ipcMain?.handle('compass:maximize', () => {
const first = BrowserWindow.getAllWindows()[0];
first.maximize();
// Save the maximized state
void saveWindowBounds(first, compassApp);
});

await electronApp.whenReady();
Expand Down