From 471ae69b1ac2551e0cb0cf37f05e65c5f0e8eaab Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 20 May 2026 19:30:10 +0300 Subject: [PATCH] refactor(ui): full-width header with framed chat shell Header now spans the full window width above the [sidebar | chat | right-sidebar] row instead of nesting inside the central column. The chat area becomes a self-contained framed window with its own border and rounded corners on all four sides, and sidebars sit flush against the header sharing its bg-sidebar so the seam is invisible. Removed the duplicated shell controls the old layout needed to fake header-height inside sidebars: portal host on RightSidebar, paddingTop reservation, top drag overlay, duplicated layout-left / chat-new buttons in SidebarHeader, the showDesktopSidebarChrome block in SessionSidebar, and the conditional traffic-lights inset on the desktop header. Mac WCO inset now lives only on the header. Moved the new-session action into the SessionSwitcher dropdown as its first item, removed the standalone chat-new button from the header, relocated scheduled-tasks into the left action group of the sidebar header, and bumped ContextPanel tab strip to h-10 to balance the more prominent header. --- .../ui/src/components/layout/ContextPanel.tsx | 2 +- packages/ui/src/components/layout/Header.tsx | 62 +-- .../ui/src/components/layout/MainLayout.tsx | 45 +- .../ui/src/components/layout/RightSidebar.tsx | 61 +-- .../src/components/session/SessionSidebar.tsx | 120 +----- .../session/SessionSwitcherDropdown.tsx | 40 +- .../session/sidebar/SidebarHeader.tsx | 405 ++++++++---------- 7 files changed, 228 insertions(+), 507 deletions(-) diff --git a/packages/ui/src/components/layout/ContextPanel.tsx b/packages/ui/src/components/layout/ContextPanel.tsx index d94e65e5..9024a61d 100644 --- a/packages/ui/src/components/layout/ContextPanel.tsx +++ b/packages/ui/src/components/layout/ContextPanel.tsx @@ -1828,7 +1828,7 @@ export const ContextPanel: React.FC = () => { const isFileTabActive = activeTab?.mode === 'file'; const header = ( -
+
void; leftDrawerOpen?: boolean; rightDrawerOpen?: boolean; - desktopRightSidebarActionsHost?: HTMLElement | null; } export const Header: React.FC = ({ @@ -654,13 +652,10 @@ export const Header: React.FC = ({ onToggleRightDrawer, leftDrawerOpen, rightDrawerOpen, - desktopRightSidebarActionsHost = null, }) => { const { t } = useI18n(); const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen); const toggleSidebar = useUIStore((state) => state.toggleSidebar); - const isSidebarOpen = useUIStore((state) => state.isSidebarOpen); - const isRightSidebarOpen = useUIStore((state) => state.isRightSidebarOpen); const toggleBottomTerminal = useUIStore((state) => state.toggleBottomTerminal); const toggleRightSidebar = useUIStore((state) => state.toggleRightSidebar); const openContextOverview = useUIStore((state) => state.openContextOverview); @@ -677,7 +672,6 @@ export const Header: React.FC = ({ const [isDevShutdownInFlight, setIsDevShutdownInFlight] = React.useState(false); const getContextUsage = useSessionUIStore((state) => state.getContextUsage); - const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft); const isNewSessionDraftOpen = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open)); const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const currentSessionMessagesResolved = useSessionMessagesResolved(currentSessionId ?? ''); @@ -815,15 +809,6 @@ export const Header: React.FC = ({ }, [desktopServicesTab, isDesktopApp]); const isVSCode = React.useMemo(() => isVSCodeRuntime(), []); - const isLeftSidebarOpen = React.useMemo(() => { - if (!isMobile) { - return isSidebarOpen; - } - if (typeof onToggleLeftDrawer === 'function') { - return Boolean(leftDrawerOpen); - } - return isSessionSwitcherOpen; - }, [isMobile, isSessionSwitcherOpen, isSidebarOpen, leftDrawerOpen, onToggleLeftDrawer]); const showDesktopHeaderContextUsage = !isVSCode && activeMainTab === 'chat' && !!stableDesktopContextUsage && stableDesktopContextUsage.totalTokens > 0; const desktopHeaderDisplayPercentage = stableDesktopContextUsage && stableDesktopContextUsage.contextLimit > 0 ? Math.min(999, (stableDesktopContextUsage.totalTokens / stableDesktopContextUsage.contextLimit) * 100) @@ -1275,12 +1260,6 @@ export const Header: React.FC = ({ toggleSidebar(); }, [blurActiveElement, isMobile, isSessionSwitcherOpen, setSessionSwitcherOpen, toggleSidebar]); - const handleHeaderNewSession = React.useCallback(() => { - setActiveMainTab('chat'); - setSessionSwitcherOpen(false); - openNewSessionDraft(); - }, [openNewSessionDraft, setActiveMainTab, setSessionSwitcherOpen]); - const handleOpenDraftMiniChat = React.useCallback(() => { void invokeDesktop('desktop_open_draft_mini_chat_window', { directory: normalize(openDirectory || activeProject?.path || ''), @@ -1367,11 +1346,11 @@ export const Header: React.FC = ({ const mobileHeaderIconButtonClass = MOBILE_HEADER_ICON_BUTTON_CLASS; const desktopPaddingClass = React.useMemo(() => { - if (!isSidebarOpen && ((isDesktopApp && isMacPlatform && !isDesktopWindowFullscreen) || isTabletStandalonePwa)) { + if ((isDesktopApp && isMacPlatform && !isDesktopWindowFullscreen) || isTabletStandalonePwa) { return 'pl-[5.5rem]'; } return 'pl-3'; - }, [isDesktopApp, isDesktopWindowFullscreen, isMacPlatform, isSidebarOpen, isTabletStandalonePwa]); + }, [isDesktopApp, isDesktopWindowFullscreen, isMacPlatform, isTabletStandalonePwa]); useEffect(() => { if (!isDesktopApp || !isMacPlatform) { @@ -1439,14 +1418,14 @@ export const Header: React.FC = ({ } return { - paddingLeft: isTabletStandalonePwa && !isSidebarOpen + paddingLeft: isTabletStandalonePwa ? 'max(calc(0.75rem + var(--oc-wco-left-inset, 0px)), 5.5rem)' : 'calc(0.75rem + var(--oc-wco-left-inset, 0px))', 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, isSidebarOpen, isTabletStandalonePwa, isVSCode]); + }, [isDesktopApp, isTabletStandalonePwa, isVSCode]); const updateHeaderHeight = React.useCallback(() => { if (typeof document === 'undefined') { @@ -1844,7 +1823,6 @@ export const Header: React.FC = ({ ); - const desktopSidebarActionsInline = !isRightSidebarOpen || !desktopRightSidebarActionsHost; const showMiniChatHeaderAction = hasElectronDesktopIPC && (isNewSessionDraftOpen || Boolean(currentSessionId)); const renderDesktop = () => ( @@ -1860,7 +1838,6 @@ export const Header: React.FC = ({ aria-label={t('header.navigation.mainAria')} > = ({ Icon={'layout-left'} /> -
- {!isLeftSidebarOpen ? ( - - - - - -

{t('header.actions.newSessionWithShortcut', { shortcut: shortcutLabel('new_chat') })}

-
-
- ) : null} +
{projectActionsContext && ( = ({ showPercentIcon onClick={handleOpenContextPanel} pressed={isContextPanelActive} - className={desktopSidebarActionsInline && !showMiniChatHeaderAction ? 'mr-3.5' : ''} + className={!showMiniChatHeaderAction ? 'mr-3.5' : ''} valueClassName="typography-ui-label font-medium leading-none text-foreground" percentIconClassName="h-5 w-5" /> @@ -1963,13 +1923,10 @@ export const Header: React.FC = ({ title={isNewSessionDraftOpen ? t('header.actions.newMiniChat') : t('header.actions.openSessionMiniChat')} ariaLabel={isNewSessionDraftOpen ? t('header.actions.newMiniChatAria') : t('header.actions.openSessionMiniChatAria')} onClick={handleOpenCurrentMiniChat} - className={cn(desktopHeaderIconButtonClass, desktopSidebarActionsInline && showDesktopHeaderContextUsage ? 'mr-3.5' : 'mr-1')} + className={cn(desktopHeaderIconButtonClass, showDesktopHeaderContextUsage ? 'mr-3.5' : 'mr-1')} Icon={'picture-in-picture-2'} /> - {desktopSidebarActionsInline ? desktopSidebarActions : null} - {!desktopSidebarActionsInline && desktopRightSidebarActionsHost - ? createPortal(desktopSidebarActions, desktopRightSidebarActionsHost) - : null} + {desktopSidebarActions}
@@ -2386,8 +2343,7 @@ export const Header: React.FC = ({ const headerClassName = cn( 'header-safe-area relative z-10', - isMobile && 'border-b border-border/50', - 'bg-background' + isMobile ? 'border-b border-border/50 bg-background' : 'bg-sidebar' ); return ( diff --git a/packages/ui/src/components/layout/MainLayout.tsx b/packages/ui/src/components/layout/MainLayout.tsx index fc257279..7b39875d 100644 --- a/packages/ui/src/components/layout/MainLayout.tsx +++ b/packages/ui/src/components/layout/MainLayout.tsx @@ -23,7 +23,6 @@ import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; import { useVisualViewport } from '@/hooks/useVisualViewport'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; -import { isDesktopShell } from '@/lib/desktop'; import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery'; import { ChatView } from '@/components/views/ChatView'; @@ -86,10 +85,8 @@ export const MainLayout: React.FC = () => { const { isMobile, isTablet } = useDeviceInfo(); const visualViewport = useVisualViewport(); - const isDesktopShellRuntime = React.useMemo(() => isDesktopShell(), []); const sidebarWidth = useUIStore((state) => state.sidebarWidth); const rightSidebarWidth = useUIStore((state) => state.rightSidebarWidth); - const [desktopRightSidebarActionsHost, setDesktopRightSidebarActionsHost] = React.useState(null); const effectiveDirectory = useEffectiveDirectory() ?? ''; const directoryKey = React.useMemo(() => normalizeDirectoryKey(effectiveDirectory), [effectiveDirectory]); const isContextPanelOpen = useUIStore((state) => { @@ -605,20 +602,15 @@ export const MainLayout: React.FC = () => { ) : ( <> - {/* Desktop: Sidebar is a left column; header belongs to content column */} -
-
+ {/* Desktop: full-width Header above [Sidebar | chat-frame | RightSidebar] row */} +
+
+
{isSidebarOpen ? ( <>
{ />
{ <>
{ />
{
-
-
+
@@ -721,12 +700,10 @@ export const MainLayout: React.FC = () => {
-
{/* Desktop settings: windowed dialog with blur */} diff --git a/packages/ui/src/components/layout/RightSidebar.tsx b/packages/ui/src/components/layout/RightSidebar.tsx index 40a0a847..44c59f68 100644 --- a/packages/ui/src/components/layout/RightSidebar.tsx +++ b/packages/ui/src/components/layout/RightSidebar.tsx @@ -2,8 +2,6 @@ import React from 'react'; import { cn } from '@/lib/utils'; import { useUIStore } from '@/stores/useUIStore'; import { useI18n } from '@/lib/i18n'; -import { isDesktopShell, isVSCodeRuntime, startDesktopWindowDrag } from '@/lib/desktop'; -import { useTabletStandalonePwaRuntime } from '@/lib/device'; export const RIGHT_SIDEBAR_CONTENT_WIDTH = 420; const RIGHT_SIDEBAR_MIN_WIDTH = 400; @@ -13,16 +11,12 @@ interface RightSidebarProps { isOpen: boolean; children: React.ReactNode; className?: string; - onTopActionsHostChange?: (element: HTMLDivElement | null) => void; } -export const RightSidebar: React.FC = ({ isOpen, children, className, onTopActionsHostChange }) => { +export const RightSidebar: React.FC = ({ isOpen, children, className }) => { const { t } = useI18n(); const rightSidebarWidth = useUIStore((state) => state.rightSidebarWidth); const setRightSidebarWidth = useUIStore((state) => state.setRightSidebarWidth); - const isDesktopApp = React.useMemo(() => isDesktopShell(), []); - const isVSCode = React.useMemo(() => isVSCodeRuntime(), []); - const isTabletStandalonePwa = useTabletStandalonePwaRuntime(); const [isResizing, setIsResizing] = React.useState(false); const startXRef = React.useRef(0); const startWidthRef = React.useRef(rightSidebarWidth || 420); @@ -107,42 +101,6 @@ export const RightSidebar: React.FC = ({ isOpen, children, cl } }, [isResizing]); - React.useEffect(() => { - if (!isOpen) { - onTopActionsHostChange?.(null); - } - }, [isOpen, onTopActionsHostChange]); - - const handleDragStart = React.useCallback(async (event: React.MouseEvent) => { - const target = event.target as HTMLElement; - if (target.closest('.app-region-no-drag')) { - return; - } - if (target.closest('button, a, input, select, textarea')) { - return; - } - if (event.button !== 0) { - return; - } - if (!isDesktopApp) { - return; - } - - await startDesktopWindowDrag(); - }, [isDesktopApp]); - - const webWindowControlsOverlayStyle = React.useMemo(() => { - if (isDesktopApp || isVSCode) { - return undefined; - } - - return { - paddingLeft: 'calc(0.75rem + var(--oc-wco-left-inset, 0px))', - paddingRight: 'calc(0.75rem + var(--oc-wco-right-inset, 0px))', - ...(isTabletStandalonePwa ? { paddingTop: 'var(--oc-safe-area-top, 0px)' } : null), - }; - }, [isDesktopApp, isTabletStandalonePwa, isVSCode]); - return (