From 0ce65b4025d44e0289acc40774eb81ef7044b8a1 Mon Sep 17 00:00:00 2001 From: Cole McIntosh Date: Fri, 15 May 2026 09:49:51 -0500 Subject: [PATCH 1/2] fix(ui): align sidebar hamburger in macOS fullscreen The hamburger button is absolutely positioned with a 96px left inset to clear the traffic-light controls, which are hidden in macOS fullscreen. Without traffic lights to clear, that inset made the button appear detached from the sidebar navigation. Forward enter-full-screen / leave-full-screen events from the main window to the renderer, expose an initial-state query via IPC, and drop the traffic-light inset on the hamburger header when fullscreen is active. Fixes #8510 Signed-off-by: Cole McIntosh --- .../src/components/Layout/AppLayout.tsx | 21 +++++++++++++++++-- ui/desktop/src/main.ts | 13 ++++++++++++ ui/desktop/src/preload.ts | 2 ++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/ui/desktop/src/components/Layout/AppLayout.tsx b/ui/desktop/src/components/Layout/AppLayout.tsx index 4ce9274882..845d525ae9 100644 --- a/ui/desktop/src/components/Layout/AppLayout.tsx +++ b/ui/desktop/src/components/Layout/AppLayout.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { IpcRendererEvent } from 'electron'; import { Outlet, useLocation } from 'react-router-dom'; import { motion } from 'framer-motion'; import { Menu } from 'lucide-react'; @@ -37,6 +38,21 @@ const AppLayoutContent: React.FC = ({ activeSessions }) = const chatContext = useChatContext(); const isOnPairRoute = location.pathname === '/pair'; + const [isFullScreen, setIsFullScreen] = useState(false); + + useEffect(() => { + if (!safeIsMacOS) return; + window.electron + .getIsFullScreen() + .then(setIsFullScreen) + .catch(() => {}); + const handler = (_event: IpcRendererEvent, ...args: unknown[]) => { + setIsFullScreen(Boolean(args[0])); + }; + window.electron.on('fullscreen-change', handler); + return () => window.electron.off('fullscreen-change', handler); + }, [safeIsMacOS]); + const { isNavExpanded, setIsNavExpanded, @@ -146,8 +162,9 @@ const AppLayoutContent: React.FC = ({ activeSessions }) = }; }, [isPushTopNav]); - const headerPadding = safeIsMacOS ? 'pl-[96px]' : 'pl-4'; - const headerTop = safeIsMacOS ? 'top-[15px]' : 'top-[11px]'; + const needsTrafficLightInset = safeIsMacOS && !isFullScreen; + const headerPadding = needsTrafficLightInset ? 'pl-[96px]' : 'pl-4'; + const headerTop = needsTrafficLightInset ? 'top-[15px]' : 'top-[11px]'; // Determine flex direction based on navigation position (for push mode) const getLayoutClass = () => { diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 343b5e1cda..7e84139248 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -1126,6 +1126,14 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => { } }); + const broadcastFullScreenState = () => { + if (!mainWindow.isDestroyed()) { + mainWindow.webContents.send('fullscreen-change', mainWindow.isFullScreen()); + } + }; + mainWindow.on('enter-full-screen', broadcastFullScreenState); + mainWindow.on('leave-full-screen', broadcastFullScreenState); + // Handle mouse back button (button 3) // Use type assertion for non-standard Electron event // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -1793,6 +1801,11 @@ ipcMain.handle('is-any-window-focused', () => { return BrowserWindow.getFocusedWindow() !== null; }); +ipcMain.handle('get-is-fullscreen', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + return win?.isFullScreen() ?? false; +}); + // Add file/directory selection handler ipcMain.handle('select-file-or-directory', async (_event, defaultPath?: string) => { const dialogOptions: OpenDialogOptions = { diff --git a/ui/desktop/src/preload.ts b/ui/desktop/src/preload.ts index 777cff7b39..b19da554f9 100644 --- a/ui/desktop/src/preload.ts +++ b/ui/desktop/src/preload.ts @@ -148,6 +148,7 @@ type ElectronAPI = { getSpellcheckState: () => Promise; openNotificationsSettings: () => Promise; isAnyWindowFocused: () => Promise; + getIsFullScreen: () => Promise; onMouseBackButtonClicked: (callback: () => void) => void; offMouseBackButtonClicked: (callback: () => void) => void; on: ( @@ -271,6 +272,7 @@ const electronAPI: ElectronAPI = { getSpellcheckState: () => ipcRenderer.invoke('get-spellcheck-state'), openNotificationsSettings: () => ipcRenderer.invoke('open-notifications-settings'), isAnyWindowFocused: () => ipcRenderer.invoke('is-any-window-focused'), + getIsFullScreen: () => ipcRenderer.invoke('get-is-fullscreen'), onMouseBackButtonClicked: (callback: () => void) => { // Wrapper that ignores the event parameter. const wrappedCallback = (_event: Electron.IpcRendererEvent) => callback(); From 4daffad8c71da7b212e989158dbf1a90d98e7441 Mon Sep 17 00:00:00 2001 From: Cole McIntosh Date: Fri, 15 May 2026 10:38:46 -0500 Subject: [PATCH 2/2] test(ui): add fullscreen IPC methods to window.electron mock AppLayout now calls getIsFullScreen / on / off on window.electron; keep the renderer test mock in sync so future tests that mount the layout don't trip a TypeError. Signed-off-by: Cole McIntosh --- ui/desktop/src/test/setup.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/desktop/src/test/setup.ts b/ui/desktop/src/test/setup.ts index cae38d82ae..bed778d485 100644 --- a/ui/desktop/src/test/setup.ts +++ b/ui/desktop/src/test/setup.ts @@ -87,5 +87,8 @@ Object.defineProperty(window, 'electron', { return Promise.resolve(); }), showMessageBox: vi.fn(() => Promise.resolve({ response: 0 })), + getIsFullScreen: vi.fn(() => Promise.resolve(false)), + on: vi.fn(), + off: vi.fn(), }, });