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 000000000..8c1712923 Binary files /dev/null and b/public/icons/icon.png differ diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 000000000..2b7c35981 --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,3 @@ + + + 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 (