From f9acf68c8f0ac2fa71193c18a9374730ea329941 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Tue, 9 Jun 2026 15:51:04 +0300 Subject: [PATCH] feat(ui): full-height left sidebar with decoupled header layout Rework the desktop layout so the left sidebar spans the full window height and reads as a distinct column, separate from the header. - Persistent top-left controls (sidebar toggle + project actions) as a single overlay that no longer migrates between header and sidebar, so it stays put while panels animate. Drag regions carve out only under the buttons so the strip stays draggable and the buttons clickable. - Header sits in the content column (bg-background); the divider lives on the chat content (border-t) so it doesn't run between header and right sidebar. Right sidebar gets a top border + small content padding. - Drop the rounded chat card and corner masks for flat 1px borders. - Right sidebar, context panel and git/files views inherit the background color instead of bg-sidebar. - Remove hover background from Changes/Staged rows. - Mini-chat header: borderless, bg-background. --- packages/ui/src/components/layout/Header.tsx | 91 +++++++--- .../ui/src/components/layout/MainLayout.tsx | 161 ++++++------------ .../ui/src/components/layout/RightSidebar.tsx | 2 +- .../components/layout/RightSidebarTabs.tsx | 6 +- packages/ui/src/components/layout/Sidebar.tsx | 7 +- .../components/layout/SidebarFilesTree.tsx | 2 +- .../src/components/layout/SidebarTopBar.tsx | 34 ++++ .../layout/TitlebarLeftControls.tsx | 98 +++++++++++ .../components/mini-chat/MiniChatLayout.tsx | 2 +- packages/ui/src/components/views/GitView.tsx | 5 +- .../ui/src/components/views/git/ChangeRow.tsx | 2 +- .../src/components/views/git/ChangesPanel.tsx | 2 +- .../ui/src/hooks/useProjectActionsContext.ts | 77 +++++++++ 13 files changed, 338 insertions(+), 151 deletions(-) create mode 100644 packages/ui/src/components/layout/SidebarTopBar.tsx create mode 100644 packages/ui/src/components/layout/TitlebarLeftControls.tsx create mode 100644 packages/ui/src/hooks/useProjectActionsContext.ts diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index 4712c112..01d7d024 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -706,6 +706,7 @@ export const Header: React.FC = ({ const { t } = useI18n(); const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen); const toggleSidebar = useUIStore((state) => state.toggleSidebar); + const isSidebarOpen = useUIStore((state) => state.isSidebarOpen); const toggleBottomTerminal = useUIStore((state) => state.toggleBottomTerminal); const toggleRightSidebar = useUIStore((state) => state.toggleRightSidebar); const openContextOverview = useUIStore((state) => state.openContextOverview); @@ -1575,12 +1576,42 @@ export const Header: React.FC = ({ onToggleRightDrawer?.(); }, [onToggleRightDrawer, rightDrawerOpen]); - const desktopPaddingClass = React.useMemo(() => { - if ((isDesktopApp && isMacPlatform && !isDesktopWindowFullscreen) || isTabletStandalonePwa) { - return 'pl-[5.5rem]'; + // Left padding the header needs to clear the OS window controls (macOS + // traffic lights / window-controls-overlay). When the sidebar is open this + // space is owned by the sidebar's top strip instead, so the header drops back + // to its normal content padding. The full value is published as + // `--oc-titlebar-left-inset` so the sidebar strip can mirror it. + const titlebarLeftInset = React.useMemo(() => { + if (isDesktopApp && isMacPlatform && !isDesktopWindowFullscreen) { + return '5.5rem'; } - return 'pl-3'; - }, [isDesktopApp, isDesktopWindowFullscreen, isMacPlatform, isTabletStandalonePwa]); + if (isTabletStandalonePwa) { + return 'max(calc(0.75rem + var(--oc-wco-left-inset, 0px)), 5.5rem)'; + } + if ((!isDesktopApp || isWindowsElectronDesktop) && !isVSCode) { + return 'calc(0.75rem + var(--oc-wco-left-inset, 0px))'; + } + return '0.75rem'; + }, [isDesktopApp, isDesktopWindowFullscreen, isMacPlatform, isTabletStandalonePwa, isVSCode, isWindowsElectronDesktop]); + + useEffect(() => { + if (typeof document === 'undefined') { + return; + } + document.documentElement.style.setProperty('--oc-titlebar-left-inset', titlebarLeftInset); + }, [titlebarLeftInset]); + + // Space reserved on the header's left for the persistent overlay when the + // sidebar is collapsed (the overlay sits over the header then). Split into two + // spacers so the strip stays a window drag area while the buttons stay + // clickable: a drag region for the window-controls inset (traffic lights) and + // a no-drag carve under the control cluster. Both animate so the session title + // slides in/out in lockstep with the sidebar. When the sidebar is open the + // overlay is over the sidebar, so the header only keeps normal content padding. + const headerInsetSpacerWidth = isSidebarOpen ? '0.75rem' : 'var(--oc-titlebar-left-inset, 0.75rem)'; + const headerControlsSpacerWidth = isSidebarOpen + ? '0px' + : 'calc(var(--oc-titlebar-controls-width, 5.5rem) + 0.5rem)'; useEffect(() => { if (!isDesktopApp || !isMacPlatform) { @@ -1635,14 +1666,13 @@ export const Header: React.FC = ({ } return { - paddingLeft: isTabletStandalonePwa - ? 'max(calc(0.75rem + var(--oc-wco-left-inset, 0px)), 5.5rem)' - : 'calc(0.75rem + var(--oc-wco-left-inset, 0px))', + // Left inset is handled by the no-drag spacer (see renderDesktop); only + // the right inset / titlebar height are owned by the window-controls overlay. paddingRight: 'calc(0.75rem + var(--oc-wco-right-inset, 0px))', minHeight: 'max(3rem, var(--oc-wco-titlebar-height, 0px))', height: 'max(3rem, var(--oc-wco-titlebar-height, 0px))', }; - }, [isDesktopApp, isTabletStandalonePwa, isVSCode, isWindowsElectronDesktop]); + }, [isDesktopApp, isVSCode, isWindowsElectronDesktop]); const updateHeaderHeight = React.useCallback(() => { if (typeof document === 'undefined') { @@ -2061,13 +2091,27 @@ export const Header: React.FC = ({ onMouseDown={handleDragStart} className={cn( 'app-region-drag relative flex h-12 select-none items-center pr-3', - desktopPaddingClass, macosHeaderSizeClass )} style={webWindowControlsOverlayStyle} role="tablist" aria-label={t('header.navigation.mainAria')} > + {/* Drag region for the window-controls inset (traffic lights) to the left + of the overlay buttons — stays a window drag area. */} +
+ {/* No-drag carve under the persistent TitlebarLeftControls overlay so its + buttons stay clickable. Width animates with the sidebar so the session + title slides in lockstep instead of snapping. */} +
{isWindowsElectronDesktop ? ( = ({ Icon={'menu-2'} /> ) : null} - - -
- {projectActionsContext && ( - - )} + {/* Sidebar toggle + project actions live in the persistent + TitlebarLeftControls overlay; the header reserves matching left space + via padding (see headerStyle) when the sidebar is collapsed. */} +
+ + +

{t('header.actions.openSessionsWithShortcut', { shortcut: toggleShortcut })}

+
+ + + {projectActionsContext ? ( + + ) : null} +
+
+ ); +}; diff --git a/packages/ui/src/components/mini-chat/MiniChatLayout.tsx b/packages/ui/src/components/mini-chat/MiniChatLayout.tsx index bff69eb7..a8797def 100644 --- a/packages/ui/src/components/mini-chat/MiniChatLayout.tsx +++ b/packages/ui/src/components/mini-chat/MiniChatLayout.tsx @@ -260,7 +260,7 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => { return (
{ } return ( -
+
{
-
+
{ onVisiblePathsChange={setVisibleChangePaths} onRevertAll={handleRevertAll} onRevertDirectory={handleRevertDirectory} + headerBackgroundClassName="bg-background" />
diff --git a/packages/ui/src/components/views/git/ChangeRow.tsx b/packages/ui/src/components/views/git/ChangeRow.tsx index f3c1e665..8c65069b 100644 --- a/packages/ui/src/components/views/git/ChangeRow.tsx +++ b/packages/ui/src/components/views/git/ChangeRow.tsx @@ -118,7 +118,7 @@ export const ChangeRow = React.memo(function ChangeRow({ return (
= ({ const isDirectoryReverting = isRevertingAll || directoryPaths.some((path) => revertingPaths.has(path)); return (