fix(ui): keep main window closable after a native dialog on Windows (#6319)

A native Windows MessageBox attached to a parent window disables that
window (WS_DISABLED) for its lifetime and re-enables it on dismissal.
When the parent is the main window — whose WindowClosing hook hides
instead of closes — the enable/hide sequence races and leaves the window
unable to process its close (X) button afterwards, so e.g. a rejected
login error dialog left the main window stuck open.

Route all native dialogs through src/lib/dialogs.ts, which forces
Detached: true on Windows (NULL owner, no window ever disabled) and is a
no-op on macOS/Linux (keeps the attached sheet-style presentation).
This commit is contained in:
Zoltan Papp
2026-06-01 17:03:53 +02:00
committed by GitHub
parent 49df24b18c
commit 53d43980ad
13 changed files with 80 additions and 32 deletions
@@ -8,7 +8,8 @@ import {
useState,
type ReactNode,
} from "react";
import { Dialogs, Events } from "@wailsio/runtime";
import { Events } from "@wailsio/runtime";
import { errorDialog } from "@/lib/dialogs.ts";
import { Update as UpdateSvc, WindowManager } from "@bindings/services";
@@ -66,7 +67,7 @@ export const ClientVersionProvider = ({ children }: { children: ReactNode }) =>
})
.catch((e) => {
if (cancelled || isDaemonUnavailable(e)) return;
void Dialogs.Error({
void errorDialog({
Title: i18next.t("update.error.loadStateTitle"),
Message: formatErrorMessage(e),
});
@@ -106,7 +107,7 @@ export const ClientVersionProvider = ({ children }: { children: ReactNode }) =>
// produce a result) and surface the error.
if (isDaemonUnavailable(e)) return;
WindowManager.CloseInstallProgress().catch(console.error);
await Dialogs.Error({
await errorDialog({
Title: i18next.t("update.error.triggerTitle"),
Message: formatErrorMessage(e),
});
@@ -5,7 +5,7 @@ import {
useState,
type ReactNode,
} from "react";
import { Dialogs } from "@wailsio/runtime";
import { errorDialog } from "@/lib/dialogs.ts";
import {
Connection as ConnectionSvc,
Debug as DebugSvc,
@@ -163,7 +163,7 @@ const useDebugBundle = () => {
return;
}
setStage({ kind: "idle" });
await Dialogs.Error({
await errorDialog({
Title: i18next.t("settings.error.debugBundleTitle"),
Message: formatErrorMessage(e),
});
@@ -6,7 +6,8 @@ import {
useState,
type ReactNode,
} from "react";
import { Dialogs, Events } from "@wailsio/runtime";
import { Events } from "@wailsio/runtime";
import { errorDialog } from "@/lib/dialogs.ts";
import {
Connection,
ProfileSwitcher,
@@ -65,7 +66,7 @@ export const ProfileProvider = ({ children }: { children: ReactNode }) => {
if (msg.includes("code = Unavailable")) {
return;
}
await Dialogs.Error({
await errorDialog({
Title: i18next.t("profile.error.loadTitle"),
Message: formatErrorMessage(e),
});
@@ -7,7 +7,7 @@ import {
useState,
type ReactNode,
} from "react";
import { Dialogs } from "@wailsio/runtime";
import { errorDialog } from "@/lib/dialogs.ts";
import { Autostart, Settings as SettingsSvc } from "@bindings/services";
import type { Config } from "@bindings/services/models.js";
import i18next from "@/lib/i18n";
@@ -68,7 +68,7 @@ const useSettingsState = () => {
});
setConfig(c);
} catch (e) {
await Dialogs.Error({
await errorDialog({
Title: i18next.t("settings.error.loadTitle"),
Message: errorMessage(e),
});
@@ -98,7 +98,7 @@ const useSettingsState = () => {
username,
});
} catch (e) {
await Dialogs.Error({
await errorDialog({
Title: i18next.t("settings.error.saveTitle"),
Message: errorMessage(e),
});
@@ -211,7 +211,7 @@ export const AutostartSettingsProvider = ({ children }: { children: ReactNode })
await Autostart.SetEnabled(enabled);
} catch (e) {
setAutostart((s) => (s ? { ...s, enabled: !enabled } : s));
await Dialogs.Error({
await errorDialog({
Title: i18next.t("settings.general.autostart.errorTitle"),
Message: errorMessage(e),
});