Skip to content

Commit 3930015

Browse files
committed
feat(window): restore fullscreen after update restart only
Persist a one-shot "restore fullscreen" flag set exactly on the update-quit path (AppLifecycleService.setQuittingForUpdate), so a "restart to apply" relaunch reopens fullscreen if the session was fullscreen. A normal quit (Cmd+Q, window close) never sets the flag, so the app reverts to windowed launch behaviour. Live-track fullscreen via enter/leave-full-screen listeners, guard saveWindowState against persisting maximized/fullscreen bounds, and clear the flag on both launch (one-shot) and aborted update handoff. Generated-By: PostHog Code Task-Id: 2854ee1d-35b1-479f-aea5-07c367280dc4
1 parent 3f25bd8 commit 3930015

4 files changed

Lines changed: 96 additions & 4 deletions

File tree

apps/code/src/main/services/app-lifecycle/service.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const {
1313
mockShutdownPostHog,
1414
mockShutdownOtelTransport,
1515
mockProcessExit,
16+
mockGetFullScreenState,
17+
mockSetRestoreFullScreenOnNextLaunch,
1618
} = vi.hoisted(() => {
1719
const mockDatabaseService = {
1820
close: vi.fn(),
@@ -49,9 +51,16 @@ const {
4951
mockShutdownPostHog: vi.fn(() => Promise.resolve()),
5052
mockShutdownOtelTransport: vi.fn(() => Promise.resolve()),
5153
mockProcessExit: vi.fn() as unknown as (code?: number) => never,
54+
mockGetFullScreenState: vi.fn(() => false),
55+
mockSetRestoreFullScreenOnNextLaunch: vi.fn(),
5256
};
5357
});
5458

59+
vi.mock("../../utils/store.js", () => ({
60+
getFullScreenState: mockGetFullScreenState,
61+
setRestoreFullScreenOnNextLaunch: mockSetRestoreFullScreenOnNextLaunch,
62+
}));
63+
5564
vi.mock("../../utils/logger.js", () => ({
5665
logger: {
5766
scope: () => ({
@@ -119,6 +128,27 @@ describe("AppLifecycleService", () => {
119128
service.clearQuittingForUpdate();
120129
expect(service.isQuittingForUpdate).toBe(false);
121130
});
131+
132+
it("remembers fullscreen for the next launch when quitting for update in fullscreen", () => {
133+
mockGetFullScreenState.mockReturnValue(true);
134+
service.setQuittingForUpdate();
135+
expect(mockSetRestoreFullScreenOnNextLaunch).toHaveBeenCalledWith(true);
136+
});
137+
138+
it("does not schedule a fullscreen restore when not fullscreen", () => {
139+
mockGetFullScreenState.mockReturnValue(false);
140+
service.setQuittingForUpdate();
141+
expect(mockSetRestoreFullScreenOnNextLaunch).toHaveBeenCalledWith(false);
142+
});
143+
144+
it("clears the fullscreen-restore flag when the update handoff is aborted", () => {
145+
mockGetFullScreenState.mockReturnValue(true);
146+
service.setQuittingForUpdate();
147+
service.clearQuittingForUpdate();
148+
expect(mockSetRestoreFullScreenOnNextLaunch).toHaveBeenLastCalledWith(
149+
false,
150+
);
151+
});
122152
});
123153

124154
describe("isShuttingDown", () => {

apps/code/src/main/services/app-lifecycle/service.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import { posthogNodeAnalytics } from "../../platform-adapters/posthog-analytics"
1717
import { withTimeout } from "../../utils/async";
1818
import { logger } from "../../utils/logger";
1919
import { shutdownOtelTransport } from "../../utils/otel-log-transport";
20+
import {
21+
getFullScreenState,
22+
setRestoreFullScreenOnNextLaunch,
23+
} from "../../utils/store";
2024

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

@@ -53,10 +57,17 @@ export class AppLifecycleService {
5357

5458
setQuittingForUpdate(): void {
5559
this._isQuittingForUpdate = true;
60+
// Remember whether we were fullscreen so the post-update relaunch restores
61+
// it. A normal quit never runs this, so the flag stays false and the app
62+
// reopens windowed.
63+
setRestoreFullScreenOnNextLaunch(getFullScreenState());
5664
}
5765

5866
clearQuittingForUpdate(): void {
5967
this._isQuittingForUpdate = false;
68+
// The install handoff was aborted; don't let a stale flag restore
69+
// fullscreen on the next manual launch.
70+
setRestoreFullScreenOnNextLaunch(false);
6071
}
6172

6273
/**

apps/code/src/main/utils/store.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export interface WindowStateSchema {
2525
height: number;
2626
isMaximized: boolean;
2727
zoomLevel: number;
28+
isFullScreen: boolean;
29+
restoreFullScreenOnNextLaunch: boolean;
2830
}
2931

3032
const userDataDir = getUserDataDir();
@@ -52,9 +54,33 @@ export const windowStateStore = new Store<WindowStateSchema>({
5254
height: 600,
5355
isMaximized: true,
5456
zoomLevel: 0,
57+
isFullScreen: false,
58+
restoreFullScreenOnNextLaunch: false,
5559
},
5660
});
5761

5862
export function saveZoomLevel(level: number): void {
5963
windowStateStore.set("zoomLevel", level);
6064
}
65+
66+
export function saveFullScreenState(isFullScreen: boolean): void {
67+
windowStateStore.set("isFullScreen", isFullScreen);
68+
}
69+
70+
export function getFullScreenState(): boolean {
71+
return windowStateStore.get("isFullScreen", false);
72+
}
73+
74+
/**
75+
* One-shot flag that survives a single relaunch. Set only when the app quits
76+
* to install an update, so a fullscreen session is restored after the
77+
* "restart to apply" handoff — but a normal quit (Cmd+Q, window close) leaves
78+
* it false and launches windowed.
79+
*/
80+
export function setRestoreFullScreenOnNextLaunch(restore: boolean): void {
81+
windowStateStore.set("restoreFullScreenOnNextLaunch", restore);
82+
}
83+
84+
export function getRestoreFullScreenOnNextLaunch(): boolean {
85+
return windowStateStore.get("restoreFullScreenOnNextLaunch", false);
86+
}

apps/code/src/main/window.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import { collectMemorySnapshot } from "./utils/crash-diagnostics";
2424
import { isDevBuild } from "./utils/env";
2525
import { logger, readChromiumLogTail } from "./utils/logger";
2626
import {
27+
getRestoreFullScreenOnNextLaunch,
28+
saveFullScreenState,
2729
saveZoomLevel,
30+
setRestoreFullScreenOnNextLaunch,
2831
type WindowStateSchema,
2932
windowStateStore,
3033
} from "./utils/store";
@@ -54,6 +57,11 @@ function getSavedWindowState(): WindowStateSchema {
5457
height: windowStateStore.get("height", 600),
5558
isMaximized: windowStateStore.get("isMaximized", true),
5659
zoomLevel: windowStateStore.get("zoomLevel", 0),
60+
isFullScreen: windowStateStore.get("isFullScreen", false),
61+
restoreFullScreenOnNextLaunch: windowStateStore.get(
62+
"restoreFullScreenOnNextLaunch",
63+
false,
64+
),
5765
};
5866

5967
// Validate position is still on a connected display
@@ -71,9 +79,11 @@ export function saveWindowState(window: BrowserWindow): void {
7179
const isMaximized = window.isMaximized();
7280
windowStateStore.set("isMaximized", isMaximized);
7381

74-
// Only save bounds when not maximized, so restoring from maximized
75-
// gives the user their previous windowed size/position
76-
if (!isMaximized) {
82+
// Only save bounds when the window is in its normal state. Persisting the
83+
// maximized or fullscreen bounds would clobber the user's windowed
84+
// size/position, so a later "revert to windowed" launch has nothing to
85+
// restore to.
86+
if (!isMaximized && !window.isFullScreen()) {
7787
const bounds = window.getBounds();
7888
windowStateStore.set("x", bounds.x);
7989
windowStateStore.set("y", bounds.y);
@@ -158,6 +168,15 @@ function setupEditableContextMenu(window: BrowserWindow): void {
158168
export function createWindow(): void {
159169
const isDev = isDevBuild();
160170
const savedState = getSavedWindowState();
171+
172+
// Read the one-shot fullscreen-restore flag and clear it immediately, so it
173+
// only ever affects the single launch that follows an update restart. A
174+
// normal quit never sets it, so this is false and the app launches windowed.
175+
const restoreFullScreen = getRestoreFullScreenOnNextLaunch();
176+
if (restoreFullScreen) {
177+
setRestoreFullScreenOnNextLaunch(false);
178+
}
179+
161180
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
162181

163182
const scheduleSaveWindowState = (window: BrowserWindow): void => {
@@ -232,7 +251,9 @@ export function createWindow(): void {
232251
if (windowShown) return;
233252
windowShown = true;
234253
clearTimeout(showFallback);
235-
if (savedState.isMaximized) {
254+
if (restoreFullScreen) {
255+
mainWindow?.setFullScreen(true);
256+
} else if (savedState.isMaximized) {
236257
mainWindow?.maximize();
237258
}
238259
mainWindow?.show();
@@ -273,6 +294,10 @@ export function createWindow(): void {
273294
mainWindow.on("unmaximize", () => mainWindow && saveWindowState(mainWindow));
274295
mainWindow.on("close", () => mainWindow && saveWindowState(mainWindow));
275296

297+
// Live-track fullscreen so the update-quit path can read the current state.
298+
mainWindow.on("enter-full-screen", () => saveFullScreenState(true));
299+
mainWindow.on("leave-full-screen", () => saveFullScreenState(false));
300+
276301
container
277302
.get<ElectronMainWindow>(MAIN_WINDOW_SERVICE)
278303
.setMainWindowGetter(() => mainWindow);

0 commit comments

Comments
 (0)