diff --git a/src/App.tsx b/src/App.tsx index 89e3c923c..975e9a3a8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,6 +35,7 @@ import { checkAllEnvConflicts, checkEnvConflicts } from "@/lib/api/env"; import { useProviderActions } from "@/hooks/useProviderActions"; import { openclawKeys } from "@/hooks/useOpenClaw"; import { useProxyStatus } from "@/hooks/useProxyStatus"; +import { useAutoCompact } from "@/hooks/useAutoCompact"; import { useLastValidValue } from "@/hooks/useLastValidValue"; import { extractErrorMessage } from "@/utils/errorUtils"; import { isTextEditableTarget } from "@/utils/domUtils"; @@ -200,6 +201,9 @@ function App() { const effectiveEditingProvider = useLastValidValue(editingProvider); const effectiveUsageProvider = useLastValidValue(usageProvider); + const toolbarRef = useRef(null); + const isToolbarCompact = useAutoCompact(toolbarRef); + const promptPanelRef = useRef(null); const mcpPanelRef = useRef(null); const skillsPageRef = useRef(null); @@ -961,250 +965,252 @@ function App() {
- {currentView === "prompts" && ( - - )} - {currentView === "mcp" && ( - <> +
+ {currentView === "prompts" && ( - - - )} - {currentView === "skills" && ( - <> - - - - - )} - {currentView === "skillsDiscovery" && ( - <> - - - - )} - {currentView === "providers" && ( - <> - {activeApp !== "opencode" && - activeApp !== "openclaw" && - settingsData?.enableLocalProxy && ( - <> - -
+ + + + )} + {currentView === "skills" && ( + <> + + + + + )} + {currentView === "skillsDiscovery" && ( + <> + + + + )} + {currentView === "providers" && ( + <> + {activeApp !== "opencode" && + activeApp !== "openclaw" && + settingsData?.enableLocalProxy && ( + <> + +
+ +
+ + )} + + + +
+ + - -
- - )} + {activeApp === "openclaw" ? ( + <> + + + + + + + ) : ( + <> + + + + + + )} + + +
- = 4 - } - /> - -
- - - {activeApp === "openclaw" ? ( - <> - - - - - - - ) : ( - <> - - - - - - )} - - -
- - - - )} + + + )} +
diff --git a/src/components/AppSwitcher.tsx b/src/components/AppSwitcher.tsx index 3be13a0d9..933347bcf 100644 --- a/src/components/AppSwitcher.tsx +++ b/src/components/AppSwitcher.tsx @@ -63,7 +63,11 @@ export function AppSwitcher({ name={appDisplayName[app]} size={iconSize} /> - {!compact && {appDisplayName[app]}} + {!compact && ( + + {appDisplayName[app]} + + )} ))} diff --git a/src/hooks/useAutoCompact.ts b/src/hooks/useAutoCompact.ts new file mode 100644 index 000000000..52b02fb32 --- /dev/null +++ b/src/hooks/useAutoCompact.ts @@ -0,0 +1,43 @@ +import { useEffect, useRef, useState, type RefObject } from "react"; + +/** + * Detects whether the container's children overflow the available width + * and returns a `compact` flag for the AppSwitcher. + * + * Uses ResizeObserver on a flex-constrained container. The container + * must have `flex-1 min-w-0 overflow-hidden` so its width is determined + * by the parent layout, not its own content — avoiding the oscillation + * problem when toggling compact mode. + */ +export function useAutoCompact( + containerRef: RefObject, +): boolean { + const [compact, setCompact] = useState(false); + const normalWidthRef = useRef(0); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + + const ro = new ResizeObserver(() => { + if (!compact) { + // Cache the total content width in normal mode + normalWidthRef.current = el.scrollWidth; + // Overflow detected → switch to compact + if (el.scrollWidth > el.clientWidth + 1) { + setCompact(true); + } + } else if (normalWidthRef.current > 0) { + // In compact mode: only recover to normal if + // available space >= what normal mode needed + if (el.clientWidth >= normalWidthRef.current) { + setCompact(false); + } + } + }); + ro.observe(el); + return () => ro.disconnect(); + }, [compact]); + + return compact; +}