From 2dd4a7a3a6c84f2314b93b827754860f68a7dbf1 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Thu, 22 Jan 2026 14:12:19 +0200 Subject: [PATCH] feature: header layout changes (#195) * fix(ui): remove fixed sessions button and mac titlebar coupling Remove fixed sessions button from header on desktop Mac Eliminate Mac titlebar spacer and drag-to-dock logic in sidebar Update layout to rely on standard header/sidebar without mac-specific tweaks * feat: improve mobile header with session toggle and back button Add back button in mobile header to exit session switcher Show Sessions label when session switcher is open in mobile header Toggle between opening sessions and back navigation based on session state --- packages/ui/src/components/layout/Header.tsx | 257 +++++++----------- .../ui/src/components/layout/MainLayout.tsx | 56 ++-- packages/ui/src/components/layout/Sidebar.tsx | 34 +-- 3 files changed, 130 insertions(+), 217 deletions(-) diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index a4ceb5ae..71a8827b 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -5,7 +5,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; -import { RiChat4Line, RiCodeLine, RiCommandLine, RiFolder6Line, RiGitBranchLine, RiLayoutLeftLine, RiPlayListAddLine, RiQuestionLine, RiSettings3Line, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react'; +import { RiArrowLeftSLine, RiChat4Line, RiCodeLine, RiCommandLine, RiFolder6Line, RiGitBranchLine, RiLayoutLeftLine, RiPlayListAddLine, RiQuestionLine, RiSettings3Line, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react'; import { useUIStore, type MainTab } from '@/stores/useUIStore'; import { useUpdateStore } from '@/stores/useUpdateStore'; import { useConfigStore } from '@/stores/useConfigStore'; @@ -24,71 +24,12 @@ interface TabConfig { showDot?: boolean; } -export const FixedSessionsButton: React.FC = () => { - const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen); - const toggleSidebar = useUIStore((state) => state.toggleSidebar); - const { isMobile } = useDeviceInfo(); - - const [isDesktopApp, setIsDesktopApp] = React.useState(() => { - if (typeof window === 'undefined') { - return false; - } - return typeof (window as typeof window & { opencodeDesktop?: unknown }).opencodeDesktop !== 'undefined'; - }); - - const isMacPlatform = React.useMemo(() => { - if (typeof navigator === 'undefined') { - return false; - } - return /Macintosh|Mac OS X/.test(navigator.userAgent || ''); - }, []); - - React.useEffect(() => { - if (typeof window === 'undefined') { - return; - } - const detected = typeof (window as typeof window & { opencodeDesktop?: unknown }).opencodeDesktop !== 'undefined'; - setIsDesktopApp(detected); - }, []); - - const handleOpenSessionSwitcher = React.useCallback(() => { - if (isMobile) { - setSessionSwitcherOpen(true); - } else { - toggleSidebar(); - } - }, [isMobile, setSessionSwitcherOpen, toggleSidebar]); - - const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center rounded-md gap-2 p-2 typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-secondary/50 transition-colors'; - - if (isMobile || !isDesktopApp || !isMacPlatform) { - return null; - } - - return ( -
- -
- ); -}; - export const Header: React.FC = () => { const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen); const toggleSidebar = useUIStore((state) => state.toggleSidebar); const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen); const toggleCommandPalette = useUIStore((state) => state.toggleCommandPalette); const toggleHelpDialog = useUIStore((state) => state.toggleHelpDialog); - const isSidebarOpen = useUIStore((state) => state.isSidebarOpen); const activeMainTab = useUIStore((state) => state.activeMainTab); const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); @@ -171,10 +112,11 @@ export const Header: React.FC = () => { const desktopPaddingClass = React.useMemo(() => { if (isDesktopApp && isMacPlatform) { - return isSidebarOpen ? 'pl-2' : 'pl-[8.0rem]'; + // Always reserve space for Mac traffic lights since header is always on top + return 'pl-[5.75rem]'; } return 'pl-3'; - }, [isDesktopApp, isMacPlatform, isSidebarOpen]); + }, [isDesktopApp, isMacPlatform]); const updateHeaderHeight = React.useCallback(() => { if (typeof document === 'undefined') { @@ -344,18 +286,14 @@ export const Header: React.FC = () => { role="tablist" aria-label="Main navigation" > - {!(isDesktopApp && isMacPlatform) && ( - <> - - - )} +
{tabs.map((tab) => renderTab(tab))} @@ -404,14 +342,25 @@ export const Header: React.FC = () => { const renderMobile = () => (
- - {contextUsage && contextUsage.totalTokens > 0 && activeMainTab === 'chat' && ( + {/* Show back button when sessions sidebar is open, otherwise show sessions toggle */} + {isSessionSwitcherOpen ? ( + + ) : ( + + )} + {!isSessionSwitcherOpen && contextUsage && contextUsage.totalTokens > 0 && activeMainTab === 'chat' && ( { isMobile={true} /> )} + {isSessionSwitcherOpen && ( + Sessions + )}
-
+ {/* Hide tabs and right-side buttons when sessions sidebar is open */} + {!isSessionSwitcherOpen && ( +
+
+ {tabs.map((tab) => { + const isActive = activeMainTab === tab.id; + const Icon = tab.icon; + return ( + + + + + +

{tab.label}

+
+
+ ); + })} +
-
+ - {tabs.map((tab) => { - const isActive = activeMainTab === tab.id; - const Icon = tab.icon; - return ( - - - - - -

{tab.label}

-
-
- ); - })} + + + + + +

{updateAvailable ? 'Settings (Update available)' : 'Settings'}

+
+
- - - - - - - - -

{updateAvailable ? 'Settings (Update available)' : 'Settings'}

-
-
-
+ )}
); diff --git a/packages/ui/src/components/layout/MainLayout.tsx b/packages/ui/src/components/layout/MainLayout.tsx index adf750dc..e8f83392 100644 --- a/packages/ui/src/components/layout/MainLayout.tsx +++ b/packages/ui/src/components/layout/MainLayout.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import { Header, FixedSessionsButton } from './Header'; +import { Header } from './Header'; import { Sidebar } from './Sidebar'; import { ErrorBoundary } from '../ui/ErrorBoundary'; import { CommandPalette } from '../ui/CommandPalette'; import { HelpDialog } from '../ui/HelpDialog'; import { SessionSidebar } from '@/components/session/SessionSidebar'; import { SessionDialogs } from '@/components/session/SessionDialogs'; -import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { DiffWorkerProvider } from '@/contexts/DiffWorkerProvider'; import { MultiRunLauncher } from '@/components/multirun'; @@ -24,7 +23,6 @@ export const MainLayout: React.FC = () => { activeMainTab, setIsMobile, isSessionSwitcherOpen, - setSessionSwitcherOpen, isSettingsDialogOpen, setSettingsDialogOpen, isMultiRunLauncherOpen, @@ -331,35 +329,34 @@ export const MainLayout: React.FC = () => { {isMobile ? ( <> - {/* Mobile: Header + content + overlays */} + {/* Mobile: Header + content with drill-down pattern */} {!(isSettingsDialogOpen || isMultiRunLauncherOpen) &&
}
-
-
- + {/* Mobile drill-down: show sessions sidebar OR main content */} + {isSessionSwitcherOpen ? ( +
+
- {secondaryView && ( -
- {secondaryView} + ) : ( +
+
+
- )} -
+ {secondaryView && ( +
+ {secondaryView} +
+ )} +
+ )}
- setSessionSwitcherOpen(false)} - title="Sessions" - > - - - {/* Mobile multi-run launcher: full screen */} {isMultiRunLauncherOpen && (
@@ -382,18 +379,15 @@ export const MainLayout: React.FC = () => { ) : ( <> - {!isSettingsActive && ( - - - - )} - - {/* Main content area */} + {/* Desktop: Header always on top, then Sidebar + Content below */}
- {/* Normal view: Header + content */} + {/* Normal view: Header above Sidebar + content (like SettingsView) */}
-
+
+ + +
@@ -430,8 +424,6 @@ export const MainLayout: React.FC = () => { )} - {/* Hide fixed sessions button when settings is open */} - {!isSettingsActive && }
); diff --git a/packages/ui/src/components/layout/Sidebar.tsx b/packages/ui/src/components/layout/Sidebar.tsx index 96846f67..9ebf7c9a 100644 --- a/packages/ui/src/components/layout/Sidebar.tsx +++ b/packages/ui/src/components/layout/Sidebar.tsx @@ -11,7 +11,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; export const SIDEBAR_CONTENT_WIDTH = 264; const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_MAX_WIDTH = 500; -const MAC_TITLEBAR_SAFE_AREA = 40; const CHECK_FOR_UPDATES_EVENT = 'openchamber:check-for-updates'; interface SidebarProps { @@ -40,12 +39,7 @@ export const Sidebar: React.FC = ({ isOpen, isMobile, children }) return typeof (window as typeof window & { opencodeDesktop?: unknown }).opencodeDesktop !== 'undefined'; }); - const isMacPlatform = React.useMemo(() => { - if (typeof navigator === 'undefined') { - return false; - } - return /Macintosh|Mac OS X/.test(navigator.userAgent || ''); - }, []); + React.useEffect(() => { if (typeof window === 'undefined') { @@ -127,23 +121,6 @@ export const Sidebar: React.FC = ({ isOpen, isMobile, children }) } }, [isMobile, isResizing]); - const handleTitlebarDragStart = React.useCallback(async (e: React.MouseEvent) => { - - if (e.button !== 0) { - return; - } - - if (isDesktopApp) { - try { - const { getCurrentWindow } = await import('@tauri-apps/api/window'); - const window = getCurrentWindow(); - await window.startDragging(); - } catch (error) { - console.error('Failed to start window dragging from sidebar:', error); - } - } - }, [isDesktopApp]); - if (isMobile) { return null; @@ -153,7 +130,6 @@ export const Sidebar: React.FC = ({ isOpen, isMobile, children }) SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, sidebarWidth || SIDEBAR_CONTENT_WIDTH) ) : 0; - const shouldRenderTitlebarSpacer = isDesktopApp && isMacPlatform; const handlePointerDown = (event: React.PointerEvent) => { if (!isOpen) { @@ -203,14 +179,6 @@ export const Sidebar: React.FC = ({ isOpen, isMobile, children }) style={{ width: `${appliedWidth}px`, overflowX: 'hidden' }} aria-hidden={!isOpen} > - {shouldRenderTitlebarSpacer && ( -
- )}
{children}