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(); 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(), }, });