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
13 changes: 10 additions & 3 deletions packages/desktop/src/main/windows/early.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type EarlyWindow = {
readonly win: BrowserWindow
readonly state: WindowState
readonly shownAt: number
reportStateError: (error: Error) => void
}

const current = { state: initialEarlyWindowState<EarlyWindow>() }
Expand Down Expand Up @@ -107,11 +108,17 @@ export function createEarlyWindow() {
},
})

manageWindowState(win, file, state, displays)
const entry: EarlyWindow = {
id,
win,
state,
shownAt: Date.now(),
reportStateError: (error) => console.warn("failed to save window state", { file, error }),
}

if (deferReveal) win.show()
manageWindowState(win, file, state, displays, (error) => entry.reportStateError(error))

const entry: EarlyWindow = { id, win, state, shownAt: Date.now() }
if (deferReveal) win.show()

dispatch({ type: "early.created", id, entry })
win.once("closed", () => dispatch({ type: "early.closed", entry }))
Expand Down
8 changes: 7 additions & 1 deletion packages/desktop/src/main/windows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,18 @@ export const mainWindows = Effect.fn("Window.make")(function* () {
},
})

const reportStateError = (error: Error) =>
runFork(scoped("window", Effect.logWarning("failed to save window state", { file: stateFile, error })))

// Upgrade the early window's reporter once the logging layer is available.
if (early) early.reportStateError = reportStateError

// The early window was secured and loaded when it was created.
if (!early) {
allowRendererPermissions(win)
wireNavigationPolicy(win, (url) => runFork(openExternalURL(url)))
wireRendererHeaders(win)
manageWindowState(win, stateFile, state, displays)
manageWindowState(win, stateFile, state, displays, reportStateError)
}

const focusForTests = () => {
Expand Down
38 changes: 35 additions & 3 deletions packages/desktop/src/main/windows/window-state.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { describe, expect, test } from "bun:test"
import { mkdtempSync, writeFileSync } from "node:fs"
import { describe, expect, mock, test } from "bun:test"
import { EventEmitter } from "node:events"
import type { BrowserWindow } from "electron"
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import path from "node:path"
import { readWindowState, resolveWindowState } from "./window-state"
import { manageWindowState, readWindowState, resolveWindowState } from "./window-state"

const defaults = { width: 1280, height: 800 }

Expand Down Expand Up @@ -109,3 +111,33 @@ describe("window state", () => {
expect(readWindowState(path.join(dir, "missing.json"))).toBeUndefined()
})
})

test("a failed window-state save reports the error and allows closing to continue", () => {
const dir = mkdtempSync(path.join(tmpdir(), "opencode-window-close-"))
const file = path.join(dir, "window.json")
const report = mock((_error: Error) => {})
const closed = mock(() => {})

const win = Object.assign(new EventEmitter(), {
isDestroyed: () => false,
getBounds: () => ({ x: 10, y: 20, width: 800, height: 600 }),
isMaximized: () => false,
isMinimized: () => false,
isFullScreen: () => false,
})

try {
writeFileSync(file, "previous state")
mkdirSync(`${file}.tmp`)
// SAFETY: the fixture implements the window events and getters used for this non-maximized state.
manageWindowState(win as BrowserWindow, file, defaults, displays, report)
win.on("closed", closed)
win.emit("close")
expect(() => win.emit("closed")).not.toThrow()
expect(closed).toHaveBeenCalledTimes(1)
expect(report).toHaveBeenCalledWith(expect.any(Error))
expect(readFileSync(file, "utf8")).toBe("previous state")
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
19 changes: 15 additions & 4 deletions packages/desktop/src/main/windows/window-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,13 @@ export function readWindowState(file: string) {

// Tracks the window until it closes and writes the final state then. Bounds are recorded while
// the window is in its normal state so a maximized window restores to its previous size.
export function manageWindowState(win: BrowserWindow, file: string, initial: WindowState, displays: Displays) {
export function manageWindowState(
win: BrowserWindow,
file: string,
initial: WindowState,
displays: Displays,
onError: (error: Error) => void,
) {
const state = { ...initial }

if (state.isMaximized) win.maximize()
Expand Down Expand Up @@ -137,9 +143,14 @@ export function manageWindowState(win: BrowserWindow, file: string, initial: Win
win.off("move", changed)
win.off("close", update)
win.off("closed", closed)
mkdirSync(path.dirname(file), { recursive: true })
writeFileSync(`${file}.tmp`, JSON.stringify(state))
renameSync(`${file}.tmp`, file)

try {
mkdirSync(path.dirname(file), { recursive: true })
writeFileSync(`${file}.tmp`, JSON.stringify(state))
renameSync(`${file}.tmp`, file)
} catch (error) {
onError(error instanceof Error ? error : new Error(String(error)))
}
}

win.on("resize", changed)
Expand Down
Loading