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 (