diff --git a/apps/desktop/src/app/hooks/use-keybinds.ts b/apps/desktop/src/app/hooks/use-keybinds.ts index dc25f42b77..9fbd662a75 100644 --- a/apps/desktop/src/app/hooks/use-keybinds.ts +++ b/apps/desktop/src/app/hooks/use-keybinds.ts @@ -2,11 +2,14 @@ import { useEffect, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { setRightSidebarTab } from '@/app/right-sidebar/store' +import { PANE_TOGGLE_REVEAL_EVENT } from '@/components/pane-shell' import { PROFILE_SLOT_COUNT } from '@/lib/keybinds/actions' import { comboAllowedInInput, comboFromEvent, isEditableTarget } from '@/lib/keybinds/combo' import { toggleCommandPalette } from '@/store/command-palette' import { $capture, $comboIndex, endCapture, setBinding, toggleKeybindPanel } from '@/store/keybinds' import { + CHAT_SIDEBAR_PANE_ID, + FILE_BROWSER_PANE_ID, requestSessionSearchFocus, setFileBrowserOpen, toggleFileBrowserOpen, @@ -109,8 +112,14 @@ export function useKeybinds(deps: KeybindRuntimeDeps): void { 'session.focusSearch': requestSessionSearchFocus, 'session.togglePin': deps.toggleSelectedPin, - 'view.toggleSidebar': toggleSidebarOpen, - 'view.toggleRightSidebar': toggleFileBrowserOpen, + 'view.toggleSidebar': () => { + window.dispatchEvent(new CustomEvent(PANE_TOGGLE_REVEAL_EVENT, { detail: { id: CHAT_SIDEBAR_PANE_ID } })) + toggleSidebarOpen() + }, + 'view.toggleRightSidebar': () => { + window.dispatchEvent(new CustomEvent(PANE_TOGGLE_REVEAL_EVENT, { detail: { id: FILE_BROWSER_PANE_ID } })) + toggleFileBrowserOpen() + }, 'view.showFiles': () => showRightSidebarTab('files'), 'view.showTerminal': () => showRightSidebarTab('terminal'), 'view.flipPanes': togglePanesFlipped, diff --git a/apps/desktop/src/components/pane-shell/index.ts b/apps/desktop/src/components/pane-shell/index.ts index 40946890cf..1874b4bf00 100644 --- a/apps/desktop/src/components/pane-shell/index.ts +++ b/apps/desktop/src/components/pane-shell/index.ts @@ -1,4 +1,4 @@ export type { PaneShellContextValue, PaneSlot } from './context' export { PaneShellContext } from './context' -export { Pane, PaneMain, PaneShell } from './pane-shell' +export { Pane, PANE_TOGGLE_REVEAL_EVENT, PaneMain, PaneShell } from './pane-shell' export type { PaneMainProps, PaneProps, PaneShellProps } from './pane-shell' diff --git a/apps/desktop/src/components/pane-shell/pane-shell.tsx b/apps/desktop/src/components/pane-shell/pane-shell.tsx index 05bbacdf0f..ae7d24f4a6 100644 --- a/apps/desktop/src/components/pane-shell/pane-shell.tsx +++ b/apps/desktop/src/components/pane-shell/pane-shell.tsx @@ -80,6 +80,10 @@ const HOVER_INTENT_SENSITIVITY = 5 // px; below this between polls === settled const HOVER_REVEAL_SLIDE_MS = 260 // panel slide-in duration; inert until elapsed const HOVER_REVEAL_GRACE = 24 // px slop around the panel before a revealed pane closes +// Fired (window CustomEvent<{ id }>) to toggle a force-collapsed pane's reveal +// from the keyboard, since its store-open toggle is a no-op while collapsed. +export const PANE_TOGGLE_REVEAL_EVENT = 'hermes:pane-toggle-reveal' + const widthToCss = (value: WidthValue | undefined, fallback: string) => value === undefined ? fallback : typeof value === 'number' ? `${value}px` : value @@ -234,6 +238,15 @@ export function Pane({ // then so the cursor never flips or lands on a row before it's in view. const [interactive, setInteractive] = useState(false) + const slot = ctx?.paneById.get(id) + const open = Boolean(slot?.open && !disabled) + // Collapsed + hoverReveal: float the pane contents over the main column on + // hover/focus instead of hiding them. Honors any persisted resize width. + const overlayActive = !open && hoverReveal && !disabled + const override = resizable ? paneStates[id]?.widthOverride : undefined + const overlayWidth = override !== undefined ? `${override}px` : widthToCss(width, DEFAULT_WIDTH) + const revealed = overlayActive && hoverRevealed + const stopPoll = useCallback(() => { if (pollId.current !== null) { clearTimeout(pollId.current) @@ -301,6 +314,25 @@ export function Pane({ useEffect(() => stopPoll, [stopPoll]) + // Keyboard toggle (mod+b / mod+j) routes here when the track is force-collapsed + // (narrow window) so the shortcut still does something — it flips the reveal. + useEffect(() => { + if (typeof window === 'undefined' || !overlayActive) { + return + } + + const onToggle = (e: Event) => { + if ((e as CustomEvent<{ id: string }>).detail?.id === id) { + stopPoll() + setHoverRevealed(v => !v) + } + } + + window.addEventListener(PANE_TOGGLE_REVEAL_EVENT, onToggle) + + return () => window.removeEventListener(PANE_TOGGLE_REVEAL_EVENT, onToggle) + }, [id, overlayActive, stopPoll]) + // While revealed, drive close off cursor geometry rather than pointer-events // bookkeeping: a panel that slid in under a still cursor never fires // pointerenter/leave, so listen on the document and close once the cursor @@ -356,20 +388,11 @@ export function Pane({ ensurePaneRegistered(id, { open: defaultOpen }) }, [defaultOpen, id]) - const slot = ctx?.paneById.get(id) - const open = Boolean(slot?.open && !disabled) const canResize = open && resizable const lo = widthToPx(minWidth) ?? DEFAULT_RESIZE_MIN_WIDTH 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 hiding them. Honors any persisted resize width. - const overlayActive = !open && hoverReveal && !disabled - const override = resizable ? paneStates[id]?.widthOverride : undefined - const overlayWidth = override !== undefined ? `${override}px` : widthToCss(width, DEFAULT_WIDTH) - const revealed = overlayActive && hoverRevealed - // Reset stale reveal state when the track reopens/disables, and surface the // effective state so consumers can render full content while floated. useEffect(() => {