diff --git a/apps/desktop/src/app/chat/sidebar/index.tsx b/apps/desktop/src/app/chat/sidebar/index.tsx index ef1832837f3..e8a2c3827e3 100644 --- a/apps/desktop/src/app/chat/sidebar/index.tsx +++ b/apps/desktop/src/app/chat/sidebar/index.tsx @@ -580,7 +580,12 @@ export function ChatSidebar({ panesFlipped ? 'border-l border-r-0' : 'border-r border-l-0', sidebarOpen ? 'border-(--sidebar-edge-border) bg-(--ui-sidebar-surface-background) opacity-100' - : 'pointer-events-none border-transparent bg-transparent opacity-0' + : 'pointer-events-none border-transparent bg-transparent opacity-0', + // Hover-reveal overlay: when collapsed, the PaneShell floats this + // sidebar over the content and marks the wrapper `data-pane-hover-reveal`. + // Force it fully visible + interactive while revealed, regardless of the + // collapsed (sidebarOpen=false) styling above. + 'in-data-[pane-hover-reveal=open]:pointer-events-auto in-data-[pane-hover-reveal=open]:border-(--sidebar-edge-border) in-data-[pane-hover-reveal=open]:bg-(--ui-sidebar-surface-background) in-data-[pane-hover-reveal=open]:opacity-100' )} collapsible="none" > diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index 15466d20950..c0a8d4809c0 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -873,6 +873,7 @@ export function DesktopController() { > (null) + const [hoverRevealed, setHoverRevealed] = useState(false) useEffect(() => { if (registered.current) { @@ -218,6 +230,24 @@ export function Pane({ const hi = widthToPx(maxWidth) ?? Number.POSITIVE_INFINITY const side = slot?.side ?? 'left' + // Collapsed + hoverReveal: float the pane contents over the main column on + // hover/focus instead of leaving them fully hidden. Honors any persisted + // resize width so the overlay matches the pane's expanded size. + const overlayActive = !open && hoverReveal && !disabled + + const overlayWidth = + resizable && paneStates[id]?.widthOverride !== undefined + ? `${paneStates[id]?.widthOverride}px` + : widthToCss(width, DEFAULT_WIDTH) + + // Collapse the reveal whenever the pane reopens or gets disabled so it never + // sticks "open" underneath the now-expanded track. + useEffect(() => { + if (!overlayActive && hoverRevealed) { + setHoverRevealed(false) + } + }, [overlayActive, hoverRevealed]) + const startResize = useCallback( (event: ReactPointerEvent) => { const paneWidth = paneRef.current?.getBoundingClientRect().width ?? 0 @@ -273,6 +303,61 @@ export function Pane({ return null } + // Collapsed hover-reveal track: keep the grid cell at 0px (no reserved space) + // but don't clip — the trigger strip and the floating overlay both escape the + // zero-width box via absolute positioning, so the panel renders over the main + // content rather than pushing it. + if (overlayActive) { + const revealEdge = slot.side === 'left' ? { left: 0 } : { right: 0 } + + return ( +
+ {/* Edge hot-zone: hovering it (or moving onto the revealed overlay) + keeps the panel open. Sits above main content but is invisible. */} +
+ ) + } + return (