Electron IPC & UI (MVP)
Files
-
src/main/index.ts
-
Main process, BrowserWindow config
-
src/main/ipc.ts
-
IPC handlers
-
src/preload/index.ts
-
Context bridge API
-
src/renderer/styles.css
-
Global styles (Tailwind + custom)
-
src/renderer/components/
-
React UI components
Preload API (window.api)
interface API { // Invoke handlers (renderer → main) startRun(prompt: string, credentials: Credentials): Promise<void> selectWinner(agentId: string): Promise<void> cancelRun(): Promise<void> previewAll(): Promise<PreviewResult[]>
// Event listeners (main → renderer) onAgentOutput(cb: (data: { agentId: string; chunk: string }) => void): () => void onAgentStatus(cb: (data: { agentId: string; status: AgentStatus }) => void): () => void onError(cb: (error: string) => void): () => void }
interface Credentials { daytonaApiKey: string claudeOAuthToken?: string anthropicApiKey?: string }
interface PreviewResult { agentId: string url: string | null error?: string }
type AgentStatus = 'idle' | 'running' | 'completed' | 'failed'
IPC Communication
Main → Renderer Events
sendToRenderer('agent-output', { agentId, chunk }) // Terminal data sendToRenderer('agent-status', { agentId, status }) // Status change sendToRenderer('error', errorMessage) // Error string
Renderer → Main Handlers
ipcMain.handle('start-run', async (_event, { prompt, credentials }) => { ... }) ipcMain.handle('select-winner', async (_event, { agentId }) => { ... }) ipcMain.handle('cancel-run', async () => { ... }) ipcMain.handle('preview-all', async () => { ... })
Output Buffering
const buffer = new OutputBuffer((agentId, data) => { sendToRenderer('agent-output', { agentId, chunk: data }) })
buffer.append(agentId, chunk) // Add to buffer buffer.flush(agentId) // Force flush buffer.flushAll() // Flush all agents
BrowserWindow Configuration
Current config in src/main/index.ts :
const mainWindow = new BrowserWindow({ width: 1400, height: 900, minWidth: 1000, minHeight: 700, show: false, autoHideMenuBar: true, backgroundColor: '#171717', webPreferences: { preload: join(__dirname, '../preload/index.js'), sandbox: false, contextIsolation: true, nodeIntegration: false } })
UI Beautification Options
Custom Title Bar (Frameless Window)
// Hidden title bar with traffic lights (macOS) const win = new BrowserWindow({ titleBarStyle: 'hidden', trafficLightPosition: { x: 15, y: 15 } })
// Hidden with custom overlay (cross-platform) const win = new BrowserWindow({ titleBarStyle: 'hidden', titleBarOverlay: { color: '#171717', symbolColor: '#ffffff', height: 40 } })
// Custom traffic light behavior (macOS) const win = new BrowserWindow({ titleBarStyle: 'hiddenInset' // Inset traffic lights }) const win = new BrowserWindow({ titleBarStyle: 'customButtonsOnHover' // Show on hover only })
Platform Vibrancy Effects
// macOS Vibrancy win.setVibrancy('sidebar') // Options: titlebar, sidebar, window, hud, etc. win.setVibrancy('under-window') // Translucent background
// Windows 11 Mica/Acrylic (22H2+) const win = new BrowserWindow({ backgroundColor: '#00000000', // Transparent for effects // ... }) win.setBackgroundMaterial('mica') // Long-lived windows win.setBackgroundMaterial('acrylic') // Transient/popup windows
Transparent Window
const win = new BrowserWindow({ frame: false, transparent: true, resizable: false, // Required when transparent backgroundColor: '#00000000' })
Window Controls Overlay API
Exposes native controls area to web content:
const win = new BrowserWindow({ titleBarStyle: 'hidden', titleBarOverlay: true // Enable CSS env() variables })
In CSS, use safe area insets:
.titlebar { padding-top: env(titlebar-area-y, 0); padding-left: env(titlebar-area-x, 0); height: env(titlebar-area-height, 40px); }
/* Draggable region */ .drag-region { -webkit-app-region: drag; }
.no-drag { -webkit-app-region: no-drag; }
Tailwind CSS Patterns
Current setup uses Tailwind with dark theme. Key patterns:
Color Palette (neutral-based)
bg-neutral-900 /* Main background #171717 / bg-neutral-800 / Cards, inputs / bg-neutral-700 / Borders, dividers / text-neutral-100 / Primary text / text-neutral-400 / Secondary text / text-neutral-500 / Muted text, placeholders */
Status Colors
/* Running */ bg-yellow-500/20 text-yellow-400 animate-pulse
/* Completed */ bg-green-500/20 text-green-400
/* Failed */ bg-red-500/20 text-red-400
/* Winner highlight */ border-green-500 bg-green-500/5
Interactive Elements
/* Buttons - Primary */ bg-blue-600 hover:bg-blue-500 disabled:bg-neutral-700
/* Buttons - Danger */ bg-red-600/20 text-red-400 hover:bg-red-600/30
/* Inputs */ bg-neutral-800 border-neutral-700 focus:ring-2 focus:ring-blue-500
/* Cards */ rounded-lg border border-neutral-700 bg-neutral-800
Useful Effects for Beautification
/* Glassmorphism */ backdrop-blur-md bg-white/10
/* Subtle gradients */ bg-gradient-to-br from-neutral-800 to-neutral-900
/* Glow effects */ shadow-lg shadow-blue-500/20
/* Smooth transitions */ transition-all duration-200
/* Hover lift */ hover:-translate-y-0.5 hover:shadow-lg
xterm.js Theme
Current terminal theme in Terminal.tsx :
const terminal = new XTerm({ theme: { background: '#1e1e1e', foreground: '#d4d4d4', cursor: '#d4d4d4', selectionBackground: '#264f78', black: '#1e1e1e', red: '#f44747', green: '#6a9955', yellow: '#dcdcaa', blue: '#569cd6', magenta: '#c586c0', cyan: '#4ec9b0', white: '#d4d4d4' }, fontSize: 12, fontFamily: 'Menlo, Monaco, "Courier New", monospace', scrollback: 10000, cursorBlink: false, disableStdin: true })
Alternative themes to consider:
// Dracula theme: { background: '#282a36', foreground: '#f8f8f2', cursor: '#f8f8f2', red: '#ff5555', green: '#50fa7b', yellow: '#f1fa8c', blue: '#bd93f9', magenta: '#ff79c6', cyan: '#8be9fd' }
// One Dark theme: { background: '#282c34', foreground: '#abb2bf', cursor: '#528bff', red: '#e06c75', green: '#98c379', yellow: '#e5c07b', blue: '#61afef', magenta: '#c678dd', cyan: '#56b6c2' }
Component Architecture
App.tsx # State: phase, credentials, agents, winner ├── SetupWizard.tsx # Credentials input form └── GridView.tsx # Header bar + 2x2 grid └── AgentCard.tsx # Card with status badge + terminal └── Terminal.tsx # xterm.js instance
App Phases
-
setup
-
Show SetupWizard
-
ready
-
Show prompt input + idle grid
-
running
-
Agents executing, cancel available
-
completed
-
All done, select winner
Cleanup
Always return unsubscribe function and call on unmount to prevent memory leaks:
useEffect(() => { const unsub = window.api.onAgentOutput(({ agentId, chunk }) => { terminals[agentId].write(chunk) }) return () => unsub() }, [])