Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ resources/bin

build/

.cursor/
.cursor/
.pnpm-store/
13 changes: 10 additions & 3 deletions electron/gateway/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
private async waitForReady(retries = 120, interval = 1000): Promise<void> {
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<boolean>((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);
Expand All @@ -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})`);
}

Expand Down
24 changes: 9 additions & 15 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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,
Expand All @@ -38,8 +40,9 @@ function createWindow(): BrowserWindow {
sandbox: false,
webviewTag: true, // Enable <webview> 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,
});

Expand All @@ -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'));
Expand Down Expand Up @@ -91,8 +93,6 @@ async function initialize(): Promise<void> {
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');

Expand All @@ -102,10 +102,8 @@ async function initialize(): Promise<void> {
}

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' *")
Expand All @@ -131,22 +129,21 @@ async function initialize(): Promise<void> {
appUpdater.checkForUpdates().catch((err) => {
console.error('Failed to check for updates:', err);
});
}, 10000); // Check after 10 seconds
}, 10000);
}

// Handle window close
mainWindow.on('closed', () => {
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));
}
}
Expand All @@ -155,21 +152,18 @@ async function initialize(): Promise<void> {
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();
});

Expand Down
28 changes: 28 additions & 0 deletions electron/main/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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();
});
}
5 changes: 5 additions & 0 deletions electron/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Binary file added public/icons/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 0 additions & 61 deletions src/components/layout/Header.tsx

This file was deleted.

30 changes: 9 additions & 21 deletions src/components/layout/MainLayout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex h-screen overflow-hidden bg-background">
{/* Sidebar */}
<Sidebar />

{/* Main Content Area */}
<div
className={cn(
'flex flex-1 flex-col overflow-hidden transition-all duration-300',
sidebarCollapsed ? 'ml-16' : 'ml-64'
)}
>
{/* Header */}
<Header />

{/* Page Content */}
<div className="flex h-screen flex-col overflow-hidden bg-background">
{/* Title bar: drag region on macOS, icon + controls on Windows */}
<TitleBar />

{/* Below the title bar: sidebar + content */}
<div className="flex flex-1 overflow-hidden">
<Sidebar />
<main className="flex-1 overflow-auto p-6">
<Outlet />
</main>
Expand Down
Loading
Loading