From 05b58748329e69fabb9e4571a63269088c09278d Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Mon, 9 Feb 2026 01:00:27 -0800 Subject: [PATCH] feat(style): refactor layout, remove Header & Improve gateway readiness checks (#12) --- .gitignore | 3 +- electron/gateway/manager.ts | 13 ++++- electron/main/index.ts | 24 +++----- electron/main/ipc-handlers.ts | 28 +++++++++ electron/preload/index.ts | 5 ++ public/icons/icon.png | Bin 0 -> 1344 bytes src/assets/logo.svg | 3 + src/components/layout/Header.tsx | 61 -------------------- src/components/layout/MainLayout.tsx | 30 +++------- src/components/layout/Sidebar.tsx | 29 +++------- src/components/layout/TitleBar.tsx | 83 +++++++++++++++++++++++++++ src/pages/Chat/index.tsx | 8 ++- src/pages/Setup/index.tsx | 61 +++++++++++++++++--- src/vite-env.d.ts | 1 + 14 files changed, 217 insertions(+), 132 deletions(-) create mode 100644 public/icons/icon.png create mode 100644 src/assets/logo.svg delete mode 100644 src/components/layout/Header.tsx create mode 100644 src/components/layout/TitleBar.tsx create mode 100644 src/vite-env.d.ts diff --git a/.gitignore b/.gitignore index 262c76e68..06f508a36 100644 --- a/.gitignore +++ b/.gitignore @@ -60,4 +60,5 @@ resources/bin build/ -.cursor/ \ No newline at end of file +.cursor/ +.pnpm-store/ \ No newline at end of file diff --git a/electron/gateway/manager.ts b/electron/gateway/manager.ts index 2aafca1ad..63209bb3f 100644 --- a/electron/gateway/manager.ts +++ b/electron/gateway/manager.ts @@ -504,15 +504,22 @@ export class GatewayManager extends EventEmitter { /** * Wait for Gateway to be ready by checking if the port is accepting connections */ - private async waitForReady(retries = 30, interval = 1000): Promise { + private async waitForReady(retries = 120, interval = 1000): Promise { for (let i = 0; i < retries; i++) { + // Early exit if the gateway process has already exited + if (this.process && this.process.exitCode !== null) { + const code = this.process.exitCode; + logger.error(`Gateway process exited with code ${code} before becoming ready`); + throw new Error(`Gateway process exited with code ${code} before becoming ready`); + } + try { const ready = await new Promise((resolve) => { const testWs = new WebSocket(`ws://localhost:${this.status.port}/ws`); const timeout = setTimeout(() => { testWs.close(); resolve(false); - }, 1000); + }, 2000); testWs.on('open', () => { clearTimeout(timeout); @@ -534,7 +541,7 @@ export class GatewayManager extends EventEmitter { // Gateway not ready yet } - if (i > 0 && i % 5 === 0) { + if (i > 0 && i % 10 === 0) { logger.info(`Still waiting for Gateway... (attempt ${i + 1}/${retries})`); } diff --git a/electron/main/index.ts b/electron/main/index.ts index fca4480ca..42f4aed88 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -12,11 +12,11 @@ import { createMenu } from './menu'; import { appUpdater, registerUpdateHandlers } from './updater'; import { logger } from '../utils/logger'; +import { ClawHubService } from '../gateway/clawhub'; + // Disable GPU acceleration for better compatibility app.disableHardwareAcceleration(); -import { ClawHubService } from '../gateway/clawhub'; - // Global references let mainWindow: BrowserWindow | null = null; const gatewayManager = new GatewayManager(); @@ -26,6 +26,8 @@ const clawHubService = new ClawHubService(); * Create the main application window */ function createWindow(): BrowserWindow { + const isMac = process.platform === 'darwin'; + const win = new BrowserWindow({ width: 1280, height: 800, @@ -38,8 +40,9 @@ function createWindow(): BrowserWindow { sandbox: false, webviewTag: true, // Enable for embedding OpenClaw Control UI }, - titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', - trafficLightPosition: { x: 16, y: 16 }, + titleBarStyle: isMac ? 'hiddenInset' : 'hidden', + trafficLightPosition: isMac ? { x: 16, y: 16 } : undefined, + frame: isMac, show: false, }); @@ -57,7 +60,6 @@ function createWindow(): BrowserWindow { // Load the app if (process.env.VITE_DEV_SERVER_URL) { win.loadURL(process.env.VITE_DEV_SERVER_URL); - // Open DevTools in development win.webContents.openDevTools(); } else { win.loadFile(join(__dirname, '../../dist/index.html')); @@ -91,8 +93,6 @@ async function initialize(): Promise { createTray(mainWindow); // Override security headers ONLY for the OpenClaw Gateway Control UI - // The Control UI sets X-Frame-Options: DENY and CSP frame-ancestors 'none' - // which prevents embedding in an iframe. Only apply to gateway URLs. session.defaultSession.webRequest.onHeadersReceived((details, callback) => { const isGatewayUrl = details.url.includes('127.0.0.1:18789') || details.url.includes('localhost:18789'); @@ -102,10 +102,8 @@ async function initialize(): Promise { } const headers = { ...details.responseHeaders }; - // Remove X-Frame-Options to allow embedding in iframe delete headers['X-Frame-Options']; delete headers['x-frame-options']; - // Remove restrictive CSP frame-ancestors if (headers['Content-Security-Policy']) { headers['Content-Security-Policy'] = headers['Content-Security-Policy'].map( (csp) => csp.replace(/frame-ancestors\s+'none'/g, "frame-ancestors 'self' *") @@ -131,7 +129,7 @@ async function initialize(): Promise { appUpdater.checkForUpdates().catch((err) => { console.error('Failed to check for updates:', err); }); - }, 10000); // Check after 10 seconds + }, 10000); } // Handle window close @@ -139,14 +137,13 @@ async function initialize(): Promise { mainWindow = null; }); - // Start Gateway automatically (optional based on settings) + // Start Gateway automatically try { logger.info('Auto-starting Gateway...'); await gatewayManager.start(); logger.info('Gateway auto-start succeeded'); } catch (error) { logger.error('Gateway auto-start failed:', error); - // Notify renderer about the error mainWindow?.webContents.send('gateway:error', String(error)); } } @@ -155,21 +152,18 @@ async function initialize(): Promise { app.whenReady().then(initialize); app.on('window-all-closed', () => { - // On macOS, keep the app running in the menu bar if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { - // On macOS, re-create window when dock icon is clicked if (BrowserWindow.getAllWindows().length === 0) { mainWindow = createWindow(); } }); app.on('before-quit', async () => { - // Clean up Gateway process await gatewayManager.stop(); }); diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index fdcff27a3..0a90b9352 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -77,6 +77,9 @@ export function registerIpcHandlers( // Cron task handlers (proxy to Gateway RPC) registerCronHandlers(gatewayManager); + + // Window control handlers (for custom title bar on Windows/Linux) + registerWindowHandlers(mainWindow); } /** @@ -1151,3 +1154,28 @@ function registerAppHandlers(): void { app.quit(); }); } + +/** + * Window control handlers (for custom title bar on Windows/Linux) + */ +function registerWindowHandlers(mainWindow: BrowserWindow): void { + ipcMain.handle('window:minimize', () => { + mainWindow.minimize(); + }); + + ipcMain.handle('window:maximize', () => { + if (mainWindow.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow.maximize(); + } + }); + + ipcMain.handle('window:close', () => { + mainWindow.close(); + }); + + ipcMain.handle('window:isMaximized', () => { + return mainWindow.isMaximized(); + }); +} diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 94377a042..2261347d0 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -41,6 +41,11 @@ const electronAPI = { 'app:platform', 'app:quit', 'app:relaunch', + // Window controls + 'window:minimize', + 'window:maximize', + 'window:close', + 'window:isMaximized', // Settings 'settings:get', 'settings:set', diff --git a/public/icons/icon.png b/public/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8c171292352ef9d9f67ae1c1faa6d1638a3f8700 GIT binary patch literal 1344 zcmV-G1;6@=YJGESwbg3+t<_eW%dNKD{1D|I7$G9mjZ|g`n2Nx?_uO;OJ^%0d^R)Lp z_Z$y+`4f}+;%~b<@4fe&_j!KL@ArG&!{C1o<~9M`qObs%7GPNR)%veWV3;=GO@s&u zYXJ$E9sqJ3jhkp1)KJr}qJ1O*S-1bRZZ!cjDKQCE2kI|q*w85AeV>R;0TDY+$;fY! zu=-dW!7<%^X5Iv>Bp{L6$(Vd^8A>3h$p$TKmr&3uA-hqepR!&Bje{E2)Cn_!&xwFH z5(aSQs*atVGPbu%*xDvxU4wwUfP`cHD!Q)dID1XUCqGNr*eoI2FXA$1+KK#I0#@=G zmMpXnCs21@L)GspO3y0Tb6P=$UqErEjNzya-~^ch5?MYG=@mRg&6*#9glAvP00vnT z^#dBVwM*Fbi;R&enGtn#6E5JY${5zx#ofK8D+MzhfvdcU*5L$|_Ha$@+WjtBJ(WWI)6;#2zJB}qq5nN>L`(Hg4$CiLd*Nz&@Oa#d6 zllbCS89B`&R+MsdA{-Oa>}1e9n84zltUGdFcFEY#B;rHAh_>Mb{*IYQKNJI#Ie{Ms zv`Kq2Q#1B`-)hk<%aM;QG{lG zM{WpA_5|+yIE3{+@y6+x$_I=jf!gyLJhyg;_Iuv1Lo70(}v(GGw7a~6F`Ad(DG)SKx{Z%B-{1lwCB`XRBRj6;4vq5#e&j!LBV z9U}>>I?Pj>exaJDdrSqQZ%ZlMZ(0HedlfwLRTRtib9lQ!z%|}P$!W#C`MYx}UayT~ zIBHP;KiaS2l_R|C4Gzhnkki`BTg-*i|Yv8jE zd$eYI@t6|N%JW1{MZofuqif-chgh=E}?}YFxBrDGMI&jxJ>{vmf#;OH57ar!TLl10000 + + diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx deleted file mode 100644 index 6967db75c..000000000 --- a/src/components/layout/Header.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Header Component - * Top navigation bar with page title and page-specific controls. - * On the Chat page, shows session selector, refresh, thinking toggle, and new session. - */ -import { useLocation } from 'react-router-dom'; -import { Terminal } from 'lucide-react'; -import { ChatToolbar } from '@/pages/Chat/ChatToolbar'; -import { Button } from '@/components/ui/button'; - -// Page titles mapping -const pageTitles: Record = { - '/': 'Chat', - '/dashboard': 'Dashboard', - '/channels': 'Channels', - '/skills': 'Skills', - '/cron': 'Cron Tasks', - '/settings': 'Settings', -}; - -export function Header() { - const location = useLocation(); - const currentTitle = pageTitles[location.pathname] || 'ClawX'; - const isChatPage = location.pathname === '/'; - const isDashboard = location.pathname === '/dashboard'; - - const handleOpenDevConsole = async () => { - try { - const result = await window.electron.ipcRenderer.invoke('gateway:getControlUiUrl') as { success: boolean; url?: string; error?: string }; - if (result.success && result.url) { - window.electron.openExternal(result.url); - } else { - console.error('Failed to get Dev Console URL:', result.error); - } - } catch (err) { - console.error('Error opening Dev Console:', err); - } - }; - - return ( -
-

{currentTitle}

- - {/* Chat-specific controls */} - {isChatPage && } - - {/* Dashboard specific controls - Dev Console Button */} - {isDashboard && ( - - )} -
- ); -} diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 5702454df..942184966 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -1,32 +1,20 @@ /** * Main Layout Component - * Provides the primary app layout with sidebar and content area + * TitleBar at top, then sidebar + content below. */ import { Outlet } from 'react-router-dom'; import { Sidebar } from './Sidebar'; -import { Header } from './Header'; -import { useSettingsStore } from '@/stores/settings'; -import { cn } from '@/lib/utils'; +import { TitleBar } from './TitleBar'; export function MainLayout() { - const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed); - return ( -
- {/* Sidebar */} - - - {/* Main Content Area */} -
- {/* Header */} -
- - {/* Page Content */} +
+ {/* Title bar: drag region on macOS, icon + controls on Windows */} + + + {/* Below the title bar: sidebar + content */} +
+
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 847de0620..66e62efcc 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,6 +1,7 @@ /** * Sidebar Component - * Navigation sidebar with menu items + * Navigation sidebar with menu items. + * No longer fixed - sits inside the flex layout below the title bar. */ import { NavLink } from 'react-router-dom'; import { @@ -17,11 +18,9 @@ import { } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useSettingsStore } from '@/stores/settings'; - import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; - interface NavItemProps { to: string; icon: React.ReactNode; @@ -64,11 +63,11 @@ export function Sidebar() { const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed); const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed); const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked); - // Open developer console + const openDevConsole = () => { window.electron.openExternal('http://localhost:18789'); }; - + const navItems = [ { to: '/', icon: , label: 'Chat' }, { to: '/cron', icon: , label: 'Cron Tasks' }, @@ -77,25 +76,16 @@ export function Sidebar() { { to: '/dashboard', icon: , label: 'Dashboard' }, { to: '/settings', icon: , label: 'Settings' }, ]; - + return (