- uninstaller: taskkill only the first, digits-only postmaster.pid line (the for /f loop ran taskkill on the port/epoch lines — potential unrelated-process kill) - git-missing dialogs use new ConfirmOptions.alwaysAsk so global skip-confirmations cannot silently pick an unseen choice - Windows quit prompt: embedded-local runtimes only, skipped during OS session end (sync dialog blocked Windows shutdown) - 'leave it running' detaches the embedded lifecycle (disarms its process shutdown hook) so Electron exit cannot kill the postmaster the operator chose to keep (new detachKeepingEmbedded) - wizard: ref-based double-submit guard around the async git preflight - clone route: ENOENT invalidate-and-retry matching runGitCommand - openExternalUrl: drop the async window.open fallback (always popup-blocked); log bridge failures instead - DirectoryPicker: close the panel when listing the created folder fails so Select cannot re-commit the parent - git status probe bounded to two spawns (PATH + first candidate) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
158 lines
5.1 KiB
TypeScript
158 lines
5.1 KiB
TypeScript
import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
|
import type { ReactNode } from "react";
|
|
import React from "react";
|
|
import { ConfirmDialog } from "../components/ConfirmDialog";
|
|
|
|
export interface ConfirmOptions {
|
|
title: string;
|
|
message: string;
|
|
/*
|
|
FNXC:ConfirmDialogs 2026-07-18-06:00:
|
|
Dialogs that GATE an action on an informed choice (e.g. "git is missing —
|
|
create without a repo?") must render even when the operator globally skips
|
|
critical-action confirmations: auto-resolving "primary" would silently pick
|
|
an option the operator never saw (review finding: skip-confirmations turned
|
|
the git-missing clone dialog into an endless silent browser-tab opener).
|
|
*/
|
|
alwaysAsk?: boolean;
|
|
confirmLabel?: string;
|
|
cancelLabel?: string;
|
|
danger?: boolean;
|
|
tertiaryLabel?: string;
|
|
tertiaryDanger?: boolean;
|
|
checkbox?: {
|
|
label: string;
|
|
description?: string;
|
|
defaultChecked?: boolean;
|
|
};
|
|
}
|
|
|
|
export type ConfirmChoice = "primary" | "tertiary" | "cancel";
|
|
|
|
interface PendingConfirm {
|
|
options: ConfirmOptions;
|
|
checkboxValue: boolean;
|
|
resolve: (value: { choice: ConfirmChoice; checkboxValue: boolean }) => void;
|
|
}
|
|
|
|
interface ConfirmContextValue {
|
|
confirm: (options: ConfirmOptions) => Promise<boolean>;
|
|
confirmWithChoice: (options: ConfirmOptions) => Promise<ConfirmChoice>;
|
|
confirmWithCheckbox: (options: ConfirmOptions) => Promise<{ choice: ConfirmChoice; checkboxValue: boolean }>;
|
|
}
|
|
|
|
const ConfirmContext = createContext<ConfirmContextValue | null>(null);
|
|
|
|
export function ConfirmDialogProvider({
|
|
children,
|
|
skipConfirmations = false,
|
|
}: {
|
|
children: ReactNode;
|
|
skipConfirmations?: boolean;
|
|
}) {
|
|
const [queue, setQueue] = useState<PendingConfirm[]>([]);
|
|
const queueRef = useRef<PendingConfirm[]>([]);
|
|
const skipConfirmationsRef = useRef(skipConfirmations);
|
|
skipConfirmationsRef.current = skipConfirmations;
|
|
|
|
const updateQueue = useCallback((updater: (current: PendingConfirm[]) => PendingConfirm[]) => {
|
|
setQueue((current) => {
|
|
const next = updater(current);
|
|
queueRef.current = next;
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const confirmWithCheckbox = useCallback((options: ConfirmOptions) => {
|
|
/*
|
|
FNXC:ConfirmDialogs 2026-07-16-05:30:
|
|
Operators who globally skip critical-action confirmations must receive the same primary/default result as clicking the dialog's primary button. Never invent a different outcome or enqueue a hidden dialog; checkbox prompts retain their configured default value.
|
|
*/
|
|
if (skipConfirmationsRef.current && !options.alwaysAsk) {
|
|
return Promise.resolve({
|
|
choice: "primary" as const,
|
|
checkboxValue: options.checkbox?.defaultChecked ?? false,
|
|
});
|
|
}
|
|
|
|
return new Promise<{ choice: ConfirmChoice; checkboxValue: boolean }>((resolve) => {
|
|
updateQueue((current) => [
|
|
...current,
|
|
{
|
|
options,
|
|
checkboxValue: options.checkbox?.defaultChecked ?? false,
|
|
resolve,
|
|
},
|
|
]);
|
|
});
|
|
}, [updateQueue]);
|
|
|
|
const confirmWithChoice = useCallback(async (options: ConfirmOptions) => {
|
|
const { choice } = await confirmWithCheckbox(options);
|
|
return choice;
|
|
}, [confirmWithCheckbox]);
|
|
|
|
const confirm = useCallback(async (options: ConfirmOptions) => {
|
|
const choice = await confirmWithChoice(options);
|
|
return choice === "primary";
|
|
}, [confirmWithChoice]);
|
|
|
|
const resolveCurrent = useCallback((value: ConfirmChoice) => {
|
|
const current = queueRef.current[0];
|
|
if (!current) {
|
|
return;
|
|
}
|
|
|
|
current.resolve({ choice: value, checkboxValue: current.checkboxValue });
|
|
updateQueue((items) => items.slice(1));
|
|
}, [updateQueue]);
|
|
|
|
const active = queue[0] ?? null;
|
|
|
|
const contextValue = useMemo<ConfirmContextValue>(
|
|
() => ({ confirm, confirmWithChoice, confirmWithCheckbox }),
|
|
[confirm, confirmWithCheckbox, confirmWithChoice]
|
|
);
|
|
|
|
return React.createElement(
|
|
ConfirmContext.Provider,
|
|
{ value: contextValue },
|
|
children,
|
|
React.createElement(ConfirmDialog, {
|
|
isOpen: active !== null,
|
|
options: active?.options ?? null,
|
|
onConfirm: () => resolveCurrent("primary"),
|
|
onTertiary: () => resolveCurrent("tertiary"),
|
|
onCancel: () => resolveCurrent("cancel"),
|
|
checkboxLabel: active?.options.checkbox?.label,
|
|
checkboxDescription: active?.options.checkbox?.description,
|
|
checkboxChecked: active?.checkboxValue ?? false,
|
|
onCheckboxChange: (next) => {
|
|
updateQueue((current) => {
|
|
if (current.length === 0) {
|
|
return current;
|
|
}
|
|
const [head, ...tail] = current;
|
|
return [{ ...head, checkboxValue: next }, ...tail];
|
|
});
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
export function useConfirm(): ConfirmContextValue {
|
|
const context = useContext(ConfirmContext);
|
|
if (context) {
|
|
return context;
|
|
}
|
|
|
|
return {
|
|
confirm: async (_options: ConfirmOptions) => false,
|
|
confirmWithChoice: async (_options: ConfirmOptions) => "cancel",
|
|
confirmWithCheckbox: async (options: ConfirmOptions) => ({
|
|
choice: "cancel",
|
|
checkboxValue: options.checkbox?.defaultChecked ?? false,
|
|
}),
|
|
};
|
|
}
|