From c056644fa55642c9ac08cc8cff487448af33be55 Mon Sep 17 00:00:00 2001 From: loosand Date: Thu, 13 Aug 2026 11:44:56 +0800 Subject: [PATCH 1/5] refactor(ui): replace modal implementation with Dialog component in RemotePathPickerModal and GitBranchSelector --- .../src/components/RemotePathPickerModal.tsx | 95 ++--- .../src/components/git/GitBranchSelector.tsx | 355 +++++++++--------- crates/agent-ui/src/components/ui/dialog.tsx | 119 ++++++ 3 files changed, 349 insertions(+), 220 deletions(-) create mode 100644 crates/agent-ui/src/components/ui/dialog.tsx diff --git a/crates/agent-gateway/web/src/components/RemotePathPickerModal.tsx b/crates/agent-gateway/web/src/components/RemotePathPickerModal.tsx index f9b84c4ad..cc58f9eb6 100644 --- a/crates/agent-gateway/web/src/components/RemotePathPickerModal.tsx +++ b/crates/agent-gateway/web/src/components/RemotePathPickerModal.tsx @@ -10,9 +10,16 @@ import { X, } from "@liveagent/ui/components/IconSet"; import { Button } from "@liveagent/ui/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogTitle, +} from "@liveagent/ui/components/ui/dialog"; import { Input } from "@liveagent/ui/components/ui/input"; import { useLocale } from "@liveagent/ui/i18n/index"; -import { useModalMotion } from "@liveagent/ui/lib/shared/modalMotion"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { IndividualTreeViewState, @@ -21,7 +28,6 @@ import type { TreeViewState, } from "react-complex-tree"; import { ControlledTreeEnvironment, Tree } from "react-complex-tree"; -import { createPortal } from "react-dom"; import type { RemoteFsRoot } from "./remotePathPickerPaths"; import { basenameFromPath, @@ -147,7 +153,6 @@ export function RemotePathPickerModal(props: RemotePathPickerModalProps) { const [creatingFolder, setCreatingFolder] = useState(false); const [createFolderError, setCreateFolderError] = useState(null); const didExpandInitialPathRef = useRef(false); - const { modalState, requestClose } = useModalMotion(onClose); const modalTitle = title ?? (mode === "file" ? t("settings.filePickerTitle") : t("settings.workdirPickerTitle")); @@ -283,16 +288,6 @@ export function RemotePathPickerModal(props: RemotePathPickerModalProps) { }; }, []); - useEffect(() => { - function onKeyDown(event: KeyboardEvent) { - if (event.key !== "Escape") return; - event.preventDefault(); - requestClose(); - } - window.addEventListener("keydown", onKeyDown); - return () => window.removeEventListener("keydown", onKeyDown); - }, [requestClose]); - useEffect(() => { if (loadingRoots || didExpandInitialPathRef.current) return; @@ -520,31 +515,40 @@ export function RemotePathPickerModal(props: RemotePathPickerModalProps) { } } - const overlay = ( -
{ + if (!open) onClose(); + }} > -
- -
+
{mode === "file" ? : }
-

{modalTitle}

-

{modalDescription}

