diff --git a/crates/agent-gateway/test/webui/markdown-image-policy.test.mjs b/crates/agent-gateway/test/webui/markdown-image-policy.test.mjs index a7b16c2b9..9b2ef3c55 100644 --- a/crates/agent-gateway/test/webui/markdown-image-policy.test.mjs +++ b/crates/agent-gateway/test/webui/markdown-image-policy.test.mjs @@ -32,11 +32,6 @@ const loader = createWebModuleLoader({ "../lib/shared/utils": { cn: (...parts) => parts.filter(Boolean).join(" "), }, - "../lib/shared/modalMotion": { - useModalMotion(onClose) { - return { modalState: "open", requestClose: onClose }; - }, - }, }, }); diff --git a/crates/agent-gateway/web/src/app/AgentSelector.tsx b/crates/agent-gateway/web/src/app/AgentSelector.tsx index 65a6c38ca..8e32bab52 100644 --- a/crates/agent-gateway/web/src/app/AgentSelector.tsx +++ b/crates/agent-gateway/web/src/app/AgentSelector.tsx @@ -1,6 +1,7 @@ import { Check } from "@liveagent/ui/components/IconSet"; import { DropdownMenuItem, DropdownMenuLabel } from "@liveagent/ui/components/ui/dropdown-menu"; import { useLocale } from "@liveagent/ui/i18n/index"; +import { cn } from "@liveagent/ui/lib/shared/utils"; import { useEffect, useState } from "react"; import type { GatewayWebSocketClientLike } from "@/lib/gatewaySocket"; import type { AgentStatus } from "@/lib/gatewayTypes"; @@ -86,11 +87,12 @@ export function AgentSelector({
{statusLabel} @@ -104,11 +106,12 @@ export function AgentSelector({ {statusLabel} @@ -139,11 +142,12 @@ export function AgentSelector({ onSelect={() => handleChange(agentID)} > {statusLabel} diff --git a/crates/agent-gateway/web/src/app/GatewayAppView.tsx b/crates/agent-gateway/web/src/app/GatewayAppView.tsx index 7828827c1..ef890202a 100644 --- a/crates/agent-gateway/web/src/app/GatewayAppView.tsx +++ b/crates/agent-gateway/web/src/app/GatewayAppView.tsx @@ -18,6 +18,7 @@ import { WorkspaceOverlayHost } from "@liveagent/ui/components/workspace-editor/ import { LocaleContext, t as translate } from "@liveagent/ui/i18n/index"; import type { PendingUploadedFile } from "@liveagent/ui/lib/chat/uploadedFiles"; import { mergePendingUploadedFiles } from "@liveagent/ui/lib/chat/uploadedFiles"; +import { cn } from "@liveagent/ui/lib/shared/utils"; import { ChatComposerBar } from "@liveagent/ui/pages/chat/ChatComposerBar"; import { FloorNavRail } from "@liveagent/ui/pages/chat/transcript/FloorNavRail"; import { @@ -892,9 +893,10 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel } {settingsOpen ? (
{userAvatarLabel || } {statusLabel} diff --git a/crates/agent-gateway/web/src/components/RemotePathPickerModal.tsx b/crates/agent-gateway/web/src/components/RemotePathPickerModal.tsx index ebec718e4..cc3c43fe1 100644 --- a/crates/agent-gateway/web/src/components/RemotePathPickerModal.tsx +++ b/crates/agent-gateway/web/src/components/RemotePathPickerModal.tsx @@ -1,4 +1,3 @@ -import { Dialog } from "@base-ui/react/dialog"; import { invoke } from "@liveagent/app/shims/tauriCore"; import { AlertTriangle, @@ -8,11 +7,23 @@ import { Home, Loader2, Plus, - X, } from "@liveagent/ui/components/IconSet"; import { Button } from "@liveagent/ui/components/ui/button"; +import { + Dialog, + DialogActions, + DialogBody, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogSubheader, + DialogTitle, +} from "@liveagent/ui/components/ui/dialog"; import { Input } from "@liveagent/ui/components/ui/input"; import { useLocale } from "@liveagent/ui/i18n/index"; +import { cn } from "@liveagent/ui/lib/shared/utils"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { IndividualTreeViewState, @@ -149,7 +160,6 @@ export function RemotePathPickerModal(props: RemotePathPickerModalProps) { // Base UI 控制 open:先置 false 播放退场过渡,onOpenChangeComplete 再通知 // 调用方卸载(onSelect 已先行 resolve,onClose 的 resolve(null) 为 no-op)。 const [open, setOpen] = useState(true); - const requestClose = useCallback(() => setOpen(false), []); const modalTitle = title ?? (mode === "file" ? t("settings.filePickerTitle") : t("settings.workdirPickerTitle")); @@ -512,201 +522,185 @@ export function RemotePathPickerModal(props: RemotePathPickerModalProps) { } } - // 挂 z-[120]:需要压过新建 worktree 弹窗(z-[110])等上层来源;作为嵌套 - // Base UI Dialog 打开时 DOM 顺序也保证在后。settings-modal-* 类只复用其 - // 响应式布局规则,动画由 modal-dialog-* 的 Base UI 过渡属性驱动。 + // Nested dialogs share the portal layer; the later portal wins by DOM order. return ( - { if (!nextOpen) onClose(); }} > - - - - -
-
- {mode === "file" ? ( - - ) : ( - - )} -
-
- {modalTitle} - - {modalDescription} - -
- + + +
+ {mode === "file" ? : } +
+
+ {modalTitle} + {modalDescription} +
+
+ + +
+
+ {mode === "file" ? t("settings.pathPickerPathLabel") : t("settings.workdir")}
- -
-
-
- {mode === "file" ? t("settings.pathPickerPathLabel") : t("settings.workdir")} -
- -
+ +
+ + + +
+
+ + ~
- -
-
-
- - ~ -
- · -
- - Root -
-
- - {mode === "directory" ? ( -
-
- { - setNewFolderName(event.currentTarget.value); - if (createFolderError) { - setCreateFolderError(null); - } - }} - onKeyDown={(event) => { - if (event.key !== "Enter") return; - event.preventDefault(); - void createFolderInActivePath(); - }} - placeholder={ - activePath - ? t("settings.newFolderNamePlaceholder") - : t("settings.newFolderSelectParentFirst") - } - disabled={!activePath || creatingFolder} - className="h-9" - /> - -
- {createFolderError ? ( -
- - {createFolderError} -
- ) : null} -
- ) : null} - -
- item.data.label} - viewState={viewState} - onExpandItem={(item, treeId) => { - const index = typeof item.index === "string" ? item.index : ""; - if (!index || index === ROOT_ID) return; - updateTreeViewState(treeId, (treePrev) => ({ - ...treePrev, - expandedItems: treePrev.expandedItems?.includes(index) - ? treePrev.expandedItems - : [...(treePrev.expandedItems ?? []), index], - })); - void loadChildren(index); - }} - onCollapseItem={(item, treeId) => { - const index = typeof item.index === "string" ? item.index : ""; - if (!index || index === ROOT_ID) return; - updateTreeViewState(treeId, (treePrev) => ({ - ...treePrev, - expandedItems: (treePrev.expandedItems ?? []).filter( - (entry) => entry !== index, - ), - })); - }} - onSelectItems={(selectedItems, treeId) => { - updateTreeViewState(treeId, (treePrev) => ({ - ...treePrev, - selectedItems, - })); + · +
+ + Root +
+
+ + {mode === "directory" ? ( +
+
+ { + setNewFolderName(event.currentTarget.value); + if (createFolderError) { + setCreateFolderError(null); + } }} - onFocusItem={(item, treeId) => { - updateTreeViewState(treeId, (treePrev) => ({ - ...treePrev, - focusedItem: item.index, - })); + onKeyDown={(event) => { + if (event.key !== "Enter") return; + event.preventDefault(); + void createFolderInActivePath(); }} + placeholder={ + activePath + ? t("settings.newFolderNamePlaceholder") + : t("settings.newFolderSelectParentFirst") + } + disabled={!activePath || creatingFolder} + className="h-9" + /> +
- - {statusLine ? ( -
- {statusLine.kind === "loading" ? ( - - ) : statusLine.kind === "error" ? ( - + {creatingFolder ? ( + ) : ( - + )} - {statusLine.text} + {t("settings.createFolder")} + +
+ {createFolderError ? ( +
+ + {createFolderError}
) : null}
- -
- - + ) : null} + +
+ item.data.label} + viewState={viewState} + onExpandItem={(item, treeId) => { + const index = typeof item.index === "string" ? item.index : ""; + if (!index || index === ROOT_ID) return; + updateTreeViewState(treeId, (treePrev) => ({ + ...treePrev, + expandedItems: treePrev.expandedItems?.includes(index) + ? treePrev.expandedItems + : [...(treePrev.expandedItems ?? []), index], + })); + void loadChildren(index); + }} + onCollapseItem={(item, treeId) => { + const index = typeof item.index === "string" ? item.index : ""; + if (!index || index === ROOT_ID) return; + updateTreeViewState(treeId, (treePrev) => ({ + ...treePrev, + expandedItems: (treePrev.expandedItems ?? []).filter((entry) => entry !== index), + })); + }} + onSelectItems={(selectedItems, treeId) => { + updateTreeViewState(treeId, (treePrev) => ({ + ...treePrev, + selectedItems, + })); + }} + onFocusItem={(item, treeId) => { + updateTreeViewState(treeId, (treePrev) => ({ + ...treePrev, + focusedItem: item.index, + })); + }} + > + + +
+ + {statusLine ? ( +
+ {statusLine.kind === "loading" ? ( + + ) : statusLine.kind === "error" ? ( + + ) : ( + + )} + {statusLine.text}
- - - - + ) : null} + + + + + }>{t("settings.cancel")} + { + if (!canConfirm || !selectedPath) return; + onSelect(selectedPath); + }} + /> + } + > + {t("settings.select")} + + + + + ); } diff --git a/crates/agent-gateway/web/src/pages/settings/DevicesSection.tsx b/crates/agent-gateway/web/src/pages/settings/DevicesSection.tsx index 1321a67ae..3b812c411 100644 --- a/crates/agent-gateway/web/src/pages/settings/DevicesSection.tsx +++ b/crates/agent-gateway/web/src/pages/settings/DevicesSection.tsx @@ -15,12 +15,22 @@ import { X, } from "@liveagent/ui/components/IconSet"; import { Button } from "@liveagent/ui/components/ui/button"; +import { + Dialog, + DialogActions, + DialogBody, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@liveagent/ui/components/ui/dialog"; import { Input } from "@liveagent/ui/components/ui/input"; import { Label } from "@liveagent/ui/components/ui/label"; import { useLocale } from "@liveagent/ui/i18n/index"; +import { cn } from "@liveagent/ui/lib/shared/utils"; import { ConfirmActionPopover } from "@liveagent/ui/pages/settings/shared"; import { useCallback, useEffect, useState } from "react"; -import { createPortal } from "react-dom"; import { type AdminAgentEntry, type AdminAgentStatus, @@ -40,7 +50,10 @@ const STATUS_FILTERS = [ { value: "all", labelKey: "settings.devicesFilterAll" }, { value: "online", labelKey: "settings.devicesFilterOnline" }, { value: "offline", labelKey: "settings.devicesFilterOffline" }, -] as const satisfies ReadonlyArray<{ value: AdminAgentStatus; labelKey: string }>; +] as const satisfies ReadonlyArray<{ + value: AdminAgentStatus; + labelKey: string; +}>; function errorMessage(error: unknown, fallback: string) { return error instanceof Error && error.message.trim() ? error.message : fallback; @@ -73,7 +86,10 @@ export function DevicesSection({ const [newAgentId, setNewAgentId] = useState(""); const [newName, setNewName] = useState(""); const [addDialogOpen, setAddDialogOpen] = useState(false); - const [issuedToken, setIssuedToken] = useState<{ agentId: string; token: string } | null>(null); + const [issuedToken, setIssuedToken] = useState<{ + agentId: string; + token: string; + } | null>(null); const load = useCallback( async ( @@ -301,11 +317,12 @@ export function DevicesSection({ type="button" disabled={loading} onClick={() => handleStatusFilter(value)} - className={`inline-flex h-7 items-center justify-center whitespace-nowrap rounded-md px-3 text-xs font-medium transition-all disabled:pointer-events-none disabled:opacity-50 ${ + className={cn( + "inline-flex h-7 items-center justify-center whitespace-nowrap rounded-md px-3 text-xs font-medium transition-all disabled:pointer-events-none disabled:opacity-50", statusFilter === value ? "bg-background text-foreground shadow" - : "hover:text-foreground/80" - }`} + : "hover:text-foreground/80", + )} > {t(labelKey)} @@ -387,116 +404,87 @@ function AddClientDialog({ }) { const { t } = useLocale(); - useEffect(() => { - function handleKeyDown(event: KeyboardEvent) { - if (event.key === "Escape" && !loading) onClose(); - } - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [loading, onClose]); - - return createPortal( -
- -
+ -
- {error ? ( -
- - {error} + + {error ? ( +
+ + {error} +
+ ) : null} +
+ + onNameChange(event.currentTarget.value)} + />
- ) : null} -
- - onNameChange(event.currentTarget.value)} - /> -
-
- - onAgentIdChange(event.currentTarget.value)} - /> -
-
+
+ + onAgentIdChange(event.currentTarget.value)} + /> +
+ -
- - -
- -
, - document.body, + + + + + + + + + ); } @@ -510,14 +498,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,49 +505,24 @@ function IssuedTokenDialog({ window.setTimeout(() => setCopied(false), 1800); } - return createPortal( -
- -
+ -
+

{t("settings.devicesTokenOnce")}

@@ -592,16 +547,17 @@ function IssuedTokenDialog({ )}
-
+ -
- -
-
-
, - document.body, + + + + + + + ); } @@ -722,7 +678,7 @@ function DeviceRow(props: {
{displayName || agent.agent_id} 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..0229f3d76 --- /dev/null +++ b/crates/agent-gateway/web/src/shims/browserPathPrompt.tsx @@ -0,0 +1,94 @@ +import { Button } from "@liveagent/ui/components/ui/button"; +import { + Dialog, + DialogActions, + DialogBody, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + 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 b0c04326c..d4ac86a0c 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/base-chat.css b/crates/agent-gateway/web/src/styles/base-chat.css index 30e4632ae..91ac9621e 100644 --- a/crates/agent-gateway/web/src/styles/base-chat.css +++ b/crates/agent-gateway/web/src/styles/base-chat.css @@ -555,7 +555,7 @@ html[data-liveagent-webui="gateway"] [role="textbox"]:focus-visible { .gateway-chat-frame { position: relative; - z-index: 1; + z-index: var(--layer-content); display: flex; height: 100%; min-height: 0; @@ -577,7 +577,7 @@ html[data-liveagent-webui="gateway"] [role="textbox"]:focus-visible { .history-share-frame { position: relative; - z-index: 1; + z-index: var(--layer-content); display: flex; height: 100%; min-height: 0; @@ -675,7 +675,7 @@ html[data-liveagent-webui="gateway"] [role="textbox"]:focus-visible { .gateway-history-switch-overlay { position: absolute; inset: 0; - z-index: 18; + z-index: var(--layer-panel); display: flex; align-items: center; justify-content: center; @@ -751,7 +751,7 @@ html[data-liveagent-webui="gateway"] [role="textbox"]:focus-visible { position: absolute; right: 18px; bottom: calc(var(--gateway-chat-composer-overlay-height, 176px) + 12px); - z-index: 12; + z-index: var(--layer-raised); display: inline-flex; height: 42px; width: 42px; @@ -1100,7 +1100,7 @@ html[data-liveagent-webui="gateway"] [role="textbox"]:focus-visible { .gateway-settings-overlay { position: absolute; inset: 0; - z-index: 40; + z-index: var(--layer-panel); min-height: 100%; opacity: 0; transform: translateY(24px); @@ -1139,65 +1139,10 @@ html[data-liveagent-webui="gateway"] .settings-back-bar { } html[data-liveagent-webui="gateway"] .settings-section-actions, -html[data-liveagent-webui="gateway"] .settings-card-actions, -html[data-liveagent-webui="gateway"] .settings-modal-actions { +html[data-liveagent-webui="gateway"] .settings-card-actions { min-width: 0; } 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; - } -} diff --git a/crates/agent-gateway/web/src/styles/responsive.css b/crates/agent-gateway/web/src/styles/responsive.css index b66b20647..1f6d6cddd 100644 --- a/crates/agent-gateway/web/src/styles/responsive.css +++ b/crates/agent-gateway/web/src/styles/responsive.css @@ -114,7 +114,7 @@ .gateway-editor-host > .chat-history-sidebar { position: fixed; inset: 0 auto 0 0; - z-index: 45; + z-index: var(--layer-panel); height: 100svh; height: 100dvh; width: min(86vw, 272px) !important; @@ -168,7 +168,7 @@ html[data-liveagent-webui="gateway"] .project-tools-panel { inset: 0; - z-index: 50; + z-index: var(--layer-panel); width: 100%; height: 100svh; height: 100dvh; @@ -210,7 +210,7 @@ html[data-liveagent-webui="gateway"] .workspace-file-preview-overlay, html[data-liveagent-webui="gateway"] .workspace-image-preview-overlay, html[data-liveagent-webui="gateway"] .workspace-ssh-terminal-overlay { - z-index: 60; + z-index: var(--layer-modal); } html[data-liveagent-webui="gateway"] .workspace-ssh-terminal-tabs { @@ -539,75 +539,6 @@ 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; - max-width: calc(100vw - 16px) !important; - max-height: calc(100dvh - 16px) !important; - flex-direction: column; - overflow: hidden; - border-radius: 16px; - } - - html[data-liveagent-webui="gateway"] .settings-modal-header, - html[data-liveagent-webui="gateway"] .settings-modal-subheader { - flex: 0 0 auto; - padding: 12px 14px; - } - - html[data-liveagent-webui="gateway"] .settings-modal-body { - min-height: 0; - overflow-y: auto; - } - - html[data-liveagent-webui="gateway"] .settings-modal-body.px-6 { - padding-left: 14px; - padding-right: 14px; - } - - html[data-liveagent-webui="gateway"] .settings-modal-body.py-5, - html[data-liveagent-webui="gateway"] .settings-modal-body.py-4 { - padding-top: 14px; - padding-bottom: 14px; - } - - html[data-liveagent-webui="gateway"] .settings-modal-body > .px-6 { - padding-left: 14px; - padding-right: 14px; - } - - html[data-liveagent-webui="gateway"] .settings-modal-body > .py-5, - html[data-liveagent-webui="gateway"] .settings-modal-body > .py-4 { - padding-top: 14px; - padding-bottom: 14px; - } - - html[data-liveagent-webui="gateway"] .settings-modal-footer { - align-items: stretch; - flex: 0 0 auto; - flex-direction: column; - gap: 10px; - padding: 12px 14px; - } - - html[data-liveagent-webui="gateway"] .settings-modal-footer.settings-modal-footer-row { - flex-direction: row; - align-items: center; - justify-content: flex-end; - flex-wrap: wrap; - gap: 8px; - } - - html[data-liveagent-webui="gateway"] .settings-modal-actions { - width: 100%; - justify-content: flex-end; - } - html[data-liveagent-webui="gateway"] .settings-tools-header { align-items: stretch; flex-direction: column; @@ -653,12 +584,6 @@ min-width: 40px; } - html[data-liveagent-webui="gateway"] .settings-modal-step-row { - align-items: flex-start; - flex-direction: column; - gap: 10px; - } - html[data-liveagent-webui="gateway"] .settings-http-row { flex-wrap: wrap; align-items: stretch; @@ -813,33 +738,6 @@ line-height: 1.5; } - html[data-liveagent-webui="gateway"] .settings-cron-view-panel { - height: calc(100dvh - 16px) !important; - flex-direction: column; - } - - html[data-liveagent-webui="gateway"] .settings-cron-view-left { - width: 100% !important; - max-height: 42%; - border-right: 0; - border-bottom: 1px solid hsl(var(--border) / 0.4); - } - - html[data-liveagent-webui="gateway"] .settings-cron-view-right { - min-height: 0; - } - - /* Stacked layout: the logs header (desktop close location) sits mid-modal, - so surface the hero copy of the close button at the modal's top-right, - right of run-now, and hide the logs-header one. */ - html[data-liveagent-webui="gateway"] .settings-cron-view-hero-close { - display: flex; - } - - html[data-liveagent-webui="gateway"] .settings-cron-logs-close { - display: none; - } - html[data-liveagent-webui="gateway"] .settings-log-row { flex-wrap: wrap; } @@ -1120,38 +1018,10 @@ flex-direction: column; } - html[data-liveagent-webui="gateway"] .settings-inline-form > button, - html[data-liveagent-webui="gateway"] .settings-modal-actions > button { + html[data-liveagent-webui="gateway"] .settings-inline-form > button { width: 100%; } - html[data-liveagent-webui="gateway"] .settings-modal-actions { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - html[data-liveagent-webui="gateway"] .settings-modal-footer > .text-xs, - html[data-liveagent-webui="gateway"] .settings-modal-footer > .min-w-0 { - min-width: 0; - } - - html[data-liveagent-webui="gateway"] .settings-modal-footer .truncate { - white-space: normal; - } - - html[data-liveagent-webui="gateway"] .settings-modal-subheader .settings-field-row { - align-items: stretch; - flex-direction: column; - gap: 8px; - } - - html[data-liveagent-webui="gateway"] - .settings-modal-subheader - .settings-field-row - > div:first-child { - width: auto; - } - /* Tool detail modal becomes a bottom sheet on small screens. */ html[data-liveagent-webui="gateway"] .settings-tool-detail-overlay { align-items: flex-end; @@ -1243,14 +1113,6 @@ height: 30px; width: 30px; } - - html[data-liveagent-webui="gateway"] .settings-cron-logs-header { - flex-wrap: wrap; - } - - html[data-liveagent-webui="gateway"] .settings-cron-logs-header > div { - margin-left: auto; - } } @media (max-width: 640px) and (prefers-reduced-motion: reduce) { @@ -1306,10 +1168,6 @@ padding: 7px 9px; } - html[data-liveagent-webui="gateway"] .settings-modal-panel { - border-radius: 14px; - } - html[data-liveagent-webui="gateway"] .gateway-project-tools-panel-toggle { margin-right: 8px; } diff --git a/crates/agent-gui/src/App.tsx b/crates/agent-gui/src/App.tsx index af4f5ff74..5cbbabfdb 100644 --- a/crates/agent-gui/src/App.tsx +++ b/crates/agent-gui/src/App.tsx @@ -11,6 +11,7 @@ import { } from "@liveagent/ui/lib/settings/sync"; import { useSettingsOverlay } from "@liveagent/ui/lib/settings/useSettingsOverlay"; import { applyFontFamilies } from "@liveagent/ui/lib/shared/fontFamily"; +import { cn } from "@liveagent/ui/lib/shared/utils"; import { SettingsPage } from "@liveagent/ui/pages/settings/SettingsPage"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; @@ -642,9 +643,10 @@ export default function App() { {visible && (
@@ -667,7 +669,7 @@ export default function App() { void invoke("app_toggle_window_pin").catch(() => {}); }} title={translate("app.windowPinnedHint", settings.locale)} - className="absolute top-3 left-1/2 z-[60] flex -translate-x-1/2 items-center gap-1.5 rounded-full border border-primary/30 bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary shadow-sm backdrop-blur transition-colors hover:bg-primary/20" + className="layer-toast absolute top-3 left-1/2 flex -translate-x-1/2 items-center gap-1.5 rounded-full border border-primary/30 bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary shadow-sm backdrop-blur transition-colors hover:bg-primary/20" > {translate("app.windowPinned", settings.locale)} diff --git a/crates/agent-gui/src/agent-ui-adapters/providerSettings.tsx b/crates/agent-gui/src/agent-ui-adapters/providerSettings.tsx index 435ba256f..71f0ed4ec 100644 --- a/crates/agent-gui/src/agent-ui-adapters/providerSettings.tsx +++ b/crates/agent-gui/src/agent-ui-adapters/providerSettings.tsx @@ -5,9 +5,18 @@ import { Key, Loader2, RefreshCw, - X, } from "@liveagent/ui/components/IconSet"; import { Button } from "@liveagent/ui/components/ui/button"; +import { + Dialog, + DialogActions, + DialogBody, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@liveagent/ui/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, @@ -24,7 +33,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"; @@ -260,28 +268,24 @@ function CcsImportModal(props: { }); } - return createPortal( -
- -
-
+ + {rows.length > 0 ? ( rows.map((item) => { const identity = ccsImportIdentity({ @@ -323,33 +327,34 @@ function CcsImportModal(props: { 当前类型未发现可导入配置
)} -
+ {result ? (
{result}
) : null} -
- - -
-
-
, - document.body, + + + + + + + + ); } @@ -415,7 +420,11 @@ export function ProviderSettingsExtension(props: { ); return { id: provider.id, models, ok: true }; } catch { - return { id: provider.id, models: [] as ProviderModelConfig[], ok: false }; + return { + id: provider.id, + models: [] as ProviderModelConfig[], + ok: false, + }; } }), ); @@ -426,7 +435,11 @@ export function ProviderSettingsExtension(props: { const result = results.find((item) => item.id === provider.id); if (!result?.ok) return provider; const models = mergeFetchedModels(result.models, provider.models); - return { ...provider, models, activeModels: models.map((model) => model.id) }; + return { + ...provider, + models, + activeModels: models.map((model) => model.id), + }; }), ), ); @@ -505,7 +518,11 @@ export function ProviderSettingsExtension(props: { const result = results.find((item) => item.id === provider.id); if (!result?.ok) return provider; const models = mergeFetchedModels(result.models, provider.models); - return { ...provider, models, activeModels: models.map((model) => model.id) }; + return { + ...provider, + models, + activeModels: models.map((model) => model.id), + }; }), ), ); diff --git a/crates/agent-gui/src/components/WindowsTitleBar.tsx b/crates/agent-gui/src/components/WindowsTitleBar.tsx index a6bdcae06..596df46a4 100644 --- a/crates/agent-gui/src/components/WindowsTitleBar.tsx +++ b/crates/agent-gui/src/components/WindowsTitleBar.tsx @@ -186,7 +186,7 @@ export function WindowsTitleBar() { diff --git a/crates/agent-gui/src/components/input-context-menu/NativeInputContextMenu.tsx b/crates/agent-gui/src/components/input-context-menu/NativeInputContextMenu.tsx index 587199224..2eb81c487 100644 --- a/crates/agent-gui/src/components/input-context-menu/NativeInputContextMenu.tsx +++ b/crates/agent-gui/src/components/input-context-menu/NativeInputContextMenu.tsx @@ -401,7 +401,7 @@ export function useNativeInputContextMenu(): { ref={menuRef} role="menu" className={cn( - "editor-context-menu fixed z-[10000] w-max min-w-[9.5rem] max-w-[calc(100vw-1.5rem)] select-none overflow-hidden rounded-lg border border-border/70 bg-popover p-1.5 text-popover-foreground shadow-[0_20px_60px_-20px_rgba(15,23,42,0.35)]", + "editor-context-menu layer-popover fixed w-max min-w-[9.5rem] max-w-[calc(100vw-1.5rem)] select-none overflow-hidden rounded-lg border border-border/70 bg-popover p-1.5 text-popover-foreground shadow-[0_20px_60px_-20px_rgba(15,23,42,0.35)]", isExiting && "editor-context-menu-exit", )} style={{ left: renderedSnapshot.x, top: renderedSnapshot.y }} diff --git a/crates/agent-gui/src/pages/chat/components/AssistantBubble.tsx b/crates/agent-gui/src/pages/chat/components/AssistantBubble.tsx index 3faac6330..4df313e0c 100644 --- a/crates/agent-gui/src/pages/chat/components/AssistantBubble.tsx +++ b/crates/agent-gui/src/pages/chat/components/AssistantBubble.tsx @@ -4,6 +4,7 @@ import { RoundBlockContent } from "@liveagent/ui/components/chat/assistant-bubbl import { RetryDetailsBlock } from "@liveagent/ui/components/chat/RetryDetailsBlock"; import { UsagePanel } from "@liveagent/ui/components/chat/UsagePanel"; import type { ChatFileLink } from "@liveagent/ui/lib/chat/chatFileLinks"; +import { cn } from "@liveagent/ui/lib/shared/utils"; import { memo } from "react"; import type { RetryAttemptRecord } from "../../../lib/chat/conversation/liveTranscriptStore"; import type { AssistantUnitRow } from "../transcript/rowModel"; @@ -55,9 +56,10 @@ export const AssistantBubbleUnit = memo(function AssistantBubbleUnit(props: {