22 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Maestro is a unified, highly-responsive Electron desktop application for managing multiple AI coding assistants (Claude Code, Aider, OpenCode, etc.) simultaneously. It provides a Linear/Superhuman-level responsive interface with keyboard-first navigation, dual-mode input (terminal vs AI), and remote web access capabilities.
Development Commands
Running the Application
# Development mode with hot reload
npm run dev
# Build and run production
npm run build
npm start
Building
# Build both main and renderer processes
npm run build
# Build main process only (Electron backend)
npm run build:main
# Build renderer only (React frontend)
npm run build:renderer
Packaging
# Package for all platforms
npm run package
# Platform-specific builds
npm run package:mac # macOS (.dmg, .zip)
npm run package:win # Windows (.exe, portable)
npm run package:linux # Linux (.AppImage, .deb, .rpm)
Utilities
# Clean build artifacts and cache
npm run clean
Architecture
Dual-Process Model
Maestro uses Electron's main/renderer architecture with strict context isolation:
Main Process (src/main/) - Node.js backend with full system access
index.ts- Application entry point, IPC handler registration, window managementprocess-manager.ts- Core primitive for spawning and managing CLI processesweb-server.ts- Fastify-based HTTP/WebSocket server for remote accessagent-detector.ts- Auto-detects available AI tools (Claude Code, Aider, etc.) via PATHpreload.ts- Secure IPC bridge via contextBridge (no direct Node.js exposure to renderer)
Renderer Process (src/renderer/) - React frontend with no direct Node.js access
App.tsx- Main UI component (being refactored - currently 2,988 lines)main.tsx- Renderer entry pointcomponents/- React components (modals, panels, UI elements)SessionList.tsx- Left sidebar component (extracted from App.tsx)SettingsModal.tsx,NewInstanceModal.tsx,Scratchpad.tsx,FilePreview.tsx- Other UI components
hooks/- Custom React hooks for reusable state logicuseSettings.ts- Settings management and persistenceuseSessionManager.ts- Session and group CRUD operationsuseFileExplorer.ts- File tree state and operations
services/- Business logic services (clean wrappers around IPC calls)git.ts- Git operations (status, diff, isRepo)process.ts- Process management (spawn, write, kill, resize)
Process Management System
The ProcessManager class is the core architectural primitive that abstracts two process types:
-
PTY Processes (via
node-pty) - For terminal sessions with full shell emulation- Used for
toolType: 'terminal' - Supports resize, ANSI escape codes, interactive shell
- Used for
-
Child Processes (via
child_process) - For AI assistants- Used for all non-terminal tool types (claude-code, aider, etc.)
- Direct stdin/stdout/stderr capture without shell interpretation
- Security: Uses
spawn()withshell: falseto prevent command injection
All process operations go through IPC handlers in src/main/index.ts:
process:spawn- Start a new processprocess:write- Send data to stdinprocess:kill- Terminate a processprocess:resize- Resize PTY terminal (terminal mode only)
Events are emitted back to renderer via:
process:data- Stdout/stderr outputprocess:exit- Process exit code
Session Model
Each "session" is a unified abstraction with these attributes:
sessionId- Unique identifiertoolType- Agent type (claude-code, aider, terminal, custom)cwd- Working directorystate- Current state (idle, busy, error)stdinMode- Input routing mode (terminal vs AI)
IPC Security Model
All renderer-to-main communication goes through the preload script:
- Context isolation: Enabled (renderer has no direct Node.js access)
- Node integration: Disabled (no
require()in renderer) - Preload script: Exposes minimal API via
contextBridge.exposeInMainWorld('maestro', ...)
The window.maestro API provides type-safe access to:
- Settings management
- Process control
- Git operations
- File system access
- Tunnel management
- Agent detection
Git Integration
Git operations use the safe execFileNoThrow utility (located in src/main/utils/execFile.ts) to prevent shell injection vulnerabilities:
git:status- Get porcelain statusgit:diff- Get diff for filesgit:isRepo- Check if directory is a Git repository
Web Server Architecture
Fastify server (src/main/web-server.ts) provides:
- REST API endpoints (
/api/sessions,/health) - WebSocket endpoint (
/ws) for real-time updates - CORS enabled for mobile/remote access
- Binds to
0.0.0.0:8000for LAN access
Agent Detection
AgentDetector class auto-discovers CLI tools in PATH:
- Uses
which(Unix) orwhere(Windows) viaexecFileNoThrow - Caches results for performance
- Pre-configured agents: Claude Code, Aider, Qwen Coder, CLI Terminal
- Extensible via
AGENT_DEFINITIONSarray insrc/main/agent-detector.ts
Key Design Patterns
Dual-Mode Input Router
Sessions toggle between two input modes:
- Terminal Mode - Raw shell commands via PTY
- AI Interaction Mode - Direct communication with AI assistant
This is implemented via the isTerminal flag in ProcessManager.
Event-Driven Output Streaming
ProcessManager extends EventEmitter:
processManager.on('data', (sessionId, data) => { ... })
processManager.on('exit', (sessionId, code) => { ... })
Events are forwarded to renderer via IPC:
mainWindow?.webContents.send('process:data', sessionId, data)
Secure Command Execution
ALWAYS use execFileNoThrow utility from src/main/utils/execFile.ts for running external commands. This prevents shell injection vulnerabilities by using execFile instead of exec:
// Correct - safe from injection
import { execFileNoThrow } from './utils/execFile';
const result = await execFileNoThrow('git', ['status', '--porcelain'], cwd);
// The utility returns: { stdout: string, stderr: string, exitCode: number }
// It never throws - non-zero exit codes return exitCode !== 0
Custom Hooks Architecture
The renderer now uses custom hooks to encapsulate reusable state logic and reduce the size of App.tsx.
useSettings (src/renderer/hooks/useSettings.ts)
Manages all application settings with automatic persistence to electron-store.
What it manages:
- LLM settings (provider, model, API key)
- Tunnel settings (provider, API key)
- Agent settings (default agent)
- Font settings (family, size, custom fonts)
- UI settings (theme, enter-to-send, panel widths, markdown mode)
- Keyboard shortcuts
Usage:
import { useSettings } from './hooks';
const settings = useSettings();
// Access: settings.llmProvider, settings.fontSize, etc.
// Update: settings.setTheme('dracula'), settings.setFontSize(16), etc.
All setter functions automatically persist to electron-store.
useSessionManager (src/renderer/hooks/useSessionManager.ts)
Manages sessions and groups with CRUD operations, drag & drop, and persistence.
What it manages:
- Sessions array and groups array
- Active session selection
- Session CRUD (create, delete, rename, toggle modes)
- Group operations (create, toggle collapse, rename)
- Drag and drop state and handlers
- Automatic persistence to electron-store
Usage:
import { useSessionManager } from './hooks';
const sessionManager = useSessionManager();
// Access: sessionManager.sessions, sessionManager.activeSession, etc.
// Operations: sessionManager.createNewSession(), sessionManager.deleteSession(), etc.
Key methods:
createNewSession(agentId, workingDir, name)- Create new sessiondeleteSession(id, showConfirmation)- Delete with confirmationtoggleInputMode()- Switch between AI and terminal modeupdateScratchPad(content)- Update session scratchpadcreateNewGroup(name, emoji, moveSession, activeSessionId)- Create group
useFileExplorer (src/renderer/hooks/useFileExplorer.ts)
Manages file tree state, expansion, navigation, and file operations.
What it manages:
- File preview state
- File tree navigation and selection
- Folder expansion/collapse
- File tree filtering
- File loading and operations
Usage:
import { useFileExplorer } from './hooks';
const fileExplorer = useFileExplorer(activeSession, setActiveFocus);
// Access: fileExplorer.previewFile, fileExplorer.filteredFileTree, etc.
// Operations: fileExplorer.handleFileClick(), fileExplorer.expandAllFolders(), etc.
Key methods:
handleFileClick(node, path, activeSession)- Open file or external apploadFileTree(dirPath, maxDepth?)- Load directory treetoggleFolder(path, activeSessionId, setSessions)- Toggle folder expansionexpandAllFolders()/collapseAllFolders()- Bulk operationsupdateSessionWorkingDirectory()- Change session CWD
Services Architecture
Services provide clean wrappers around IPC calls, abstracting away the window.maestro API details.
Git Service (src/renderer/services/git.ts)
Provides type-safe git operations.
Usage:
import { gitService } from '../services/git';
// Check if directory is a git repo
const isRepo = await gitService.isRepo(cwd);
// Get git status
const status = await gitService.getStatus(cwd);
// Returns: { files: [{ path: string, status: string }] }
// Get git diff
const diff = await gitService.getDiff(cwd, ['file1.ts', 'file2.ts']);
// Returns: { diff: string }
All methods handle errors gracefully and return safe defaults.
Process Service (src/renderer/services/process.ts)
Provides type-safe process management operations.
Usage:
import { processService } from '../services/process';
// Spawn a process
await processService.spawn(sessionId, {
cwd: '/path/to/dir',
command: 'claude-code',
args: [],
isTerminal: false
});
// Write to process stdin
await processService.write(sessionId, 'user input\n');
// Kill process
await processService.kill(sessionId);
// Listen for process events
const unsubscribeData = processService.onData((sessionId, data) => {
console.log('Process output:', data);
});
const unsubscribeExit = processService.onExit((sessionId, code) => {
console.log('Process exited:', code);
});
// Clean up listeners
unsubscribeData();
unsubscribeExit();
UI Architecture & Components
Main Application Structure (App.tsx)
The main application is structured in three columns:
- Left Sidebar - Session list, groups, new instance button
- Main Panel - Terminal/AI output, input area, toolbar
- Right Panel - Files, History, Scratchpad tabs
Key Components
SettingsModal (src/renderer/components/SettingsModal.tsx)
- Tabbed interface: General, LLM, Shortcuts, Themes, Network
- All settings changes should use wrapper functions for persistence
- Includes LLM test functionality to verify API connectivity
Scratchpad (src/renderer/components/Scratchpad.tsx)
- Edit/Preview mode toggle (Command-E to switch)
- Markdown rendering with GFM support
- Smart list continuation (unordered, ordered, task lists)
- Container must be focusable (tabIndex) for keyboard shortcuts
FilePreview (src/renderer/components/FilePreview.tsx)
- Full-screen overlay for file viewing
- Syntax highlighting via react-syntax-highlighter
- Markdown rendering for .md files
- Arrow keys for scrolling, Escape to close
- Auto-focuses when opened for immediate keyboard control
Keyboard Navigation Patterns
The app is keyboard-first with these patterns:
Focus Management:
- Escape in input → Focus output window
- Escape in output → Focus back to input
- Escape in file preview → Return to file tree
- Components need
tabIndex={-1}andoutline-nonefor programmatic focus
Output Window:
/→ Open search/filter- Arrow Up/Down → Scroll output
- Cmd/Ctrl + Arrow Up/Down → Jump to top/bottom
- Escape → Close search (if open) or return to input
File Tree:
- Arrow keys → Navigate files/folders
- Enter → Open file preview
- Space → Toggle folder expansion
- Cmd+E → Expand all, Cmd+Shift+E → Collapse all
Scratchpad:
- Cmd+E → Toggle Edit/Preview mode
Theme System
Themes defined in THEMES object in App.tsx with structure:
{
id: string;
name: string;
mode: 'light' | 'dark';
colors: {
bgMain: string; // Main content background
bgSidebar: string; // Sidebar background
bgActivity: string; // Accent background
border: string; // Border colors
textMain: string; // Primary text
textDim: string; // Secondary text
accent: string; // Accent color
accentDim: string; // Dimmed accent
success: string; // Success state
warning: string; // Warning state
error: string; // Error state
}
}
Use style={{ color: theme.colors.textMain }} instead of fixed colors.
Styling Conventions
- Tailwind CSS for layout and spacing
- Inline styles for theme colors (dynamic based on selected theme)
- Standard spacing:
gap-2,p-4,mb-3for consistency - Focus states: Always add
outline-nonewhen usingtabIndex - Sticky elements: Use
sticky top-0 z-10with solid background - Overlays: Use
fixed inset-0with backdrop blur and high z-index
State Management Per Session
Each session stores:
cwd- Current working directoryfileTree- File tree structurefileExplorerExpanded- Expanded folder pathsfileExplorerScrollPos- Scroll position in file treeaiLogs/shellLogs- Output historyinputMode- 'ai' or 'terminal'state- 'idle' | 'busy' | 'waiting_input'
Sessions persist scroll positions, expanded states, and UI state per-session.
Code Conventions
TypeScript
- All code is TypeScript with strict mode enabled
- Interface definitions for all data structures
- Type exports via
preload.tsfor renderer types
Commit Message Format
Use conventional commits:
feat:- New featuresfix:- Bug fixesdocs:- Documentation changesrefactor:- Code refactoringtest:- Test additions/changeschore:- Build process or tooling changes
Security Requirements
- Use
execFileNoThrowfor all external commands - Located insrc/main/utils/execFile.ts - Context isolation - Keep enabled in BrowserWindow
- Input sanitization - Validate all user inputs
- Minimal preload exposure - Only expose necessary APIs via contextBridge
- Process spawning - Use
spawn()withshell: falseflag
Technology Stack
Backend (Main Process)
- Electron 28+
- TypeScript
- node-pty - Terminal emulation
- Fastify - Web server
- electron-store - Settings persistence
- ws - WebSocket support
Frontend (Renderer)
- React 18
- TypeScript
- Tailwind CSS
- Vite
- Lucide React - Icons
- react-syntax-highlighter - Code display
- marked - Markdown rendering
- dompurify - XSS prevention
Settings Storage
Settings persisted via electron-store:
- macOS:
~/Library/Application Support/maestro/ - Windows:
%APPDATA%/maestro/ - Linux:
~/.config/maestro/
Files:
maestro-settings.json- User preferencesmaestro-sessions.json- Session persistence (planned)maestro-groups.json- Session groups (planned)
Adding New Persistent Settings
To add a new setting that persists across sessions:
- Define state variable in App.tsx:
const [mySetting, setMySettingState] = useState<MyType>(defaultValue);
- Create wrapper function that persists:
const setMySetting = (value: MyType) => {
setMySettingState(value);
window.maestro.settings.set('mySetting', value);
};
- Load in useEffect:
// Inside the loadSettings useEffect
const savedMySetting = await window.maestro.settings.get('mySetting');
if (savedMySetting !== undefined) setMySettingState(savedMySetting);
- Pass wrapper to child components, not the direct setState
Current Persistent Settings:
llmProvider,modelSlug,apiKey- LLM configurationtunnelProvider,tunnelApiKey- Tunnel configurationdefaultAgent- Default AI agent selectionfontFamily,fontSize,customFonts- UI font settingsenterToSend- Input behavior (Enter vs Command-Enter to send)activeThemeId- Selected theme
Development Phases
The project follows a phased development approach (see PRD.md):
Completed Phases:
- Phase 1: Core Primitives (ProcessManager, Session Model, Dual-Mode Input)
- Phase 2: UI Foundations (Obsidian-inspired design, keyboard navigation)
In Progress:
- Phase 3: Intelligence Layer (auto-generated descriptions, semantic worklogs)
Planned:
- Phase 4: Workspace Intelligence (dual Git/non-Git modes)
- Phase 5: Status & Control Layer
- Phase 6: Remote Access & Tunneling (ngrok integration)
- Phase 7: Performance & Polish
- Phase 8: Multi-Device Continuity
Common Development Tasks
Adding a New UI Feature
- Plan the state - Determine if it's per-session or global
- Add state management - In App.tsx or component
- Create persistence - Use wrapper function pattern if global
- Implement UI - Follow Tailwind + theme color pattern
- Add keyboard shortcuts - Integrate with existing keyboard handler
- Test focus flow - Ensure Escape key navigation works
Adding a New Modal
- Create component in
src/renderer/components/ - Add state in App.tsx:
const [myModalOpen, setMyModalOpen] = useState(false) - Add Escape handler in keyboard shortcuts
- Use
fixed inset-0overlay withz-[50]or higher - Include close button and backdrop click handler
- Use
ref={(el) => el?.focus()}for immediate keyboard control
Adding Keyboard Shortcuts
- Find the main keyboard handler in App.tsx (around line 650)
- Add your shortcut in the appropriate section:
- Component-specific: Inside component's onKeyDown
- Global: In main useEffect keyboard handler
- Modify pressed: Check for
e.metaKey || e.ctrlKey
- Remember to
e.preventDefault()to avoid browser defaults
Working with File Tree
File tree structure is stored per-session as fileTree array of nodes:
{
name: string;
type: 'file' | 'folder';
path: string;
children?: FileTreeNode[];
}
Expanded folders tracked in session's fileExplorerExpanded: string[] as full paths.
Modifying Themes
- Find
THEMESconstant in App.tsx - Add new theme or modify existing one
- All color keys must be present (11 required colors)
- Test in both light and dark mode contexts
- Theme ID is stored in settings and persists across sessions
Adding to Settings Modal
- Add tab if needed in SettingsModal.tsx
- Create state in App.tsx with wrapper function
- Add to loadSettings useEffect
- Pass wrapper function (not setState) to SettingsModal props
- Add UI in appropriate tab section
Important File Locations
Main Process Entry Points
src/main/index.ts:81-109- IPC handler setupsrc/main/index.ts:272-282- Process event listenerssrc/main/process-manager.ts:30-116- Process spawning logic
Security-Critical Code
src/main/preload.ts:5- Context bridge API exposuresrc/main/process-manager.ts:75- Shell disabled for spawnsrc/main/utils/execFile.ts- Safe command execution wrapper
Configuration
package.json:26-86- electron-builder configtsconfig.json- Renderer TypeScript configtsconfig.main.json- Main process TypeScript configvite.config.mts- Vite bundler config
Debugging & Common Issues
Focus Not Working
If keyboard shortcuts aren't working:
- Check element has
tabIndex={0}ortabIndex={-1} - Add
outline-noneclass to hide focus ring - Use
ref={(el) => el?.focus()}oruseEffectto auto-focus - Check for
e.stopPropagation()blocking events
Settings Not Persisting
If settings don't save across sessions:
- Ensure you created a wrapper function with
window.maestro.settings.set() - Check the wrapper is passed to child components, not the direct setState
- Verify loading code exists in the
loadSettingsuseEffect - Use direct state setter (e.g.,
setMySettingState) in the loading code
Modal Escape Key Not Working
If Escape doesn't close a modal:
- Modal overlay needs
tabIndex={0} - Use
ref={(el) => el?.focus()}to focus on mount - Add
e.stopPropagation()in onKeyDown handler - Check z-index is higher than other modals
Theme Colors Not Applying
If colors appear hardcoded:
- Replace fixed colors with
style={{ color: theme.colors.textMain }} - Never use hardcoded hex colors for text/borders
- Use inline styles for theme colors, Tailwind for layout
- Check theme prop is being passed down correctly
Scroll Position Not Saving
Per-session scroll position:
- Container needs a ref:
useRef<HTMLDivElement>(null) - Add
onScrollhandler that updates session state - Add useEffect to restore scroll on session change
- Use
ref.current.scrollTopto get/set position
Running Tests
Currently no test suite implemented. When adding tests, use the test script in package.json.
Recent Features Added
- Output Search/Filter - Press
/in output window to filter logs - Scratchpad Command-E - Toggle between Edit and Preview modes
- File Preview Focus - Arrow keys scroll, Escape returns to file tree
- Sticky File Tree Header - Working directory stays visible when scrolling
- LLM Connection Test - Test API connectivity in Settings before saving
- Settings Persistence - All settings now save across sessions
- Emoji Picker Improvements - Proper positioning and Escape key support