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
@@ -1,7 +1,8 @@
import { useCallback, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useSearchParams } from "react-router-dom";
import { Dialogs, Events } from "@wailsio/runtime";
import { Events } from "@wailsio/runtime";
import { errorDialog } from "@/lib/dialogs.ts";
import { Loader2 } from "lucide-react";
import { Connection } from "@bindings/services";
import { Button } from "@/components/buttons/Button";
@@ -25,7 +26,7 @@ export default function LoginWaitingForBrowserDialog() {
const reportOpenFailure = useCallback(
(e: unknown) => {
void Dialogs.Error({
void errorDialog({
Title: t("browserLogin.openFailedTitle"),
Message: formatErrorMessage(e),
});
@@ -1,8 +1,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Dialogs, Events } from "@wailsio/runtime";
import { Events } from "@wailsio/runtime";
import { Connection, WindowManager } from "@bindings/services";
import i18next from "@/lib/i18n";
import { errorDialog } from "@/lib/dialogs.ts";
import { ToggleSwitch } from "@/components/switches/ToggleSwitch.tsx";
import { useStatus } from "@/contexts/StatusContext.tsx";
import { useProfile } from "@/contexts/ProfileContext.tsx";
@@ -105,7 +106,7 @@ async function startLogin(): Promise<void> {
} catch (e) {
WindowManager.CloseBrowserLogin().catch(console.error);
if (cancelled) return;
await Dialogs.Error({
await errorDialog({
Title: i18next.t("connect.error.loginTitle"),
Message: errorMessage(e),
});
@@ -192,7 +193,7 @@ export const MainConnectionStatusSwitch = () => {
} catch (e) {
setAction(null);
await refresh();
await Dialogs.Error({
await errorDialog({
Title: t("connect.error.connectTitle"),
Message: errorMessage(e),
});
@@ -212,7 +213,7 @@ export const MainConnectionStatusSwitch = () => {
} catch (e) {
setAction(null);
await refresh();
await Dialogs.Error({
await errorDialog({
Title: t("connect.error.disconnectTitle"),
Message: errorMessage(e),
});
@@ -1,6 +1,6 @@
import { forwardRef, useLayoutEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Dialogs } from "@wailsio/runtime";
import { errorDialog } from "@/lib/dialogs.ts";
import * as Popover from "@radix-ui/react-popover";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Command } from "cmdk";
@@ -36,7 +36,7 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
try {
await fn();
} catch (e) {
await Dialogs.Error({
await errorDialog({
Title: title,
Message: formatErrorMessage(e),
});
@@ -1,6 +1,6 @@
import { useLayoutEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Dialogs } from "@wailsio/runtime";
import { errorDialog, warningDialog } from "@/lib/dialogs.ts";
import { CircleMinus, PlusCircle, Trash2, UserCircle } from "lucide-react";
import type { Profile } from "@bindings/services/models.js";
import { Badge } from "@/components/Badge";
@@ -44,7 +44,7 @@ export function ProfilesTab() {
try {
await fn();
} catch (e) {
await Dialogs.Error({
await errorDialog({
Title: title,
Message: formatErrorMessage(e),
});
@@ -56,7 +56,7 @@ export function ProfilesTab() {
const handleDeregister = async (name: string) => {
const cancelLabel = i18next.t("common.cancel");
const confirmLabel = i18next.t("profile.deregister.confirm");
const result = await Dialogs.Warning({
const result = await warningDialog({
Title: i18next.t("profile.deregister.title"),
Message: i18next.t("profile.deregister.message", { name }),
Buttons: [
@@ -72,7 +72,7 @@ export function ProfilesTab() {
if (name === DEFAULT_PROFILE) return;
const cancelLabel = i18next.t("common.cancel");
const confirmLabel = i18next.t("common.delete");
const result = await Dialogs.Warning({
const result = await warningDialog({
Title: i18next.t("profile.delete.title"),
Message: i18next.t("profile.delete.message", { name }),
Buttons: [
@@ -89,7 +89,7 @@ export function ProfilesTab() {
await addProfile(name);
await switchProfile(name);
} catch (e) {
await Dialogs.Error({
await errorDialog({
Title: i18next.t("profile.error.createTitle"),
Message: formatErrorMessage(e),
});
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useSearchParams } from "react-router-dom";
import { Dialogs } from "@wailsio/runtime";
import { errorDialog } from "@/lib/dialogs.ts";
import { ClockIcon } from "lucide-react";
import { Button } from "@/components/buttons/Button";
import { ConfirmDialog } from "@/components/dialog/ConfirmDialog";
@@ -89,7 +89,7 @@ export default function SessionAboutToExpireDialog() {
}
WindowManager.CloseSessionAboutToExpire().catch(console.error);
} catch (e) {
await Dialogs.Error({
await errorDialog({
Title: t("sessionAboutToExpire.extendFailedTitle"),
Message: formatErrorMessage(e),
});
@@ -110,7 +110,7 @@ export default function SessionAboutToExpireDialog() {
});
WindowManager.CloseSessionAboutToExpire().catch(console.error);
} catch (e) {
await Dialogs.Error({
await errorDialog({
Title: t("sessionAboutToExpire.logoutFailedTitle"),
Message: formatErrorMessage(e),
});