Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions apps/code/src/main/services/app-lifecycle/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const {
mockShutdownPostHog,
mockShutdownOtelTransport,
mockProcessExit,
mockGetFullScreenState,
mockSetRestoreFullScreenOnNextLaunch,
} = vi.hoisted(() => {
const mockDatabaseService = {
close: vi.fn(),
Expand Down Expand Up @@ -49,9 +51,16 @@ const {
mockShutdownPostHog: vi.fn(() => Promise.resolve()),
mockShutdownOtelTransport: vi.fn(() => Promise.resolve()),
mockProcessExit: vi.fn() as unknown as (code?: number) => never,
mockGetFullScreenState: vi.fn(() => false),
mockSetRestoreFullScreenOnNextLaunch: vi.fn(),
};
});

vi.mock("../../utils/store.js", () => ({
getFullScreenState: mockGetFullScreenState,
setRestoreFullScreenOnNextLaunch: mockSetRestoreFullScreenOnNextLaunch,
}));

vi.mock("../../utils/logger.js", () => ({
logger: {
scope: () => ({
Expand Down Expand Up @@ -119,6 +128,26 @@ describe("AppLifecycleService", () => {
service.clearQuittingForUpdate();
expect(service.isQuittingForUpdate).toBe(false);
});

it.each([[true], [false]])(
"schedules restore-fullscreen=%s on update-quit",
(isFullScreen) => {
mockGetFullScreenState.mockReturnValue(isFullScreen);
service.setQuittingForUpdate();
expect(mockSetRestoreFullScreenOnNextLaunch).toHaveBeenCalledWith(
isFullScreen,
);
},
);

it("clears the fullscreen-restore flag when the update handoff is aborted", () => {
mockGetFullScreenState.mockReturnValue(true);
service.setQuittingForUpdate();
service.clearQuittingForUpdate();
expect(mockSetRestoreFullScreenOnNextLaunch).toHaveBeenLastCalledWith(
false,
);
});
});

describe("isShuttingDown", () => {
Expand Down
8 changes: 8 additions & 0 deletions apps/code/src/main/services/app-lifecycle/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import { posthogNodeAnalytics } from "../../platform-adapters/posthog-analytics"
import { withTimeout } from "../../utils/async";
import { logger } from "../../utils/logger";
import { shutdownOtelTransport } from "../../utils/otel-log-transport";
import {
getFullScreenState,
setRestoreFullScreenOnNextLaunch,
} from "../../utils/store";

const log = logger.scope("app-lifecycle");

Expand Down Expand Up @@ -53,10 +57,14 @@ export class AppLifecycleService {

setQuittingForUpdate(): void {
this._isQuittingForUpdate = true;
// Remember fullscreen state so the post-update relaunch restores it.
setRestoreFullScreenOnNextLaunch(getFullScreenState());
}

clearQuittingForUpdate(): void {
this._isQuittingForUpdate = false;
// The install handoff was aborted.
setRestoreFullScreenOnNextLaunch(false);
}

/**
Expand Down
25 changes: 25 additions & 0 deletions apps/code/src/main/utils/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export interface WindowStateSchema {
height: number;
isMaximized: boolean;
zoomLevel: number;
isFullScreen: boolean;
restoreFullScreenOnNextLaunch: boolean;
}

const userDataDir = getUserDataDir();
Expand Down Expand Up @@ -52,9 +54,32 @@ export const windowStateStore = new Store<WindowStateSchema>({
height: 600,
isMaximized: true,
zoomLevel: 0,
isFullScreen: false,
restoreFullScreenOnNextLaunch: false,
},
});

export function saveZoomLevel(level: number): void {
windowStateStore.set("zoomLevel", level);
}

export function saveFullScreenState(isFullScreen: boolean): void {
windowStateStore.set("isFullScreen", isFullScreen);
}

export function getFullScreenState(): boolean {
return windowStateStore.get("isFullScreen", false);
}

/**
* Set only when the app quits to install an update, so a fullscreen session
* is restored after the "restart to apply" handoff.
* A normal quit leaves it false and launches windowed.
*/
export function setRestoreFullScreenOnNextLaunch(restore: boolean): void {
windowStateStore.set("restoreFullScreenOnNextLaunch", restore);
}

export function getRestoreFullScreenOnNextLaunch(): boolean {
return windowStateStore.get("restoreFullScreenOnNextLaunch", false);
}
25 changes: 23 additions & 2 deletions apps/code/src/main/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import { collectMemorySnapshot } from "./utils/crash-diagnostics";
import { isDevBuild } from "./utils/env";
import { logger, readChromiumLogTail } from "./utils/logger";
import {
saveFullScreenState,
saveZoomLevel,
setRestoreFullScreenOnNextLaunch,
type WindowStateSchema,
windowStateStore,
} from "./utils/store";
Expand Down Expand Up @@ -54,6 +56,11 @@ function getSavedWindowState(): WindowStateSchema {
height: windowStateStore.get("height", 600),
isMaximized: windowStateStore.get("isMaximized", true),
zoomLevel: windowStateStore.get("zoomLevel", 0),
isFullScreen: windowStateStore.get("isFullScreen", false),
restoreFullScreenOnNextLaunch: windowStateStore.get(
"restoreFullScreenOnNextLaunch",
false,
),
};

// Validate position is still on a connected display
Expand All @@ -73,7 +80,7 @@ export function saveWindowState(window: BrowserWindow): void {

// Only save bounds when not maximized, so restoring from maximized
// gives the user their previous windowed size/position
if (!isMaximized) {
if (!isMaximized && !window.isFullScreen()) {
const bounds = window.getBounds();
windowStateStore.set("x", bounds.x);
windowStateStore.set("y", bounds.y);
Expand Down Expand Up @@ -158,6 +165,14 @@ function setupEditableContextMenu(window: BrowserWindow): void {
export function createWindow(): void {
const isDev = isDevBuild();
const savedState = getSavedWindowState();

// Read the one-shot fullscreen-restore flag and clear it immediately, so it
// only ever affects the single launch that follows an update restart.
const restoreFullScreen = savedState.restoreFullScreenOnNextLaunch;
if (restoreFullScreen) {
setRestoreFullScreenOnNextLaunch(false);
}

let saveTimeout: ReturnType<typeof setTimeout> | null = null;

const scheduleSaveWindowState = (window: BrowserWindow): void => {
Expand Down Expand Up @@ -232,7 +247,9 @@ export function createWindow(): void {
if (windowShown) return;
windowShown = true;
clearTimeout(showFallback);
if (savedState.isMaximized) {
if (restoreFullScreen) {
mainWindow?.setFullScreen(true);
} else if (savedState.isMaximized) {
mainWindow?.maximize();
}
mainWindow?.show();
Expand Down Expand Up @@ -273,6 +290,10 @@ export function createWindow(): void {
mainWindow.on("unmaximize", () => mainWindow && saveWindowState(mainWindow));
mainWindow.on("close", () => mainWindow && saveWindowState(mainWindow));

// Live-track fullscreen so the update-quit path can read the current state.
mainWindow.on("enter-full-screen", () => saveFullScreenState(true));
mainWindow.on("leave-full-screen", () => saveFullScreenState(false));

container
.get<ElectronMainWindow>(MAIN_WINDOW_SERVICE)
.setMainWindowGetter(() => mainWindow);
Expand Down
Loading