+ {modalTitle} + {modalDescription}
- +
@@ -681,26 +685,25 @@ export function RemotePathPickerModal(props: RemotePathPickerModalProps) { ) : null}
-
- - -
-
-
+ + + + ); - - return createPortal(overlay, document.body); } type PickPathOptions = { diff --git a/crates/agent-ui/src/components/git/GitBranchSelector.tsx b/crates/agent-ui/src/components/git/GitBranchSelector.tsx index e0b1a24eb..5d6e529b5 100644 --- a/crates/agent-ui/src/components/git/GitBranchSelector.tsx +++ b/crates/agent-ui/src/components/git/GitBranchSelector.tsx @@ -22,6 +22,13 @@ import { } from "@liveagent/ui/components/IconSet"; import { Button } from "@liveagent/ui/components/ui/button"; import { useConfirmDialog } from "@liveagent/ui/components/ui/confirm-dialog"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogTitle, +} from "@liveagent/ui/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, @@ -285,8 +292,8 @@ function GitInitModal(props: { ); } -// Worktree 创建弹窗:分支名、目录名与可选父目录分别传递,避免把 Git -// 引用命名规则与文件系统目录规则混为一谈。 +// 截图中的“新建 Worktree”模态框主体。分支名、目录名与可选父目录分别 +// 传递,避免把 Git 引用命名规则与文件系统目录规则混为一谈。 function WorktreeCreateModal(props: { open: boolean; repoRoot: string; @@ -325,13 +332,11 @@ function WorktreeCreateModal(props: { } = props; const { t } = useLocale(); const { pickDirectory, directoryPickerElement } = useDirectoryPicker(); - const titleId = useId(); const branchInputId = useId(); const directoryInputId = useId(); const parentInputId = useId(); - if (!open) return directoryPickerElement; - + // 截图红框中“选择…”按钮的处理函数:唤起系统目录选择器并回填保存位置。 async function chooseParentDirectory() { try { const selected = await pickDirectory(parentDirectory || repoRoot); @@ -343,194 +348,194 @@ function WorktreeCreateModal(props: { } return ( - <> - {createPortal( -
{ + if (!nextOpen && !loading) onClose(); + }} + > + +
{ + event.preventDefault(); + onSubmit(); + }} > -
- { - event.preventDefault(); - onSubmit(); - }} - > -
-
-
- -
-
-
- {t("git.branchSelector.createWorktreeTitle")} -
-
- {t("git.branchSelector.worktreeDescription")} -
-
+
+
+
+
-
+
+ + } + > + + +
+
+
+ +
+ {repoRoot} +
+
+
+ +
-
+
-
-
-
-
- - onBranchChange(event.target.value)} - className="h-9 text-sm" - placeholder={t("git.branchSelector.worktreeBranchPlaceholder")} - autoFocus - disabled={loading} - spellCheck={false} - autoCapitalize="none" - autoCorrect="off" - /> -
-
- - onDirectoryNameChange(event.target.value)} - className="h-9 text-sm" - placeholder={t("git.branchSelector.worktreeDirectoryPlaceholder")} - disabled={loading} - spellCheck={false} - autoCapitalize="none" - autoCorrect="off" - /> -
+ onDirectoryNameChange(event.target.value)} + className="h-9 text-sm" + placeholder={t("git.branchSelector.worktreeDirectoryPlaceholder")} + disabled={loading} + spellCheck={false} + autoCapitalize="none" + autoCorrect="off" + />
-
- -
- - {parentDirectory ? ( - - ) : null} +
+ {/* 截图中的“保存位置”区域。 */} +
+ +
+ + {parentDirectory ? ( -
-
-
- {parentDirectory - ? t("git.branchSelector.worktreeCustomLocationHint") - : t("git.branchSelector.worktreeLocationHint")} + ) : null} + {/* 截图红框中的“选择…”按钮。 */} +
- {error ? ( -
- {error} -
- ) : null}
-
- - +
+ {parentDirectory + ? t("git.branchSelector.worktreeCustomLocationHint") + : t("git.branchSelector.worktreeLocationHint")}
- -
, - document.body, - )} - {directoryPickerElement} - + {error ? ( +
+ {error} +
+ ) : null} +
+
+ } + > + {t("chat.cancel")} + + +
+ + {/* Web 端目录选择器是嵌套 Dialog;置于父 Popup 内交给 Base UI 管理层级与焦点。 */} + {directoryPickerElement} + + ); } @@ -1549,6 +1554,7 @@ export function GitBranchSelector(props: { [worktreeBranchDraft], ); + // 从分支选择菜单点击“新建 Worktree”后打开截图中的模态框。 const openWorktreeModal = useCallback(() => { if (!gitClient?.createWorktree) return; setWorktreeBranchDraft(""); @@ -2069,6 +2075,7 @@ export function GitBranchSelector(props: { onClose={resetBranchAction} /> {confirmDialog} + {/* “新建 Worktree”模态框的挂载位置。 */} {gitClient?.createWorktree ? ( , +) { + return ; +} + +export function DialogClose(props: React.ComponentPropsWithoutRef) { + return ; +} + +export const DialogOverlay = React.forwardRef< + HTMLDivElement, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = "DialogOverlay"; + +type DialogContentProps = React.ComponentPropsWithoutRef & { + overlayClassName?: string; + portalProps?: React.ComponentPropsWithoutRef; + viewportClassName?: string; +}; + +export const DialogContent = React.forwardRef( + ({ className, children, overlayClassName, portalProps, viewportClassName, ...props }, ref) => ( + + + + + {children} + + + + ), +); +DialogContent.displayName = "DialogContent"; + +export const DialogHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +DialogHeader.displayName = "DialogHeader"; + +export const DialogFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +DialogFooter.displayName = "DialogFooter"; + +export const DialogTitle = React.forwardRef< + HTMLHeadingElement, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = "DialogTitle"; + +export const DialogDescription = React.forwardRef< + HTMLParagraphElement, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = "DialogDescription"; + +export { DialogPrimitive }; From 58704388a56f6cf640df88d5f926c8204c6e82c2 Mon Sep 17 00:00:00 2001 From: loosand Date: Thu, 13 Aug 2026 11:46:20 +0800 Subject: [PATCH 2/5] style(ui): add isolation to #root for improved stacking context management --- crates/agent-ui/src/styles/base.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/agent-ui/src/styles/base.css b/crates/agent-ui/src/styles/base.css index b420c5884..281267ec4 100644 --- a/crates/agent-ui/src/styles/base.css +++ b/crates/agent-ui/src/styles/base.css @@ -166,6 +166,11 @@ user-select: none; } + /* Keep Base UI portals above page-local stacking contexts without z-index escalation. */ + #root { + isolation: isolate; + } + body { @apply bg-background text-foreground; font-family: var(--app-font-family); From 59f18a72a361de083e7bd82259c8109e3d3b4c7a Mon Sep 17 00:00:00 2001 From: loosand Date: Thu, 13 Aug 2026 11:48:39 +0800 Subject: [PATCH 3/5] refactor(ui): update comments in GitBranchSelector and WorktreeCreateModal for clarity and consistency --- crates/agent-ui/src/components/git/GitBranchSelector.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/crates/agent-ui/src/components/git/GitBranchSelector.tsx b/crates/agent-ui/src/components/git/GitBranchSelector.tsx index 5d6e529b5..b9b88dd53 100644 --- a/crates/agent-ui/src/components/git/GitBranchSelector.tsx +++ b/crates/agent-ui/src/components/git/GitBranchSelector.tsx @@ -292,8 +292,8 @@ function GitInitModal(props: { ); } -// 截图中的“新建 Worktree”模态框主体。分支名、目录名与可选父目录分别 -// 传递,避免把 Git 引用命名规则与文件系统目录规则混为一谈。 +// Worktree 创建弹窗:分支名、目录名与可选父目录分别传递,避免把 Git +// 引用命名规则与文件系统目录规则混为一谈。 function WorktreeCreateModal(props: { open: boolean; repoRoot: string; @@ -336,7 +336,6 @@ function WorktreeCreateModal(props: { const directoryInputId = useId(); const parentInputId = useId(); - // 截图红框中“选择…”按钮的处理函数:唤起系统目录选择器并回填保存位置。 async function chooseParentDirectory() { try { const selected = await pickDirectory(parentDirectory || repoRoot); @@ -459,7 +458,6 @@ function WorktreeCreateModal(props: { />
- {/* 截图中的“保存位置”区域。 */}
- -
-
- {error ? ( -
- - {error} +
+ {error ? ( +
+ + {error} +
+ ) : null} +
+ + onNameChange(event.currentTarget.value)} + /> +
+
+ + onAgentIdChange(event.currentTarget.value)} + />
- ) : null} -
- - onNameChange(event.currentTarget.value)} - /> -
-
- - onAgentIdChange(event.currentTarget.value)} - />
-
-
- - -
- -
, - document.body, +
+ + +
+ + + ); } @@ -510,14 +491,6 @@ function IssuedTokenDialog({ const { t } = useLocale(); const [copied, setCopied] = useState(false); - useEffect(() => { - function handleKeyDown(event: KeyboardEvent) { - if (event.key === "Escape") onClose(); - } - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [onClose]); - async function handleCopy() { if (!navigator.clipboard?.writeText) return; await navigator.clipboard.writeText(issuedToken.token); @@ -525,33 +498,19 @@ function IssuedTokenDialog({ window.setTimeout(() => setCopied(false), 1800); } - return createPortal( -
-
-
-
, - document.body, + + ); } diff --git a/crates/agent-gateway/web/src/shims/browserPathPrompt.tsx b/crates/agent-gateway/web/src/shims/browserPathPrompt.tsx new file mode 100644 index 000000000..637188f8f --- /dev/null +++ b/crates/agent-gateway/web/src/shims/browserPathPrompt.tsx @@ -0,0 +1,88 @@ +import { Button } from "@liveagent/ui/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@liveagent/ui/components/ui/dialog"; +import { Input } from "@liveagent/ui/components/ui/input"; +import { Label } from "@liveagent/ui/components/ui/label"; +import { useState } from "react"; +import { createRoot } from "react-dom/client"; + +type BrowserPathPromptOptions = { + title: string; + description: string; + label: string; + placeholder: string; + inputId: string; +}; + +function BrowserPathPromptDialog(props: { + options: BrowserPathPromptOptions; + onResolve: (value: string | null) => void; +}) { + const { options, onResolve } = props; + const [value, setValue] = useState(""); + + return ( + !open && onResolve(null)}> + +
{ + event.preventDefault(); + const path = value.trim(); + if (path) onResolve(path); + }} + > +
+ {options.title} + {options.description} +
+
+ + setValue(event.currentTarget.value)} + /> +
+
+ + +
+
+
+
+ ); +} + +export function promptPathInBrowser(options: BrowserPathPromptOptions): Promise { + if (typeof document === "undefined" || !document.body) return Promise.resolve(null); + + return new Promise((resolve) => { + const host = document.createElement("div"); + document.body.append(host); + const root = createRoot(host); + let settled = false; + + const finish = (value: string | null) => { + if (settled) return; + settled = true; + queueMicrotask(() => { + root.unmount(); + host.remove(); + resolve(value); + }); + }; + + root.render(); + }); +} diff --git a/crates/agent-gateway/web/src/shims/tauriCore.ts b/crates/agent-gateway/web/src/shims/tauriCore.ts index bec00270f..2e8cc0ec0 100644 --- a/crates/agent-gateway/web/src/shims/tauriCore.ts +++ b/crates/agent-gateway/web/src/shims/tauriCore.ts @@ -1,5 +1,6 @@ import { getGatewayWebSocketClient } from "../lib/gatewaySocket"; import { loadToken } from "../lib/storage"; +import { promptPathInBrowser } from "./browserPathPrompt"; type GatewayRuntimeStatus = { online: boolean; @@ -64,117 +65,6 @@ async function invokeGatewayMemory(command: string, args?: Record { - if (typeof window === "undefined" || typeof document === "undefined" || !document.body) { - return Promise.resolve(null); - } - - return new Promise((resolve) => { - const overlay = document.createElement("div"); - overlay.className = - "fixed inset-0 z-[120] flex items-center justify-center bg-black/55 p-4 backdrop-blur-sm"; - overlay.setAttribute("role", "dialog"); - overlay.setAttribute("aria-modal", "true"); - overlay.setAttribute("aria-label", options.title); - - const panel = document.createElement("form"); - panel.className = - "relative z-10 w-full max-w-md overflow-hidden rounded-2xl border border-border/70 bg-background shadow-2xl"; - - const header = document.createElement("div"); - header.className = "border-b border-border/60 px-5 py-4"; - - const title = document.createElement("div"); - title.className = "text-base font-semibold text-foreground"; - title.textContent = options.title; - - const description = document.createElement("div"); - description.className = "mt-1 text-xs text-muted-foreground"; - description.textContent = options.description; - - const body = document.createElement("div"); - body.className = "space-y-2 px-5 py-5"; - - const label = document.createElement("label"); - label.className = "block text-xs font-medium text-muted-foreground"; - label.htmlFor = options.inputId; - label.textContent = options.label; - - const input = document.createElement("input"); - input.id = options.inputId; - input.className = - "h-10 w-full rounded-lg border border-input bg-background px-3 font-mono text-sm text-foreground outline-none transition-colors placeholder:text-muted-foreground/70 focus:border-ring focus:ring-2 focus:ring-ring/20"; - input.placeholder = options.placeholder; - input.type = "text"; - - const footer = document.createElement("div"); - footer.className = - "flex flex-col-reverse gap-2 border-t border-border/60 bg-muted/20 px-5 py-4 sm:flex-row sm:justify-end"; - - const cancelButton = document.createElement("button"); - cancelButton.type = "button"; - cancelButton.className = - "inline-flex h-9 items-center justify-center rounded-lg border border-input bg-background px-3 text-sm font-medium text-foreground transition-colors hover:bg-muted sm:w-auto"; - cancelButton.textContent = "取消"; - - const confirmButton = document.createElement("button"); - confirmButton.type = "submit"; - confirmButton.className = - "inline-flex h-9 items-center justify-center rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50 sm:w-auto"; - confirmButton.disabled = true; - confirmButton.textContent = "确认"; - let closed = false; - - const cleanup = (value: string | null) => { - if (closed) return; - closed = true; - window.removeEventListener("keydown", handleKeyDown); - overlay.remove(); - resolve(value); - }; - - function handleKeyDown(event: KeyboardEvent) { - if (event.key === "Escape") { - cleanup(null); - } - } - - input.addEventListener("input", () => { - confirmButton.disabled = input.value.trim().length === 0; - }); - cancelButton.addEventListener("click", () => cleanup(null)); - overlay.addEventListener("click", (event) => { - if (event.target === overlay) { - cleanup(null); - } - }); - panel.addEventListener("submit", (event) => { - event.preventDefault(); - const value = input.value.trim(); - if (value) { - cleanup(value); - } - }); - window.addEventListener("keydown", handleKeyDown); - - header.append(title, description); - body.append(label, input); - footer.append(cancelButton, confirmButton); - panel.append(header, body, footer); - overlay.append(panel); - document.body.append(overlay); - window.requestAnimationFrame(() => input.focus()); - }); -} - async function pickWorkdirInBrowser(): Promise { return promptPathInBrowser({ title: "选择工作目录", diff --git a/crates/agent-gateway/web/src/styles.css b/crates/agent-gateway/web/src/styles.css index 766fd21c4..aec47a9f7 100644 --- a/crates/agent-gateway/web/src/styles.css +++ b/crates/agent-gateway/web/src/styles.css @@ -1143,60 +1143,6 @@ html[data-liveagent-webui="gateway"] .settings-confirm-popover { max-width: calc(100vw - 24px); } -html[data-liveagent-webui="gateway"] .history-share-modal-overlay, -html[data-liveagent-webui="gateway"] .external-link-modal-overlay { - opacity: 0; - transition: opacity 180ms ease; - will-change: opacity; -} - -html[data-liveagent-webui="gateway"] .history-share-modal-overlay[data-state="open"], -html[data-liveagent-webui="gateway"] .external-link-modal-overlay[data-state="open"] { - opacity: 1; -} - -html[data-liveagent-webui="gateway"] .history-share-modal-overlay[data-state="closed"], -html[data-liveagent-webui="gateway"] .external-link-modal-overlay[data-state="closed"] { - pointer-events: none; -} - -html[data-liveagent-webui="gateway"] .history-share-modal-panel, -html[data-liveagent-webui="gateway"] .external-link-modal-panel { - opacity: 0; - transform: translateY(12px) scale(0.985); - transition: - opacity 180ms ease, - transform 180ms cubic-bezier(0.22, 1, 0.36, 1); - will-change: opacity, transform; -} - -html[data-liveagent-webui="gateway"] - .history-share-modal-overlay[data-state="open"] - .history-share-modal-panel, -html[data-liveagent-webui="gateway"] - .external-link-modal-overlay[data-state="open"] - .external-link-modal-panel { - opacity: 1; - transform: translateY(0) scale(1); -} - -html[data-liveagent-webui="gateway"] - .history-share-modal-overlay[data-state="closed"] - .history-share-modal-panel, -html[data-liveagent-webui="gateway"] - .external-link-modal-overlay[data-state="closed"] - .external-link-modal-panel { - opacity: 0; - transform: translateY(8px) scale(0.985); -} - -@media (prefers-reduced-motion: reduce) { - html[data-liveagent-webui="gateway"] .external-link-modal-overlay, - html[data-liveagent-webui="gateway"] .external-link-modal-panel { - transition-duration: 1ms; - } -} - /* ─── Login Page — macOS Frosted Glass ──────────────────────────── */ .login-shell { @@ -2483,11 +2429,6 @@ html[data-liveagent-webui="gateway"] .project-terminal-viewport .xterm-scrollabl flex: 1 1 8rem; } - html[data-liveagent-webui="gateway"] .settings-modal-overlay { - align-items: stretch; - padding: max(8px, env(safe-area-inset-top)) 8px max(8px, env(safe-area-inset-bottom)); - } - html[data-liveagent-webui="gateway"] .settings-modal-panel { display: flex; width: min(100%, calc(100vw - 16px)) !important; diff --git a/crates/agent-gui/src/agent-ui-adapters/providerSettings.tsx b/crates/agent-gui/src/agent-ui-adapters/providerSettings.tsx index 7503416c6..d9e4b85f6 100644 --- a/crates/agent-gui/src/agent-ui-adapters/providerSettings.tsx +++ b/crates/agent-gui/src/agent-ui-adapters/providerSettings.tsx @@ -8,6 +8,12 @@ import { X, } from "@liveagent/ui/components/IconSet"; import { Button } from "@liveagent/ui/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@liveagent/ui/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, @@ -24,7 +30,6 @@ import { } from "@liveagent/ui/pages/settings/providerUtils"; import { invoke } from "@tauri-apps/api/core"; import { useMemo, useState } from "react"; -import { createPortal } from "react-dom"; import ccswitchLogoUrl from "../../src-tauri/icons/custom/ccswitch.png"; import cherryStudioLogoUrl from "../../src-tauri/icons/custom/cherrystudio.png"; import type { ProviderModelConfig } from "../lib/settings"; @@ -251,22 +256,16 @@ function CcsImportModal(props: { }); } - return createPortal( -
-
-
-
, - document.body, +
+ ); } diff --git a/crates/agent-gui/src/pages/settings/CherryStudioImportModal.tsx b/crates/agent-gui/src/pages/settings/CherryStudioImportModal.tsx index fb1e9b12b..91cd134df 100644 --- a/crates/agent-gui/src/pages/settings/CherryStudioImportModal.tsx +++ b/crates/agent-gui/src/pages/settings/CherryStudioImportModal.tsx @@ -10,11 +10,15 @@ import { X, } from "@liveagent/ui/components/IconSet"; import { Button } from "@liveagent/ui/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@liveagent/ui/components/ui/dialog"; import { Input } from "@liveagent/ui/components/ui/input"; -import { useAnimatedPresence } from "@liveagent/ui/lib/shared/modalMotion"; import { cn } from "@liveagent/ui/lib/shared/utils"; import { useMemo, useState } from "react"; -import { createPortal } from "react-dom"; import type { CodexRequestFormat, ProviderId } from "../../lib/settings"; export type CherryProviderImportItem = { @@ -99,7 +103,6 @@ export function CherryStudioImportModal(props: CherryStudioImportModalProps) { const candidates = response.providers; const resolvedDataPath = dataPath ?? response.dataPath ?? ""; const [pathDialogOpen, setPathDialogOpen] = useState(false); - const pathDialog = useAnimatedPresence(pathDialogOpen); const hasSyncableItems = useMemo( () => candidates.some((item) => item.enabled && item.importable), [candidates], @@ -154,21 +157,15 @@ export function CherryStudioImportModal(props: CherryStudioImportModalProps) { }); } - return createPortal( -
-
-
- - {pathDialog.shouldRender ? ( -
-
) : null} - - - ) : null} - , - document.body, + + + + ); } diff --git a/crates/agent-ui/src/components/Markdown.tsx b/crates/agent-ui/src/components/Markdown.tsx index 74896a432..80a27a9ae 100644 --- a/crates/agent-ui/src/components/Markdown.tsx +++ b/crates/agent-ui/src/components/Markdown.tsx @@ -15,7 +15,6 @@ import { useMemo, useState, } from "react"; -import { createPortal } from "react-dom"; import remarkBreaks from "remark-breaks"; import { type Components, @@ -40,6 +39,7 @@ import { normalizeLatexDelimiters } from "../lib/normalizeLatexDelimiters"; import { cn } from "../lib/shared/utils"; import { Button } from "./ui/button"; import { CopyButton } from "./ui/copy-button"; +import { Dialog, DialogContent, DialogDescription, DialogTitle } from "./ui/dialog"; const CHAT_FILE_NODE_DATA_KEY = "liveagentChatFileLink"; const LIVEAGENT_FILE_PROTOCOL = "liveagent-file:"; @@ -558,32 +558,23 @@ export function ExternalLinkModal({ isOpen, onClose, onConfirm, url }: LinkSafet } }; - const modal = ( -
-
- - + + ); - - return createPortal(modal, document.body); } const MARKDOWN_EMBED_CLASSNAME = cn( diff --git a/crates/agent-ui/src/components/chat/HistoryShareModal.tsx b/crates/agent-ui/src/components/chat/HistoryShareModal.tsx index d2e8e29c7..fb517b8fa 100644 --- a/crates/agent-ui/src/components/chat/HistoryShareModal.tsx +++ b/crates/agent-ui/src/components/chat/HistoryShareModal.tsx @@ -10,10 +10,15 @@ import { X, } from "@liveagent/ui/components/IconSet"; import { Button } from "@liveagent/ui/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@liveagent/ui/components/ui/dialog"; import { buildShareUrl, resolveShareOrigin } from "@liveagent/ui/lib/chat/historyShareOrigin"; import { cn } from "@liveagent/ui/lib/shared/utils"; import { useEffect, useId, useMemo, useState } from "react"; -import { createPortal } from "react-dom"; type ShareConversation = { title: string; @@ -182,29 +187,22 @@ export function HistoryShareModal({ }); } - return createPortal( -
-
- -
+ return ( + !open && onClose()}> +
-
分享会话
-
分享会话 + {conversation.title} -
+
-
-
, - document.body, + + ); } diff --git a/crates/agent-ui/src/components/chat/SharedHistoryManagerModal.tsx b/crates/agent-ui/src/components/chat/SharedHistoryManagerModal.tsx index 8c57f9a01..e28ccbdf6 100644 --- a/crates/agent-ui/src/components/chat/SharedHistoryManagerModal.tsx +++ b/crates/agent-ui/src/components/chat/SharedHistoryManagerModal.tsx @@ -13,11 +13,16 @@ import { X, } from "@liveagent/ui/components/IconSet"; import { Button } from "@liveagent/ui/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@liveagent/ui/components/ui/dialog"; import { useLocale } from "@liveagent/ui/i18n/index"; import { buildShareUrl, resolveShareOrigin } from "@liveagent/ui/lib/chat/historyShareOrigin"; import { cn } from "@liveagent/ui/lib/shared/utils"; import { useMemo, useState } from "react"; -import { createPortal } from "react-dom"; export type ManagedHistoryShareStatus = { conversationId?: string; @@ -217,16 +222,9 @@ export function SharedHistoryManagerModal setCopiedId(null)); } - return createPortal( -
-
- -
+ return ( + !open && onClose()}> +
@@ -234,12 +232,12 @@ export function SharedHistoryManagerModal
-
+ {t("sharedHistory.title")} -
-
+ + {t("sharedHistory.subtitle")} -
+
-
-
, - document.body, + + ); } diff --git a/crates/agent-ui/src/components/chat/WorkspaceCloneModal.tsx b/crates/agent-ui/src/components/chat/WorkspaceCloneModal.tsx index 76b7d4b1b..f7cb2dc93 100644 --- a/crates/agent-ui/src/components/chat/WorkspaceCloneModal.tsx +++ b/crates/agent-ui/src/components/chat/WorkspaceCloneModal.tsx @@ -1,6 +1,12 @@ import { useDirectoryPicker } from "@liveagent/adapters/directoryPicker"; import { FolderOpen, GitBranch, Loader2, X } from "@liveagent/ui/components/IconSet"; import { Button } from "@liveagent/ui/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@liveagent/ui/components/ui/dialog"; import { Input } from "@liveagent/ui/components/ui/input"; import { Label } from "@liveagent/ui/components/ui/label"; import { @@ -11,9 +17,7 @@ import { SelectValue, } from "@liveagent/ui/components/ui/select"; import { useLocale } from "@liveagent/ui/i18n/index"; -import { useModalMotion } from "@liveagent/ui/lib/shared/modalMotion"; import { useCallback, useEffect, useRef, useState } from "react"; -import { createPortal } from "react-dom"; type RemoteBranches = { defaultBranch: string; @@ -58,7 +62,6 @@ export function WorkspaceCloneModal({ const [name, setName] = useState(""); const [error, setError] = useState(""); const [cloning, setCloning] = useState(false); - const { modalState, requestClose } = useModalMotion(onClose); const [nameIsAutomatic, setNameIsAutomatic] = useState(true); const [branch, setBranch] = useState(""); @@ -76,17 +79,6 @@ export function WorkspaceCloneModal({ !cloning, ); - useEffect(() => { - function onKeyDown(event: KeyboardEvent) { - // defaultPrevented: an open branch Select consumes Escape to close itself. - if (event.key !== "Escape" || event.defaultPrevented || cloning) return; - event.preventDefault(); - requestClose(); - } - window.addEventListener("keydown", onKeyDown); - return () => window.removeEventListener("keydown", onKeyDown); - }, [cloning, requestClose]); - async function chooseParent() { try { const selected = await pickDirectory(parent); @@ -144,7 +136,7 @@ export function WorkspaceCloneModal({ setError(""); try { await onClone(remoteUrl.trim(), parent.trim(), name.trim(), branch); - requestClose(); + onClose(); } catch (reason) { setError(errorMessage(reason)); } finally { @@ -152,39 +144,27 @@ export function WorkspaceCloneModal({ } } - const modal = createPortal( -
{ + if (!open && !cloning) onClose(); + }} > -
@@ -196,7 +176,7 @@ export function WorkspaceCloneModal({ className="h-auto w-full justify-start gap-3 rounded-2xl p-4 text-left" onClick={() => { onOpenFolder(); - requestClose(); + onClose(); }} > @@ -313,7 +293,7 @@ export function WorkspaceCloneModal({ ) : null} {error ?

{error}

: null}
-
-
- , - document.body, + + ); return ( diff --git a/crates/agent-ui/src/components/git/GitBranchSelector.tsx b/crates/agent-ui/src/components/git/GitBranchSelector.tsx index b9b88dd53..4f4cdfe91 100644 --- a/crates/agent-ui/src/components/git/GitBranchSelector.tsx +++ b/crates/agent-ui/src/components/git/GitBranchSelector.tsx @@ -54,7 +54,6 @@ import { cn } from "@liveagent/ui/lib/shared/utils"; import type { WorkspaceActivityClient } from "@liveagent/ui/lib/workspace-activity/types"; import { useWorkspaceInvalidation } from "@liveagent/ui/lib/workspace-activity/useWorkspaceInvalidation"; import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; -import { createPortal } from "react-dom"; import type { GitBranch as GitBranchInfo, GitClient, @@ -163,132 +162,125 @@ function GitInitModal(props: { onSubmit, } = props; const { t } = useLocale(); - const titleId = useId(); const branchId = useId(); const userNameId = useId(); const userEmailId = useId(); - if (!open) return null; - - return createPortal( -
{ + if (!nextOpen && !loading) onClose(); + }} > -
-
{ - event.preventDefault(); - onSubmit(); - }} - > -
-
-
- -
-
-
- {t("git.branchSelector.initRepositoryTitle")} + + { + event.preventDefault(); + onSubmit(); + }} + > +
+
+
+
-
- {t("git.branchSelector.initRepositoryDescription")} +
+ + {t("git.branchSelector.initRepositoryTitle")} + + + {t("git.branchSelector.initRepositoryDescription")} +
-
- -
-
-
- -
- {workdir} -
-
-
- - onBranchChange(event.target.value)} - className="git-branch-selector-input h-9 text-sm" - placeholder="main" - autoFocus +
-
+
-
-
-
- {error ? ( -
- {error} +
+
+ + onUserNameChange(event.target.value)} + className="git-branch-selector-input h-9 text-sm" + disabled={loading} + /> +
+
+ + onUserEmailChange(event.target.value)} + className="git-branch-selector-input h-9 text-sm" + disabled={loading} + /> +
- ) : null} -
-
- - -
- -
, - document.body, + {error ? ( +
+ {error} +
+ ) : null} +
+
+ + +
+ +
+ ); } @@ -536,8 +528,7 @@ function WorktreeCreateModal(props: { ); } -// Per-branch action sheet opened from a branch row's "⋯" button. Lives below -// the shared ConfirmDialog (z-[120]) so delete confirmations stack above it. +// Per-branch action dialog opened from a branch row's "⋯" button. function BranchActionsModal(props: { action: GitBranchActionState | null; canWrite: boolean; @@ -576,7 +567,6 @@ function BranchActionsModal(props: { onClose, } = props; const { t } = useLocale(); - const titleId = useId(); const inputId = useId(); if (!action) return null; @@ -592,205 +582,197 @@ function BranchActionsModal(props: { ? t("git.branchSelector.renameBranch") : t("git.branchSelector.createFromHere"); - return createPortal( -
{ + if (!nextOpen && !busy) onClose(); + }} > -
-
{ - event.preventDefault(); - if (isForm) onSubmit(); - }} - > -
-
-
- -
-
-
- {branch.fullName} + + { + event.preventDefault(); + if (isForm) onSubmit(); + }} + > +
+
+
+
-
- {isForm ? formTitle : kindLabel} +
+ + {branch.fullName} + + + {isForm ? formTitle : kindLabel} +
-
- -
- {mode === "menu" ? ( -
- {canWrite ? ( - - ) : null} - {canWrite && isLocal ? ( - - ) : null} - - {canWrite && isLocal && !branch.current ? ( - checkedOutWorktreePath && onDeleteWorktree ? ( + + +
+ {mode === "menu" ? ( +
+ {canWrite ? ( - ) : ( + ) : null} + {canWrite && isLocal ? ( - ) - ) : null} - {error ? ( -
- {error} -
- ) : null} -
- ) : ( -
- {mode === "createFrom" ? ( + ) : null} + + {canWrite && isLocal && !branch.current ? ( + checkedOutWorktreePath && onDeleteWorktree ? ( + + ) : ( + + ) + ) : null} + {error ? ( +
+ {error} +
+ ) : null} +
+ ) : ( +
+ {mode === "createFrom" ? ( +
+ +
+ {branch.fullName} +
+
+ ) : null}
-
- ) : null} -
- - onDraftChange(event.target.value)} - onKeyDown={(event) => { - // Keep keystrokes local to the sheet; Escape steps back to - // the action list instead of dismissing the whole dialog. - event.stopPropagation(); - if (event.nativeEvent.isComposing) return; - if (event.key === "Escape") { - event.preventDefault(); - onBack(); + onDraftChange(event.target.value)} + onKeyDown={(event) => { + // Keep keystrokes local to the sheet; Escape steps back to + // the action list instead of dismissing the whole dialog. + event.stopPropagation(); + if (event.nativeEvent.isComposing) return; + if (event.key === "Escape") { + event.preventDefault(); + onBack(); + } + }} + placeholder={ + mode === "rename" + ? t("git.branchSelector.renamePlaceholder") + : t("git.branchSelector.newBranchPlaceholder") } - }} - placeholder={ - mode === "rename" - ? t("git.branchSelector.renamePlaceholder") - : t("git.branchSelector.newBranchPlaceholder") - } - className="h-8 text-xs" - autoFocus - disabled={busy} - /> -
- {error ? ( -
- {error} + className="h-8 text-xs" + autoFocus + disabled={busy} + />
- ) : null} -
- )} - {isForm ? ( -
- - -
- ) : null} - -
, - document.body, + {error ? ( +
+ {error} +
+ ) : null} +
+ )} + {isForm ? ( +
+ + +
+ ) : null} + + + ); } diff --git a/crates/agent-ui/src/components/project-tools/BackgroundTasksPanel.tsx b/crates/agent-ui/src/components/project-tools/BackgroundTasksPanel.tsx index 86295186d..a84ce7994 100644 --- a/crates/agent-ui/src/components/project-tools/BackgroundTasksPanel.tsx +++ b/crates/agent-ui/src/components/project-tools/BackgroundTasksPanel.tsx @@ -19,7 +19,6 @@ import { useRef, useState, } from "react"; -import { createPortal } from "react-dom"; import { clearManagedProcesses, readManagedProcessLog, @@ -29,6 +28,7 @@ import { import type { ManagedProcessLog, ManagedProcessRecord } from "../../lib/managed-process/types"; import { cn } from "../../lib/shared/utils"; import { Button } from "../ui/button"; +import { Dialog, DialogContent, DialogDescription, DialogTitle } from "../ui/dialog"; type BackgroundTasksPanelProps = { // Visibility contract from the right dock: gates the per-second uptime @@ -76,8 +76,6 @@ function processCopyText(process: ManagedProcessRecord) { ].join("\n"); } -// Portal modal following the mirrored confirm-dialog shell: bottom sheet on -// small (touch) viewports, centered card from `sm:` up. function BackgroundTaskLogDialog(props: { process: ManagedProcessRecord; actionsDisabled: boolean; @@ -113,19 +111,6 @@ function BackgroundTaskLogDialog(props: { refresh(); }, [refresh]); - useEffect(() => { - function handleKeyDown(event: KeyboardEvent) { - if (event.key !== "Escape") return; - if (contextMenu) { - setContextMenu(null); - } else { - onClose(); - } - } - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [contextMenu, onClose]); - const handleLogContextMenu = useCallback((event: ReactMouseEvent) => { // Replace the native menu with our own; stopPropagation also keeps the // desktop AppChrome's global contextmenu suppression out of the loop. @@ -170,34 +155,29 @@ function BackgroundTaskLogDialog(props: { if (log?.content) copyToClipboard(log.content); }, [copyToClipboard, log?.content]); - return createPortal( -
{ + if (open) return; + if (contextMenu) setContextMenu(null); + else onClose(); + }} > -
- - {contextMenu ? ( -
- - - + + + +
-
- ) : null} -
, - document.body, + ) : null} + + ); } diff --git a/crates/agent-ui/src/components/project-tools/SshPortForwardDialog.tsx b/crates/agent-ui/src/components/project-tools/SshPortForwardDialog.tsx index ba254d371..ed07bd6f8 100644 --- a/crates/agent-ui/src/components/project-tools/SshPortForwardDialog.tsx +++ b/crates/agent-ui/src/components/project-tools/SshPortForwardDialog.tsx @@ -1,4 +1,3 @@ -import { Dialog } from "@base-ui/react/dialog"; import { Cable, Loader2, X } from "@liveagent/ui/components/IconSet"; import { useLocale } from "@liveagent/ui/i18n/index"; import { useRef, useState } from "react"; @@ -11,6 +10,7 @@ import { validateSshLocalForwardTarget, } from "../../lib/terminal/sshLocalForwardTypes"; import { Button } from "../ui/button"; +import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from "../ui/dialog"; export type SshPortForwardDialogProps = { sessionId: string; @@ -110,157 +110,152 @@ export function SshPortForwardDialog(props: SshPortForwardDialogProps) { const previewPort = remotePort || "?"; return ( - { if (!open) onClose(); }} > - - - - -
-
-
- -
-
- - {t("projectTools.sshLocalForwardModalTitle")} - -
- {subtitle} -
-
-
+ +
+
+
+ +
+
+ + {t("projectTools.sshLocalForwardModalTitle")} + + + {subtitle} + +
+
+ + + } + > + + +
- - } +
{ + event.preventDefault(); + handleSubmit(); + }} + > +
+
+
+ + { + const value = event.currentTarget.value; + if (!isSshLocalForwardPortDraft(value)) return; + setLocalPort(value); + setError(""); + }} + onBlur={handleLocalPortBlur} + className={`${FIELD_CLASS} font-mono`} + placeholder={t("projectTools.sshLocalForwardAutoPort")} + disabled={submitting} + /> +
+
+ + { + const value = event.currentTarget.value; + if (!isSshLocalForwardPortDraft(value)) return; + setRemotePort(value); + setError(""); + }} + className={`${FIELD_CLASS} font-mono`} + placeholder={t("projectTools.sshLocalForwardRemotePortLabel")} + disabled={submitting} + /> +
+
+
+ + { + setRemoteHost(event.currentTarget.value); + setError(""); + }} + className={FIELD_CLASS} + placeholder={t("projectTools.sshLocalForwardHostPlaceholder")} + disabled={submitting} + />
- { - event.preventDefault(); - handleSubmit(); - }} - > -
-
-
- - { - const value = event.currentTarget.value; - if (!isSshLocalForwardPortDraft(value)) return; - setLocalPort(value); - setError(""); - }} - onBlur={handleLocalPortBlur} - className={`${FIELD_CLASS} font-mono`} - placeholder={t("projectTools.sshLocalForwardAutoPort")} - disabled={submitting} - /> -
-
- - { - const value = event.currentTarget.value; - if (!isSshLocalForwardPortDraft(value)) return; - setRemotePort(value); - setError(""); - }} - className={`${FIELD_CLASS} font-mono`} - placeholder={t("projectTools.sshLocalForwardRemotePortLabel")} - disabled={submitting} - /> -
-
-
- - { - setRemoteHost(event.currentTarget.value); - setError(""); - }} - className={FIELD_CLASS} - placeholder={t("projectTools.sshLocalForwardHostPlaceholder")} - disabled={submitting} - /> -
- -
- 127.0.0.1:{previewLocal} - - {previewHost}:{previewPort} -
+
+ 127.0.0.1:{previewLocal} + + {previewHost}:{previewPort} +
-
- {t("projectTools.sshLocalForwardHelp")} -
+
+ {t("projectTools.sshLocalForwardHelp")} +
- {error ? ( -
- {error} -
- ) : null} + {error ? ( +
+ {error}
+ ) : null} +
-
- } - > - {t("projectTools.sshLocalForwardCancel")} - - -
- - - - - +
+ } + > + {t("projectTools.sshLocalForwardCancel")} + + +
+ + + ); } diff --git a/crates/agent-ui/src/components/project-tools/SshTunnelPanel.tsx b/crates/agent-ui/src/components/project-tools/SshTunnelPanel.tsx index 1e212ecb7..cd5e57847 100644 --- a/crates/agent-ui/src/components/project-tools/SshTunnelPanel.tsx +++ b/crates/agent-ui/src/components/project-tools/SshTunnelPanel.tsx @@ -27,6 +27,12 @@ import { import { SshPortForwardDialog } from "@liveagent/ui/components/project-tools/SshPortForwardDialog"; import { Button } from "@liveagent/ui/components/ui/button"; import { useConfirmDialog } from "@liveagent/ui/components/ui/confirm-dialog"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@liveagent/ui/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, @@ -1549,98 +1555,104 @@ export function SshTunnelPanel(props: SshTunnelPanelProps) {
- {prompt ? ( -
-
{ - event.preventDefault(); - handleSubmitPrompt(); - }} - > -
-
- -
-
-
- {hostKeyPrompt - ? t("projectTools.sshTunnelPromptTitle") - : t("projectTools.sshTunnelAuthPromptTitle")} + { + if (!open) handleCancelPrompt(); + }} + > + + {prompt ? ( + { + event.preventDefault(); + handleSubmitPrompt(); + }} + > +
+
+
-
- {prompt.message} +
+ + {hostKeyPrompt + ? t("projectTools.sshTunnelPromptTitle") + : t("projectTools.sshTunnelAuthPromptTitle")} + + + {prompt.message} +
-
-
-
- - {t("projectTools.sshTunnelHost")} - - - {prompt.host}:{prompt.port} - -
- {prompt.keyType ? ( +
- {t("projectTools.sshTunnelKeyType")} + {t("projectTools.sshTunnelHost")} - {prompt.keyType} - -
- ) : null} - {prompt.fingerprintSha256 ? ( -
- - {t("projectTools.sshTunnelFingerprint")} - - - {prompt.fingerprintSha256} + {prompt.host}:{prompt.port}
+ {prompt.keyType ? ( +
+ + {t("projectTools.sshTunnelKeyType")} + + + {prompt.keyType} + +
+ ) : null} + {prompt.fingerprintSha256 ? ( +
+ + {t("projectTools.sshTunnelFingerprint")} + + + {prompt.fingerprintSha256} + +
+ ) : null} +
+ {!hostKeyPrompt ? ( + setPromptAnswer(event.currentTarget.value)} + className="mt-3 h-10 w-full rounded-lg border border-border/70 bg-background/80 px-3 text-[calc(11px*var(--zone-font-scale,1))] text-foreground outline-none transition-colors placeholder:text-[calc(11px*var(--zone-font-scale,1))] placeholder:text-muted-foreground/70 focus-visible:border-emerald-500/50 focus-visible:ring-1 focus-visible:ring-emerald-500/20" + type={prompt.answerEcho ? "text" : "password"} + aria-label={t("projectTools.sshTunnelAuthPromptTitle")} + autoFocus + /> ) : null} -
- {!hostKeyPrompt ? ( - setPromptAnswer(event.currentTarget.value)} - className="mt-3 h-10 w-full rounded-lg border border-border/70 bg-background/80 px-3 text-[calc(11px*var(--zone-font-scale,1))] text-foreground outline-none transition-colors placeholder:text-[calc(11px*var(--zone-font-scale,1))] placeholder:text-muted-foreground/70 focus-visible:border-emerald-500/50 focus-visible:ring-1 focus-visible:ring-emerald-500/20" - type={prompt.answerEcho ? "text" : "password"} - aria-label={t("projectTools.sshTunnelAuthPromptTitle")} - autoFocus - /> - ) : null} -
- - -
- -
- ) : null} +
+ + +
+ + ) : null} + + {closeSessionConfirmDialog} {forwardModalSession ? ( { + if (!nextOpen && !loading) onClose(); + }} > -
-
{ - event.preventDefault(); - onSubmit(); - }} - > -
-
- {t("projectTools.gitReview.remoteSetupTitle")} -
-
- {t(remoteSetupDescriptionKey(action))} + + { + event.preventDefault(); + onSubmit(); + }} + > +
+ + {t("projectTools.gitReview.remoteSetupTitle")} + + + {t(remoteSetupDescriptionKey(action))} +
-
-
-
-
- {branch} +
+
+
+ {branch} +
+
+ {workdir} +
-
- {workdir} +
+ + onRemoteUrlChange(event.target.value)} + className="h-9 text-[calc(11px*var(--zone-font-scale,1))] placeholder:text-[calc(11px*var(--zone-font-scale,1))]" + placeholder={t("projectTools.gitReview.remoteUrlPlaceholder")} + autoFocus + disabled={loading} + />
+ {error ? ( +
+ {error} +
+ ) : null}
-
- - onRemoteUrlChange(event.target.value)} - className="h-9 text-[calc(11px*var(--zone-font-scale,1))] placeholder:text-[calc(11px*var(--zone-font-scale,1))]" - placeholder={t("projectTools.gitReview.remoteUrlPlaceholder")} - autoFocus - disabled={loading} - /> +
+ +
- {error ? ( -
- {error} -
- ) : null} -
-
- - -
- -
, - document.body, + + + ); } @@ -179,8 +178,6 @@ export function GitDiscardConfirmModal(props: { }) { const { target, loading, onClose, onConfirm } = props; const { t } = useLocale(); - const titleId = useId(); - if (!target) return null; const isAll = target.kind === "all"; @@ -191,27 +188,23 @@ export function GitDiscardConfirmModal(props: { ? t("projectTools.gitReview.discardAllConfirm") : t("projectTools.gitReview.discardConfirm").replace("{path}", target.path); - return createPortal( -
{ + if (!nextOpen && !loading) onClose(); + }} > -
-
+
-
- {title} -
-
{description}
+ {title} + + {description} +
@@ -235,9 +228,8 @@ export function GitDiscardConfirmModal(props: { {title}
-
-
, - document.body, + + ); } @@ -252,84 +244,79 @@ export function GitBranchFromCommitModal(props: { }) { const { target, branchName, loading, error, onBranchNameChange, onClose, onSubmit } = props; const { t } = useLocale(); - const titleId = useId(); const branchNameId = useId(); if (!target) return null; - return createPortal( -
{ + if (!nextOpen && !loading) onClose(); + }} > -
-
{ - event.preventDefault(); - onSubmit(); - }} - > -
-
- {t("projectTools.gitReview.createBranchFromCommitTitle")} -
-
- {t("projectTools.gitReview.createBranchFromCommitDescription") - .replace("{sha}", target.shortSha) - .replace("{subject}", target.subject || target.shortSha)} + + { + event.preventDefault(); + onSubmit(); + }} + > +
+ + {t("projectTools.gitReview.createBranchFromCommitTitle")} + + + {t("projectTools.gitReview.createBranchFromCommitDescription") + .replace("{sha}", target.shortSha) + .replace("{subject}", target.subject || target.shortSha)} +
-
-
-
-
- {target.shortSha} +
+
+
+ {target.shortSha} +
+
+ {target.subject || target.commitSha} +
-
- {target.subject || target.commitSha} +
+ + onBranchNameChange(event.target.value)} + className="h-9 text-[calc(11px*var(--zone-font-scale,1))] placeholder:text-[calc(11px*var(--zone-font-scale,1))]" + placeholder={t("projectTools.gitReview.branchNamePlaceholder")} + autoFocus + disabled={loading} + />
+ {error ? ( +
+ {error} +
+ ) : null}
-
- - onBranchNameChange(event.target.value)} - className="h-9 text-[calc(11px*var(--zone-font-scale,1))] placeholder:text-[calc(11px*var(--zone-font-scale,1))]" - placeholder={t("projectTools.gitReview.branchNamePlaceholder")} - autoFocus - disabled={loading} - /> +
+ +
- {error ? ( -
- {error} -
- ) : null} -
-
- - -
- -
, - document.body, + + + ); } @@ -406,36 +393,30 @@ export function GitBranchSwitchConflictModal(props: { }) { const { conflict, loading, onClose, onConfirm } = props; const { t } = useLocale(); - const titleId = useId(); - if (!conflict) return null; - return createPortal( -
{ + if (!nextOpen && !loading) onClose(); + }} > -
-
+
-
+ {t("projectTools.gitReview.switchBranchConflictTitle")} -
-
+ + {t("projectTools.gitReview.switchBranchConflictDescription").replace( "{branch}", conflict.branch, )} -
+
@@ -451,9 +432,8 @@ export function GitBranchSwitchConflictModal(props: { {t("projectTools.gitReview.stashAndSwitch")}
-
-
, - document.body, + + ); } diff --git a/crates/agent-ui/src/components/ui/alert-dialog.tsx b/crates/agent-ui/src/components/ui/alert-dialog.tsx new file mode 100644 index 000000000..9d7b80a0b --- /dev/null +++ b/crates/agent-ui/src/components/ui/alert-dialog.tsx @@ -0,0 +1,86 @@ +import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"; +import * as React from "react"; + +import { cn } from "../../lib/shared/utils"; + +export const AlertDialog = AlertDialogPrimitive.Root; +export const AlertDialogPortal = AlertDialogPrimitive.Portal; +export const AlertDialogClose = AlertDialogPrimitive.Close; + +export const AlertDialogOverlay = React.forwardRef< + HTMLDivElement, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogOverlay.displayName = "AlertDialogOverlay"; + +type AlertDialogContentProps = React.ComponentPropsWithoutRef & { + overlayClassName?: string; + portalProps?: React.ComponentPropsWithoutRef; + viewportClassName?: string; +}; + +export const AlertDialogContent = React.forwardRef( + ({ className, children, overlayClassName, portalProps, viewportClassName, ...props }, ref) => ( + + + + + {children} + + + + ), +); +AlertDialogContent.displayName = "AlertDialogContent"; + +export const AlertDialogTitle = React.forwardRef< + HTMLHeadingElement, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = "AlertDialogTitle"; + +export const AlertDialogDescription = React.forwardRef< + HTMLParagraphElement, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogDescription.displayName = "AlertDialogDescription"; + +export { AlertDialogPrimitive }; diff --git a/crates/agent-ui/src/components/ui/confirm-dialog.tsx b/crates/agent-ui/src/components/ui/confirm-dialog.tsx index 90a0c9aa1..2484289fd 100644 --- a/crates/agent-ui/src/components/ui/confirm-dialog.tsx +++ b/crates/agent-ui/src/components/ui/confirm-dialog.tsx @@ -1,6 +1,12 @@ -import { AlertDialog } from "@base-ui/react/alert-dialog"; import { AlertTriangle, X } from "@liveagent/ui/components/IconSet"; import { type ReactNode, useCallback, useEffect, useRef, useState } from "react"; +import { + AlertDialog, + AlertDialogClose, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from "./alert-dialog"; import { Button } from "./button"; type ConfirmDialogTone = "warning" | "destructive"; @@ -60,100 +66,91 @@ function ConfirmDialog( const toneClasses = toneClassNames[tone]; return ( - { if (!open) onCancel(); }} > - - - - -
-
-
- -
-
- - {title} - - {subtitle ? ( -
- {subtitle} -
- ) : null} + +
+
+
+ +
+
+ + {title} + + {subtitle ? ( +
+ {subtitle}
-
- - - } - > - - + ) : null}
+
- {description || detail ? ( - }> - {description ? ( -
- {description} -
- ) : null} - {detail ? ( -
- {detail} -
- ) : null} -
- ) : null} - -
- {hideCancel ? null : ( - - } - > - {cancelLabel} - - )} + - {confirmLabel} - -
- - - - + variant="ghost" + size="icon" + className="h-8 w-8 shrink-0 rounded-xl text-muted-foreground hover:text-foreground" + title={closeLabel} + aria-label={closeLabel} + /> + } + > + + +
+ + {description || detail ? ( + }> + {description ? ( +
+ {description} +
+ ) : null} + {detail ? ( +
+ {detail} +
+ ) : null} +
+ ) : null} + +
+ {hideCancel ? null : ( + + } + > + {cancelLabel} + + )} + +
+ + ); } diff --git a/crates/agent-ui/src/components/ui/dialog.tsx b/crates/agent-ui/src/components/ui/dialog.tsx index bad327dab..40f702f25 100644 --- a/crates/agent-ui/src/components/ui/dialog.tsx +++ b/crates/agent-ui/src/components/ui/dialog.tsx @@ -24,7 +24,7 @@ export const DialogOverlay = React.forwardRef< ref={ref} data-slot="dialog-overlay" className={cn( - "fixed inset-0 z-50 bg-black/55 backdrop-blur-sm transition-opacity duration-200 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0", + "fixed inset-0 z-[100] bg-black/55 backdrop-blur-sm transition-opacity duration-200 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none", className, )} {...props} @@ -45,7 +45,7 @@ export const DialogContent = React.forwardRef @@ -53,7 +53,7 @@ export const DialogContent = React.forwardRef & { + backdropClassName?: string; closeLabel?: string; closeProps?: React.ComponentPropsWithoutRef; portalProps?: React.ComponentPropsWithoutRef; @@ -74,6 +75,7 @@ export const SheetPopup = React.forwardRef( { side = "right", variant = "default", + backdropClassName, className, children, closeLabel = "Close", @@ -85,7 +87,7 @@ export const SheetPopup = React.forwardRef( ref, ) => ( - + - {pendingDialog ? ( -
-
-
{dialogTitle}
-
{dialogDescription}
-
- - - -
+ { + if (!open) setPendingDialog(null); + }} + > + + {dialogTitle} + + {dialogDescription} + +
+ + +
-
- ) : null} + +
); } diff --git a/crates/agent-ui/src/components/workspace-editor/WorkspaceSftpPanel.tsx b/crates/agent-ui/src/components/workspace-editor/WorkspaceSftpPanel.tsx index 38f9e57fb..b81686c99 100644 --- a/crates/agent-ui/src/components/workspace-editor/WorkspaceSftpPanel.tsx +++ b/crates/agent-ui/src/components/workspace-editor/WorkspaceSftpPanel.tsx @@ -14,12 +14,17 @@ import { Upload, } from "@liveagent/ui/components/IconSet"; import { useConfirmDialog } from "@liveagent/ui/components/ui/confirm-dialog"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@liveagent/ui/components/ui/dialog"; import { useLocale } from "@liveagent/ui/i18n/index"; import type { SftpClient, SftpEntry, SftpSide, SftpTransfer } from "@liveagent/ui/lib/sftp/types"; import { cn } from "@liveagent/ui/lib/shared/utils"; import type { TerminalSession } from "@liveagent/ui/lib/terminal/types"; import { Fragment, useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; -import { createPortal } from "react-dom"; type WorkspaceSftpPanelProps = { session: TerminalSession; @@ -1982,83 +1987,62 @@ function CreateFolderDialog(props: { } = props; const canSubmit = value.trim().length > 0 && !submitting; - useEffect(() => { - function handleKeyDown(event: KeyboardEvent) { - if (event.key === "Escape") { - onCancel(); - } - } - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [onCancel]); - - return createPortal( -
- - -
- -
, - document.body, + return ( + !open && !submitting && onCancel()}> + +
{ + event.preventDefault(); + if (canSubmit) { + onSubmit(); + } + }} + > +
+ {title} + {path ? ( + + {path} + + ) : null} +
+
+ + onChange(event.currentTarget.value)} + /> +
+
+ + +
+
+
+
); } @@ -2071,35 +2055,12 @@ function CopyPathDialog(props: { }) { const { title, prompt, closeLabel, text, onClose } = props; - useEffect(() => { - function handleKeyDown(event: KeyboardEvent) { - if (event.key === "Escape") { - onClose(); - } - } - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [onClose]); - - return createPortal( -
-