diff --git a/webui/src/App.tsx b/webui/src/App.tsx index 8fa428a7b..4a104838d 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Menu, Moon, Sun } from "lucide-react"; +import { Moon, PanelLeft, Sun } from "lucide-react"; import { useTranslation } from "react-i18next"; import { DeleteConfirm } from "@/components/DeleteConfirm"; import { RenameChatDialog } from "@/components/RenameChatDialog"; @@ -264,11 +264,17 @@ function normalizeWorkspaceScope(scope: WorkspaceScopePayload): WorkspaceScopePa function HostChrome({ onToggleSidebar, + onSidebarPreviewEnter, + onSidebarPreviewLeave, + sidebarOpen = true, theme, onToggleTheme, showThemeButton = true, }: { onToggleSidebar?: () => void; + onSidebarPreviewEnter?: () => void; + onSidebarPreviewLeave?: () => void; + sidebarOpen?: boolean; theme: "light" | "dark"; onToggleTheme: () => void; showThemeButton?: boolean; @@ -276,21 +282,24 @@ function HostChrome({ const { t } = useTranslation(); return ( -
-
- {onToggleSidebar ? ( - - ) : null} -
+
+ {onToggleSidebar ? ( + + ) : null} {showThemeButton ? ( ) : ( -
+ null )}
); @@ -532,6 +541,7 @@ function Shell({ useState(initialRouteRef.current.settingsSection); const [hostSidebarOpen, setHostSidebarOpen] = useState(readSidebarOpen); + const [hostSidebarPreviewOpen, setHostSidebarPreviewOpen] = useState(false); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [sessionSearchOpen, setSessionSearchOpen] = useState(false); const [pendingDelete, setPendingDelete] = useState<{ @@ -560,6 +570,11 @@ function Shell({ useState>({}); const runningChatIdsRef = useRef>(new Set()); const activeChatIdRef = useRef(null); + const hostSidebarPreviewCloseTimerRef = useRef(null); + const effectiveRuntimeSurface = + settingsSnapshot?.surface ?? settingsSnapshot?.runtime_surface ?? runtimeSurface; + const showHostChrome = effectiveRuntimeSurface === "native"; + const showMainSidebar = view !== "settings"; const navigate = useCallback( (route: ShellRoute, options?: { replace?: boolean }) => { @@ -745,13 +760,74 @@ function Shell({ }); }, [client, loading, sessions]); - const closeHostSidebar = useCallback(() => { - setHostSidebarOpen(false); + const clearHostSidebarPreviewCloseTimer = useCallback(() => { + if (hostSidebarPreviewCloseTimerRef.current === null) return; + window.clearTimeout(hostSidebarPreviewCloseTimerRef.current); + hostSidebarPreviewCloseTimerRef.current = null; }, []); + const closeHostSidebarPreview = useCallback(() => { + clearHostSidebarPreviewCloseTimer(); + setHostSidebarPreviewOpen(false); + }, [clearHostSidebarPreviewCloseTimer]); + + const openHostSidebarPreview = useCallback(() => { + if (!showHostChrome || !showMainSidebar || hostSidebarOpen) return; + clearHostSidebarPreviewCloseTimer(); + setHostSidebarPreviewOpen(true); + }, [ + clearHostSidebarPreviewCloseTimer, + hostSidebarOpen, + showHostChrome, + showMainSidebar, + ]); + + const scheduleHostSidebarPreviewClose = useCallback(() => { + clearHostSidebarPreviewCloseTimer(); + if (!showHostChrome || !showMainSidebar || hostSidebarOpen) { + setHostSidebarPreviewOpen(false); + return; + } + hostSidebarPreviewCloseTimerRef.current = window.setTimeout(() => { + setHostSidebarPreviewOpen(false); + hostSidebarPreviewCloseTimerRef.current = null; + }, 160); + }, [ + clearHostSidebarPreviewCloseTimer, + hostSidebarOpen, + showHostChrome, + showMainSidebar, + ]); + + useEffect(() => { + return () => clearHostSidebarPreviewCloseTimer(); + }, [clearHostSidebarPreviewCloseTimer]); + + useEffect(() => { + if (!showHostChrome || !showMainSidebar || hostSidebarOpen) { + closeHostSidebarPreview(); + } + }, [ + closeHostSidebarPreview, + hostSidebarOpen, + showHostChrome, + showMainSidebar, + ]); + + const closeHostSidebar = useCallback(() => { + closeHostSidebarPreview(); + setHostSidebarOpen(false); + }, [closeHostSidebarPreview]); + const openHostSidebar = useCallback(() => { + closeHostSidebarPreview(); setHostSidebarOpen(true); - }, []); + }, [closeHostSidebarPreview]); + + const toggleHostSidebar = useCallback(() => { + closeHostSidebarPreview(); + setHostSidebarOpen((v) => !v); + }, [closeHostSidebarPreview]); const closeMobileSidebar = useCallback(() => { setMobileSidebarOpen(false); @@ -762,11 +838,12 @@ function Shell({ typeof window !== "undefined" && window.matchMedia("(min-width: 1024px)").matches; if (isNativeHost) { + closeHostSidebarPreview(); setHostSidebarOpen((v) => !v); } else { setMobileSidebarOpen((v) => !v); } - }, []); + }, [closeHostSidebarPreview]); const applyWorkspaceScope = useCallback( (scope: WorkspaceScopePayload) => { @@ -1041,6 +1118,10 @@ function Shell({ setMobileSidebarOpen(false); }, [activeKey, navigate]); + const onOpenModelSettings = useCallback(() => { + onOpenSettings("models"); + }, [onOpenSettings]); + const onOpenApps = useCallback(() => { setSessionSearchOpen(false); navigate({ view: "apps", activeKey, settingsSection: "apps" }); @@ -1238,11 +1319,13 @@ function Shell({ archivedCount: sidebarState.archived_keys.length, defaultWorkspacePath: workspaces?.default_scope.project_path ?? null, }; - const effectiveRuntimeSurface = - settingsSnapshot?.surface ?? settingsSnapshot?.runtime_surface ?? runtimeSurface; - const isNativeHostSetupSurface = effectiveRuntimeSurface === "native"; - const showHostChrome = isNativeHostSetupSurface; - const showMainSidebar = view !== "settings"; + const hostSidebarCollapsed = showHostChrome && !hostSidebarOpen; + const showHostSidebarPreview = + showMainSidebar && hostSidebarCollapsed && hostSidebarPreviewOpen; + const hostSidebarFlowWidth = showHostChrome + ? (hostSidebarOpen ? SIDEBAR_WIDTH : 0) + : (hostSidebarOpen ? SIDEBAR_WIDTH : SIDEBAR_RAIL_WIDTH); + const renderHostSidebarFlowContent = !showHostChrome || hostSidebarOpen; useEffect(() => { document.documentElement.classList.toggle("native-host", showHostChrome); @@ -1261,7 +1344,10 @@ function Shell({ > {showHostChrome ? ( @@ -1274,25 +1360,47 @@ function Shell({ {/* Host sidebar: in normal flow, so the thread area width stays honest. */} {showMainSidebar ? ( + ) : null} + + {showHostSidebarPreview ? ( +