feat(desktop): auto-collapse both sidebars below 600px into hover-reveal

Add a Pane `forceCollapsed` prop — collapses the track without writing to
the store (so the saved open state restores when the window widens) while
keeping hoverReveal alive (unlike `disabled`, which suppresses it).

desktop-controller watches (max-width: 600px) and force-collapses the chat
sidebar + file browser, so on a narrow window both rails get out of the way
and the hover-reveal overlay becomes the way in.
This commit is contained in:
Brooklyn Nicholson 2026-06-07 21:01:40 -05:00
parent a1e78dee2d
commit e6f437e6c0
2 changed files with 12 additions and 1 deletions

View file

@ -8,6 +8,7 @@ import { DesktopInstallOverlay } from '@/components/desktop-install-overlay'
import { DesktopOnboardingOverlay } from '@/components/desktop-onboarding-overlay'
import { GatewayConnectingOverlay } from '@/components/gateway-connecting-overlay'
import { Pane, PaneMain } from '@/components/pane-shell'
import { useMediaQuery } from '@/hooks/use-media-query'
import { useSkinCommand } from '@/themes/use-skin-command'
import { formatRefValue } from '../components/assistant-ui/directive-text'
@ -166,6 +167,10 @@ export function DesktopController() {
const terminalTakeover = useStore($terminalTakeover)
const panesFlipped = useStore($panesFlipped)
const profileScope = useStore($profileScope)
// Below 600px there's no room for a docked rail — collapse both sidebars
// (without touching their stored open state) so the hover-reveal overlay
// becomes the way in. Restores to the saved layout once it's wide again.
const narrowViewport = useMediaQuery('(max-width: 600px)')
const routedSessionId = routeSessionId(location.pathname)
const routeToken = `${location.pathname}:${location.search}:${location.hash}`
@ -848,6 +853,7 @@ export function DesktopController() {
<Pane
defaultOpen={false}
disabled={!chatOpen}
forceCollapsed={narrowViewport}
hoverReveal
id="file-browser"
key="file-browser"
@ -876,6 +882,7 @@ export function DesktopController() {
>
<Pane
disabled={terminalTakeoverActive}
forceCollapsed={narrowViewport}
hoverReveal
id="chat-sidebar"
maxWidth={SIDEBAR_MAX_WIDTH}

View file

@ -32,6 +32,8 @@ export interface PaneProps {
defaultOpen?: boolean
/** Forces the pane closed (track→0, aria-hidden) without writing to the store — for transient route gates. */
disabled?: boolean
/** Like disabled, but keeps hoverReveal alive — collapses the track without writing to the store (e.g. narrow window). */
forceCollapsed?: boolean
/** When collapsed, float the contents over the main column on hover/focus instead of hiding them (track stays 0px). */
hoverReveal?: boolean
/** Called with the reveal state whenever a collapsed hoverReveal pane floats in/out. */
@ -58,6 +60,7 @@ export interface PaneShellProps {
interface CollectedPane {
defaultOpen: boolean
disabled: boolean
forceCollapsed: boolean
id: string
resizable: boolean
side: PaneSide
@ -115,6 +118,7 @@ function collectPanes(children: ReactNode) {
const entry: CollectedPane = {
defaultOpen: props.defaultOpen ?? true,
disabled: props.disabled ?? false,
forceCollapsed: props.forceCollapsed ?? false,
id: props.id,
resizable: props.resizable ?? false,
side: props.side,
@ -129,7 +133,7 @@ function collectPanes(children: ReactNode) {
function trackForPane(pane: CollectedPane, states: Record<string, { open: boolean; widthOverride?: number }>) {
const stateOpen = states[pane.id]?.open ?? pane.defaultOpen
const open = !pane.disabled && stateOpen
const open = !pane.disabled && !pane.forceCollapsed && stateOpen
if (!open) {
return { open: false, track: '0px' }