diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 04b35fd4551..0389eeac3d9 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -216,6 +216,7 @@ export function collectUserMessageBlobPreviewUrls(message: ChatMessage): string[ } export interface PullRequestDialogState { + open: boolean; initialReference: string | null; key: number; } diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 2b9eda1a787..1d0c2fda0eb 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1278,7 +1278,12 @@ function ChatViewContent(props: ChatViewProps) { const localComposerRef = useRef(null); const composerRef = useComposerHandleContext() ?? localComposerRef; const [showScrollToBottom, setShowScrollToBottom] = useState(false); - const [expandedImage, setExpandedImage] = useState(null); + const [expandedImageDialog, setExpandedImageDialog] = useState<{ + readonly open: boolean; + readonly preview: ExpandedImagePreview; + readonly generation: number; + } | null>(null); + const expandedImage = expandedImageDialog?.preview ?? null; const [optimisticUserMessages, setOptimisticUserMessages] = useState([]); const optimisticUserMessagesRef = useRef(optimisticUserMessages); optimisticUserMessagesRef.current = optimisticUserMessages; @@ -1742,6 +1747,7 @@ function ChatViewContent(props: ChatViewProps) { return; } setPullRequestDialogState({ + open: true, initialReference: reference ?? null, key: Date.now(), }); @@ -1750,7 +1756,7 @@ function ChatViewContent(props: ChatViewProps) { ); const closePullRequestDialog = useCallback(() => { - setPullRequestDialogState(null); + setPullRequestDialogState((current) => (current ? { ...current, open: false } : current)); }, []); const openOrReuseProjectDraftThread = useCallback( @@ -3908,11 +3914,11 @@ function ChatViewContent(props: ChatViewProps) { return []; }); resetLocalDispatch(); - setExpandedImage(null); + setExpandedImageDialog(null); }, [draftId, resetLocalDispatch, threadId]); const closeExpandedImage = useCallback(() => { - setExpandedImage(null); + setExpandedImageDialog((current) => (current ? { ...current, open: false } : current)); }, []); const activeWorktreePath = activeThread?.worktreePath ?? null; @@ -5600,7 +5606,11 @@ function ChatViewContent(props: ChatViewProps) { }; const onExpandTimelineImage = useCallback((preview: ExpandedImagePreview) => { - setExpandedImage(preview); + setExpandedImageDialog((current) => ({ + open: true, + preview, + generation: (current?.generation ?? 0) + 1, + })); }, []); const onOpenTurnDiff = useCallback( (turnId: TurnId, filePath?: string) => { @@ -6077,7 +6087,7 @@ function ChatViewContent(props: ChatViewProps) { {pullRequestDialogState ? ( { + if (!open) setPullRequestDialogState(null); + }} onPrepared={handlePreparedPullRequestThread} /> ) : null} @@ -6141,8 +6154,11 @@ function ChatViewContent(props: ChatViewProps) { {rightPanelContent} ) : null} - {shouldUsePlanSidebarSheet && rightPanelOpen && activeThreadRef ? ( - + {shouldUsePlanSidebarSheet && activeThreadRef ? ( + ) : null} - {expandedImage && ( + {expandedImageDialog ? ( { + if (!open) closeExpandedImage(); + }} + onOpenChangeComplete={(open) => { + if (!open) setExpandedImageDialog(null); + }} /> - )} + ) : null} ); } diff --git a/apps/web/src/components/CommandPalette.logic.test.ts b/apps/web/src/components/CommandPalette.logic.test.ts index 4d591500f5c..18ece1e7727 100644 --- a/apps/web/src/components/CommandPalette.logic.test.ts +++ b/apps/web/src/components/CommandPalette.logic.test.ts @@ -28,7 +28,7 @@ describe("reduceCommandPaletteUiState", () => { expect( reduceCommandPaletteUiState(contentOpen, { _tag: "ToggleMode", mode: "content" }), - ).toEqual({ open: false, mode: "command", openIntent: null }); + ).toEqual({ open: false, mode: "content", openIntent: null }); }); it("switches between open modes without closing", () => { @@ -62,7 +62,7 @@ describe("reduceCommandPaletteUiState", () => { }); }); - it("resets to command mode for dialog-driven opens and closes", () => { + it("preserves the mode on close and resets it on open", () => { const filesOpen = reduceCommandPaletteUiState(closedState, { _tag: "ToggleMode", mode: "files", @@ -70,7 +70,7 @@ describe("reduceCommandPaletteUiState", () => { expect(reduceCommandPaletteUiState(filesOpen, { _tag: "SetOpen", open: false })).toEqual({ open: false, - mode: "command", + mode: "files", openIntent: null, }); expect(reduceCommandPaletteUiState(filesOpen, { _tag: "SetOpen", open: true })).toEqual({ diff --git a/apps/web/src/components/CommandPalette.logic.ts b/apps/web/src/components/CommandPalette.logic.ts index eee6ba5886e..fddeb87f6d1 100644 --- a/apps/web/src/components/CommandPalette.logic.ts +++ b/apps/web/src/components/CommandPalette.logic.ts @@ -46,14 +46,12 @@ export function reduceCommandPaletteUiState( ): CommandPaletteUiState { switch (action._tag) { case "SetOpen": - return { - open: action.open, - mode: "command", - openIntent: action.open ? state.openIntent : null, - }; + return action.open + ? { open: true, mode: "command", openIntent: state.openIntent } + : { ...state, open: false, openIntent: null }; case "ToggleMode": return state.open && state.mode === action.mode - ? { open: false, mode: "command", openIntent: null } + ? { ...state, open: false, openIntent: null } : { open: true, mode: action.mode, openIntent: null }; case "OpenAddProject": return { open: true, mode: "command", openIntent: { kind: "add-project" } }; diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 853d317655a..eae5d4d94a5 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -366,13 +366,23 @@ export function CommandPalette({ children }: { children: ReactNode }) { mode: "command", openIntent: null, }); - const setOpen = useCallback((open: boolean) => dispatch({ _tag: "SetOpen", open }), []); - const toggleMode = useCallback( - (mode: SearchOverlayMode) => dispatch({ _tag: "ToggleMode", mode }), - [], - ); - const openAddProject = useCallback(() => dispatch({ _tag: "OpenAddProject" }), []); - const openNewThreadIn = useCallback(() => dispatch({ _tag: "OpenNewThreadIn" }), []); + const [dialogContentMounted, setDialogContentMounted] = useState(false); + const setOpen = useCallback((open: boolean) => { + if (open) setDialogContentMounted(true); + dispatch({ _tag: "SetOpen", open }); + }, []); + const toggleMode = useCallback((mode: SearchOverlayMode) => { + setDialogContentMounted(true); + dispatch({ _tag: "ToggleMode", mode }); + }, []); + const openAddProject = useCallback(() => { + setDialogContentMounted(true); + dispatch({ _tag: "OpenAddProject" }); + }, []); + const openNewThreadIn = useCallback(() => { + setDialogContentMounted(true); + dispatch({ _tag: "OpenNewThreadIn" }); + }, []); const clearOpenIntent = useCallback(() => dispatch({ _tag: "ClearOpenIntent" }), []); const keybindings = useAtomValue(primaryServerKeybindingsAtom); const composerHandleRef = useRef(null); @@ -455,23 +465,26 @@ export function CommandPalette({ children }: { children: ReactNode }) { } setOpen(open); }} + onOpenChangeComplete={(open) => { + if (!open) setDialogContentMounted(false); + }} > {children} - + {dialogContentMounted ? ( + + ) : null} ); } function CommandPaletteDialog(props: { - readonly open: boolean; readonly mode: SearchOverlayMode; readonly openIntent: CommandPaletteOpenIntent | null; readonly setOpen: (open: boolean) => void; @@ -480,10 +493,6 @@ function CommandPaletteDialog(props: { }) { const composerHandleRef = useComposerHandleContext(); - if (!props.open) { - return null; - } - return ( { props.onOpenChange(open); - if (!open) { - resetState(); - } }, - [props, resetState], + [props.onOpenChange], ); const openSourceControlSettings = useCallback(() => { @@ -543,7 +541,13 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { }, [handleOpenChange, navigate]); return ( - + { + if (!open) resetState(); + }} + >
@@ -1277,6 +1281,7 @@ export default function GitActionsControl({ return; } setPendingDefaultBranchAction({ + open: true, action, branchName: actionBranch, includesCommit, @@ -1480,9 +1485,9 @@ export default function GitActionsControl({ ); const continuePendingDefaultBranchAction = () => { - if (!pendingDefaultBranchAction) return; + if (!pendingDefaultBranchAction?.open) return; const { action, commitMessage, onConfirmed, filePaths } = pendingDefaultBranchAction; - setPendingDefaultBranchAction(null); + setPendingDefaultBranchAction({ ...pendingDefaultBranchAction, open: false }); void runGitActionWithToast({ action, ...(commitMessage ? { commitMessage } : {}), @@ -1493,9 +1498,9 @@ export default function GitActionsControl({ }; const checkoutFeatureBranchAndContinuePendingAction = () => { - if (!pendingDefaultBranchAction) return; + if (!pendingDefaultBranchAction?.open) return; const { action, commitMessage, onConfirmed, filePaths } = pendingDefaultBranchAction; - setPendingDefaultBranchAction(null); + setPendingDefaultBranchAction({ ...pendingDefaultBranchAction, open: false }); void runGitActionWithToast({ action, ...(commitMessage ? { commitMessage } : {}), @@ -1511,9 +1516,6 @@ export default function GitActionsControl({ const commitMessage = dialogCommitMessage.trim(); setIsCommitDialogOpen(false); - setDialogCommitMessage(""); - setExcludedFiles(new Set()); - setIsEditingFiles(false); void runGitActionWithToast({ action: "commit", @@ -1610,9 +1612,6 @@ export default function GitActionsControl({ if (!isCommitDialogOpen) return; const commitMessage = dialogCommitMessage.trim(); setIsCommitDialogOpen(false); - setDialogCommitMessage(""); - setExcludedFiles(new Set()); - setIsEditingFiles(false); void runGitActionWithToast({ action: "commit", ...(commitMessage ? { commitMessage } : {}), @@ -1818,13 +1817,12 @@ export default function GitActionsControl({ { - if (!open) { - setIsCommitDialogOpen(false); - setDialogCommitMessage(""); - setExcludedFiles(new Set()); - setIsEditingFiles(false); - } + onOpenChange={setIsCommitDialogOpen} + onOpenChangeComplete={(open) => { + if (open) return; + setDialogCommitMessage(""); + setExcludedFiles(new Set()); + setIsEditingFiles(false); }} > @@ -1956,16 +1954,7 @@ export default function GitActionsControl({
- diff --git a/apps/web/src/components/PullRequestThreadDialog.tsx b/apps/web/src/components/PullRequestThreadDialog.tsx index 4004b4930c2..1e08535df84 100644 --- a/apps/web/src/components/PullRequestThreadDialog.tsx +++ b/apps/web/src/components/PullRequestThreadDialog.tsx @@ -33,6 +33,7 @@ interface PullRequestThreadDialogProps { cwd: string | null; initialReference: string | null; onOpenChange: (open: boolean) => void; + onOpenChangeComplete: (open: boolean) => void; onPrepared: (input: { branch: string; worktreePath: string | null }) => Promise | void; } @@ -43,6 +44,7 @@ export function PullRequestThreadDialog({ cwd, initialReference, onOpenChange, + onOpenChangeComplete, onPrepared, }: PullRequestThreadDialogProps) { const referenceInputRef = useRef(null); @@ -193,6 +195,7 @@ export function PullRequestThreadDialog({ onOpenChange(nextOpen); } }} + onOpenChangeComplete={onOpenChangeComplete} > diff --git a/apps/web/src/components/RightPanelSheet.tsx b/apps/web/src/components/RightPanelSheet.tsx index ebc4aa0a698..f3c697a7c3a 100644 --- a/apps/web/src/components/RightPanelSheet.tsx +++ b/apps/web/src/components/RightPanelSheet.tsx @@ -17,12 +17,7 @@ export function RightPanelSheet(props: { } }} > - + {props.children} diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index cffab8bd577..ef4d76df467 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1197,12 +1197,17 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec const [renamingThreadKey, setRenamingThreadKey] = useState(null); const [renamingTitle, setRenamingTitle] = useState(""); const [confirmingArchiveThreadKey, setConfirmingArchiveThreadKey] = useState(null); - const [projectRenameTarget, setProjectRenameTarget] = useState( - null, - ); + const [projectRenameDialog, setProjectRenameDialog] = useState<{ + readonly open: boolean; + readonly target: SidebarProjectGroupMember; + } | null>(null); + const projectRenameTarget = projectRenameDialog?.target ?? null; const [projectRenameTitle, setProjectRenameTitle] = useState(""); - const [projectGroupingTarget, setProjectGroupingTarget] = - useState(null); + const [projectGroupingDialog, setProjectGroupingDialog] = useState<{ + readonly open: boolean; + readonly target: SidebarProjectGroupMember; + } | null>(null); + const projectGroupingTarget = projectGroupingDialog?.target ?? null; const [projectGroupingSelection, setProjectGroupingSelection] = useState< SidebarProjectGroupingMode | "inherit" >("inherit"); @@ -1409,14 +1414,14 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec ); const openProjectRenameDialog = useCallback((member: SidebarProjectGroupMember) => { - setProjectRenameTarget(member); + setProjectRenameDialog({ open: true, target: member }); setProjectRenameTitle(member.title); }, []); const openProjectGroupingDialog = useCallback( (member: SidebarProjectGroupMember) => { const overrideKey = deriveProjectGroupingOverrideKey(member); - setProjectGroupingTarget(member); + setProjectGroupingDialog({ open: true, target: member }); setProjectGroupingSelection( projectGroupingSettings.sidebarProjectGroupingOverrides?.[overrideKey] ?? "inherit", ); @@ -2024,8 +2029,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec ); const closeProjectRenameDialog = useCallback(() => { - setProjectRenameTarget(null); - setProjectRenameTitle(""); + setProjectRenameDialog((current) => (current ? { ...current, open: false } : current)); }, []); const submitProjectRename = useCallback(async () => { @@ -2069,8 +2073,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec }, [closeProjectRenameDialog, projectRenameTarget, projectRenameTitle, updateProject]); const closeProjectGroupingDialog = useCallback(() => { - setProjectGroupingTarget(null); - setProjectGroupingSelection("inherit"); + setProjectGroupingDialog((current) => (current ? { ...current, open: false } : current)); }, []); const saveProjectGroupingPreference = useCallback(() => { @@ -2358,12 +2361,17 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec /> { if (!open) { closeProjectRenameDialog(); } }} + onOpenChangeComplete={(open) => { + if (open) return; + setProjectRenameDialog(null); + setProjectRenameTitle(""); + }} > @@ -2405,12 +2413,17 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec { if (!open) { closeProjectGroupingDialog(); } }} + onOpenChangeComplete={(open) => { + if (open) return; + setProjectGroupingDialog(null); + setProjectGroupingSelection("inherit"); + }} > diff --git a/apps/web/src/components/SidebarV2.tsx b/apps/web/src/components/SidebarV2.tsx index ee73b570514..3368cfa3c90 100644 --- a/apps/web/src/components/SidebarV2.tsx +++ b/apps/web/src/components/SidebarV2.tsx @@ -1228,9 +1228,14 @@ export default function SidebarV2() { ); }, }); - const [projectActionsTarget, setProjectActionsTarget] = useState( - null, - ); + const [projectActionsDialog, setProjectActionsDialog] = useState<{ + readonly open: boolean; + readonly target: SidebarProjectSnapshot; + } | null>(null); + const projectActionsTarget = projectActionsDialog?.target ?? null; + const closeProjectActionsDialog = useCallback(() => { + setProjectActionsDialog((current) => (current ? { ...current, open: false } : current)); + }, []); const [projectScopeMenuOpen, setProjectScopeMenuOpen] = useState(false); const newThreadContext = useHandleNewThread(); const openAddProjectCommandPalette = useCallback( @@ -1547,7 +1552,9 @@ export default function SidebarV2() { event.preventDefault(); event.stopPropagation(); setProjectScopeMenuOpen(false); - window.requestAnimationFrame(() => setProjectActionsTarget(projectGroup)); + window.requestAnimationFrame(() => + setProjectActionsDialog({ open: true, target: projectGroup }), + ); }, [], ); @@ -3023,9 +3030,12 @@ export default function SidebarV2() { { - if (!open) setProjectActionsTarget(null); + if (!open) closeProjectActionsDialog(); + }} + onOpenChangeComplete={(open) => { + if (!open) setProjectActionsDialog(null); }} > @@ -3145,7 +3155,7 @@ export default function SidebarV2() { className="text-destructive-foreground hover:bg-destructive/8 hover:text-destructive-foreground" onClick={() => { const projectGroup = projectActionsTarget; - setProjectActionsTarget(null); + closeProjectActionsDialog(); void handleRemoveProjectMembers(projectGroup, [member]); }} > @@ -3173,7 +3183,7 @@ export default function SidebarV2() { className="shrink-0" onClick={() => { const projectGroup = projectActionsTarget; - setProjectActionsTarget(null); + closeProjectActionsDialog(); void handleRemoveProjectMembers(projectGroup, projectGroup.memberProjects); }} > @@ -3194,7 +3204,7 @@ export default function SidebarV2() { variant="destructive-outline" onClick={() => { const projectGroup = projectActionsTarget; - setProjectActionsTarget(null); + closeProjectActionsDialog(); void handleRemoveProjectMembers(projectGroup, projectGroup.memberProjects); }} > @@ -3202,7 +3212,7 @@ export default function SidebarV2() { Remove project ) : null} - +
diff --git a/apps/web/src/components/chat/ExpandedImageDialog.tsx b/apps/web/src/components/chat/ExpandedImageDialog.tsx index fd14c68b0c4..ebe5e83dc54 100644 --- a/apps/web/src/components/chat/ExpandedImageDialog.tsx +++ b/apps/web/src/components/chat/ExpandedImageDialog.tsx @@ -1,32 +1,35 @@ +import { Dialog } from "@base-ui/react/dialog"; import { memo, useCallback, useEffect, useState } from "react"; import { ChevronLeftIcon, ChevronRightIcon, XIcon } from "lucide-react"; import { Button } from "../ui/button"; import type { ExpandedImagePreview } from "./ExpandedImagePreview"; interface ExpandedImageDialogProps { - preview: ExpandedImagePreview; - onClose: () => void; + preview: ExpandedImagePreview | null; + open: boolean; + onOpenChange: (open: boolean) => void; + onOpenChangeComplete: (open: boolean) => void; } export const ExpandedImageDialog = memo(function ExpandedImageDialog({ preview, - onClose, + open, + onOpenChange, + onOpenChangeComplete, }: ExpandedImageDialogProps) { const [imageOffset, setImageOffset] = useState(0); - const index = (preview.index + imageOffset + preview.images.length) % preview.images.length; + const imageCount = preview?.images.length ?? 0; + const index = + preview && imageCount > 0 ? (preview.index + imageOffset + imageCount) % imageCount : 0; const navigateImage = useCallback((direction: -1 | 1) => { setImageOffset((current) => current + direction); }, []); useEffect(() => { + if (!open || !preview) return; + const onKeyDown = (event: globalThis.KeyboardEvent) => { - if (event.key === "Escape") { - event.preventDefault(); - event.stopPropagation(); - onClose(); - return; - } if (preview.images.length <= 1) return; if (event.key === "ArrowLeft") { event.preventDefault(); @@ -41,70 +44,84 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); - }, [navigateImage, onClose, preview.images.length]); + }, [navigateImage, open, preview]); - const item = preview.images[index]; - if (!item) return null; + const item = preview?.images[index]; return ( -
{ + if (!nextOpen) setImageOffset(0); + onOpenChangeComplete(nextOpen); + }} > - - )} -
- - {item.name} -

- {item.name} - {preview.images.length > 1 ? ` (${index + 1}/${preview.images.length})` : ""} -

-
- {preview.images.length > 1 && ( - - )} -
+ {item && preview ? ( + + + + + Expanded image preview + + )} +
+ + {item.name} +

+ {item.name} + {preview.images.length > 1 ? ` (${index + 1}/${preview.images.length})` : ""} +

+
+ {preview.images.length > 1 && ( + + )} +
+
+
+ ) : null} + ); }); diff --git a/apps/web/src/components/desktop/SshPasswordPromptDialog.tsx b/apps/web/src/components/desktop/SshPasswordPromptDialog.tsx index a6ff9d8c4ec..7aec840b53d 100644 --- a/apps/web/src/components/desktop/SshPasswordPromptDialog.tsx +++ b/apps/web/src/components/desktop/SshPasswordPromptDialog.tsx @@ -68,6 +68,7 @@ function ActiveSshPasswordPrompt({ readonly onRemove: (requestId: string) => void; }) { const [password, setPassword] = useState(""); + const [open, setOpen] = useState(false); const [isResponding, setIsResponding] = useState(false); const [now, setNow] = useState(() => Date.now()); const [responseError, setResponseError] = useState(null); @@ -77,6 +78,7 @@ function ActiveSshPasswordPrompt({ useEffect(() => { const frame = window.requestAnimationFrame(() => { + setOpen(true); inputRef.current?.focus(); inputRef.current?.select(); }); @@ -120,10 +122,10 @@ function ActiveSshPasswordPrompt({ setResponseError(null); try { await window.desktopBridge?.resolveSshPasswordPrompt(requestId, nextPassword); - onRemove(requestId); + setOpen(false); } catch (error) { if (nextPassword === null) { - onRemove(requestId); + setOpen(false); } else { setResponseError(getPromptErrorMessage(error)); } @@ -134,7 +136,7 @@ function ActiveSshPasswordPrompt({ }; const dismissExpiredPrompt = () => { - onRemove(request.requestId); + setOpen(false); }; const cancelPrompt = () => { @@ -142,6 +144,7 @@ function ActiveSshPasswordPrompt({ dismissExpiredPrompt(); return; } + setOpen(false); void respond(null); }; @@ -149,12 +152,15 @@ function ActiveSshPasswordPrompt({ return ( { if (!open) { cancelPrompt(); } }} + onOpenChangeComplete={(open) => { + if (!open) onRemove(request.requestId); + }} > diff --git a/apps/web/src/components/settings/AddProviderInstanceDialog.tsx b/apps/web/src/components/settings/AddProviderInstanceDialog.tsx index a6da37c1551..1ccc189b43d 100644 --- a/apps/web/src/components/settings/AddProviderInstanceDialog.tsx +++ b/apps/web/src/components/settings/AddProviderInstanceDialog.tsx @@ -221,7 +221,20 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns }; return ( - + { + if (nextOpen) return; + setWizardStep(0); + setDriver(DEFAULT_DRIVER_KIND); + setLabel(""); + setAccentColor(""); + setInstanceIdOverride(null); + setConfigByDriver({}); + setHasAttemptedSubmit(false); + }} + >
diff --git a/apps/web/src/components/settings/ConnectionsSettings.tsx b/apps/web/src/components/settings/ConnectionsSettings.tsx index 640412e4a6d..c1c44f20d6d 100644 --- a/apps/web/src/components/settings/ConnectionsSettings.tsx +++ b/apps/web/src/components/settings/ConnectionsSettings.tsx @@ -993,8 +993,6 @@ const AuthorizedClientsHeaderAction = memo(function AuthorizedClientsHeaderActio setIsCreatingPairingLink(true); try { await createServerPairingCredential({ label: pairingLabel, scopes: pairingScopes }); - setPairingLabel(""); - setPairingScopes([...AuthStandardClientScopes]); setDialogOpen(false); } catch (error) { const message = error instanceof Error ? error.message : "Failed to create pairing URL."; @@ -1030,12 +1028,11 @@ const AuthorizedClientsHeaderAction = memo(function AuthorizedClientsHeaderActio { - setDialogOpen(open); - if (!open) { - setPairingLabel(""); - setPairingScopes([...AuthStandardClientScopes]); - } + onOpenChange={setDialogOpen} + onOpenChangeComplete={(open) => { + if (open) return; + setPairingLabel(""); + setPairingScopes([...AuthStandardClientScopes]); }} > (null); - const isWslConfirmDialogOpen = pendingWslChange !== null; - const [pendingTailscaleServeEndpoint, setPendingTailscaleServeEndpoint] = - useState(null); + const isWslConfirmDialogOpen = pendingWslChange?.open ?? false; + const [pendingTailscaleServeSetup, setPendingTailscaleServeSetup] = useState<{ + readonly open: boolean; + readonly endpoint: AdvertisedEndpoint; + } | null>(null); + const pendingTailscaleServeEndpoint = pendingTailscaleServeSetup?.endpoint ?? null; const [disableTailscaleServeDialogOpen, setDisableTailscaleServeDialogOpen] = useState(false); const [tailscaleServePortInput, setTailscaleServePortInput] = useState( String(DEFAULT_TAILSCALE_SERVE_PORT), @@ -1997,7 +1998,7 @@ export function ConnectionsSettings() { port: parsedTailscaleServePort, }); refreshDesktopNetworkAccessState(); - setPendingTailscaleServeEndpoint(null); + setPendingTailscaleServeSetup((current) => (current ? { ...current, open: false } : current)); } catch (error) { const message = error instanceof Error ? error.message : "Failed to configure Tailscale HTTPS."; @@ -2019,7 +2020,7 @@ export function ConnectionsSettings() { setTailscaleServePortInput( String(desktopServerExposureState?.tailscaleServePort ?? DEFAULT_TAILSCALE_SERVE_PORT), ); - setPendingTailscaleServeEndpoint(endpoint); + setPendingTailscaleServeSetup({ open: true, endpoint }); }, [desktopServerExposureState?.tailscaleServePort], ); @@ -2649,7 +2650,7 @@ export function ConnectionsSettings() { // on (turning the only running backend off needs to switch // back to Windows and restart — always consequential). if (hasWslRegistrationToLose || wasWslOnly) { - setPendingWslChange({ kind: "disable", wasWslOnly }); + setPendingWslChange({ open: true, kind: "disable", wasWslOnly }); return; } void applyWslSettingChange(() => desktopBridge.setWslBackendEnabled(false)); @@ -2662,7 +2663,7 @@ export function ConnectionsSettings() { // backends or only WSL. We always ask here so the user picks // the mode upfront instead of having to discover the wsl-only // switch afterwards. - setPendingWslChange({ kind: "enable", nextDistro }); + setPendingWslChange({ open: true, kind: "enable", nextDistro }); return; } // Already enabled — treat as a distro switch. Skip the change if @@ -2674,7 +2675,7 @@ export function ConnectionsSettings() { // the app (the IPC handler does this) rather than swapping a secondary, // and the user should see that coming. if (hasWslRegistrationToLose || desktopWslState.wslOnly) { - setPendingWslChange({ kind: "distro", nextDistro }); + setPendingWslChange({ open: true, kind: "distro", nextDistro }); return; } void applyWslSettingChange(() => desktopBridge.setWslDistro(nextDistro)); @@ -2687,7 +2688,7 @@ export function ConnectionsSettings() { (mode: "both" | "wsl-only") => { if (!desktopBridge || !pendingWslChange || pendingWslChange.kind !== "enable") return; const nextDistro = pendingWslChange.nextDistro; - setPendingWslChange(null); + setPendingWslChange({ ...pendingWslChange, open: false }); const persistedDistro = desktopWslState?.distro ?? null; void applyWslSettingChange(() => applyWslEnableSelection({ @@ -2710,7 +2711,7 @@ export function ConnectionsSettings() { // anything itself; the renderer warns the user to expect a // restart and (in a follow-up) can trigger it automatically. // Always prompt — even enabling is consequential here. - setPendingWslChange({ kind: "wsl-only", nextValue: enabled }); + setPendingWslChange({ open: true, kind: "wsl-only", nextValue: enabled }); }, [desktopBridge, desktopWslState], ); @@ -2721,7 +2722,7 @@ export function ConnectionsSettings() { // The enable kind resolves through handleConfirmEnableWsl, not // this single Confirm path. if (change.kind === "enable") return; - setPendingWslChange(null); + setPendingWslChange({ ...pendingWslChange, open: false }); if (change.kind === "disable") { void applyWslSettingChange(async () => { const next = await desktopBridge.setWslBackendEnabled(false); @@ -3132,6 +3133,11 @@ export function ConnectionsSettings() { open={isWslConfirmDialogOpen} onOpenChange={(open) => { if (isUpdatingWslBackend) return; + if (!open) { + setPendingWslChange((current) => (current ? { ...current, open: false } : current)); + } + }} + onOpenChangeComplete={(open) => { if (!open) setPendingWslChange(null); }} > @@ -3275,10 +3281,17 @@ export function ConnectionsSettings() { { if (isUpdatingTailscaleServe) return; - if (!open) setPendingTailscaleServeEndpoint(null); + if (!open) { + setPendingTailscaleServeSetup((current) => + current ? { ...current, open: false } : current, + ); + } + }} + onOpenChangeComplete={(open) => { + if (!open) setPendingTailscaleServeSetup(null); }} > diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 31ac4bba66e..767e300d04d 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -2199,9 +2199,10 @@ export function ProviderSettingsPanel() { })} - {isAddInstanceDialogOpen ? ( - - ) : null} + ); }