diff --git a/electron/main/index.ts b/electron/main/index.ts index 3748461c6..f82bb02ef 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -113,6 +113,8 @@ function getAppIcon(): Electron.NativeImage | undefined { */ function createWindow(): BrowserWindow { const isMac = process.platform === 'darwin'; + const isWindows = process.platform === 'win32'; + const useCustomTitleBar = isWindows; const win = new BrowserWindow({ width: 1280, @@ -127,9 +129,9 @@ function createWindow(): BrowserWindow { sandbox: false, webviewTag: true, // Enable for embedding OpenClaw Control UI }, - titleBarStyle: isMac ? 'hiddenInset' : 'hidden', + titleBarStyle: isMac ? 'hiddenInset' : useCustomTitleBar ? 'hidden' : 'default', trafficLightPosition: isMac ? { x: 16, y: 16 } : undefined, - frame: isMac, + frame: isMac || !useCustomTitleBar, show: false, }); diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index df0fd6236..6247133b4 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -2194,7 +2194,7 @@ function registerUsageHandlers(): void { }); } /** - * Window control handlers (for custom title bar on Windows/Linux) + * Window control handlers (for custom title bar on Windows) */ function registerWindowHandlers(mainWindow: BrowserWindow): void { ipcMain.handle('window:minimize', () => { diff --git a/src/components/layout/TitleBar.tsx b/src/components/layout/TitleBar.tsx index 7dd7ffc05..7fdcdfeed 100644 --- a/src/components/layout/TitleBar.tsx +++ b/src/components/layout/TitleBar.tsx @@ -1,20 +1,26 @@ /** * TitleBar Component * macOS: empty drag region (native traffic lights handled by hiddenInset). - * Windows/Linux: drag region on left, minimize/maximize/close on right. + * Windows: drag region with custom minimize/maximize/close controls. + * Linux: use native window chrome (no custom title bar). */ import { useState, useEffect } from 'react'; import { Minus, Square, X, Copy } from 'lucide-react'; import { invokeIpc } from '@/lib/api-client'; -const isMac = window.electron?.platform === 'darwin'; - export function TitleBar() { - if (isMac) { + const platform = window.electron?.platform; + + if (platform === 'darwin') { // macOS: just a drag region, traffic lights are native return
; } + // Linux keeps the native frame/title bar for better IME compatibility. + if (platform !== 'win32') { + return null; + } + return ; } diff --git a/tests/unit/title-bar.test.tsx b/tests/unit/title-bar.test.tsx new file mode 100644 index 000000000..18764e33c --- /dev/null +++ b/tests/unit/title-bar.test.tsx @@ -0,0 +1,50 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import { TitleBar } from '@/components/layout/TitleBar'; + +const invokeIpcMock = vi.hoisted(() => vi.fn()); + +vi.mock('@/lib/api-client', () => ({ + invokeIpc: (...args: unknown[]) => invokeIpcMock(...args), +})); + +describe('TitleBar platform behavior', () => { + beforeEach(() => { + invokeIpcMock.mockReset(); + invokeIpcMock.mockResolvedValue(false); + }); + + it('renders macOS drag region', () => { + window.electron.platform = 'darwin'; + + const { container } = render(); + + expect(container.querySelector('.drag-region')).toBeInTheDocument(); + expect(screen.queryByTitle('Minimize')).not.toBeInTheDocument(); + expect(invokeIpcMock).not.toHaveBeenCalled(); + }); + + it('renders custom controls on Windows', async () => { + window.electron.platform = 'win32'; + + render(); + + expect(screen.getByTitle('Minimize')).toBeInTheDocument(); + expect(screen.getByTitle('Maximize')).toBeInTheDocument(); + expect(screen.getByTitle('Close')).toBeInTheDocument(); + + await waitFor(() => { + expect(invokeIpcMock).toHaveBeenCalledWith('window:isMaximized'); + }); + }); + + it('renders no custom title bar on Linux', () => { + window.electron.platform = 'linux'; + + const { container } = render(); + + expect(container.firstChild).toBeNull(); + expect(screen.queryByTitle('Minimize')).not.toBeInTheDocument(); + expect(invokeIpcMock).not.toHaveBeenCalled(); + }); +});