diff --git a/CLAUDE.md b/CLAUDE.md index ff772719..6ef50e41 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,1221 +1,223 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +Essential guidance for working with this codebase. For detailed architecture, see [ARCHITECTURE.md](ARCHITECTURE.md). For development setup and processes, see [CONTRIBUTING.md](CONTRIBUTING.md). ## Standardized Vernacular -To maintain consistency in code, comments, and documentation, use these terms: +Use these terms consistently in code, comments, and documentation: ### UI Components -- **Left Bar** - The left sidebar containing session list and groups (`SessionList.tsx`) -- **Right Bar** - The right sidebar with Files, History, and Scratchpad tabs (`RightPanel.tsx`) -- **Main Window** - The center workspace area (`MainPanel.tsx`) - - **AI Terminal** - The main window when in AI mode (for interacting with AI agents) - - **Command Terminal** - The main window when in terminal/shell mode (for running shell commands) - - **System Log Viewer** - Special view in main window for system logs (`LogViewer.tsx`) +- **Left Bar** - Left sidebar with session list and groups (`SessionList.tsx`) +- **Right Bar** - Right sidebar with Files, History, Scratchpad tabs (`RightPanel.tsx`) +- **Main Window** - Center workspace (`MainPanel.tsx`) + - **AI Terminal** - Main window in AI mode (interacting with AI agents) + - **Command Terminal** - Main window in terminal/shell mode + - **System Log Viewer** - Special view for system logs (`LogViewer.tsx`) -### Session States -Session status indicators use color-coding: -- **Green** - Ready and waiting (idle state) -- **Yellow** - Agent is thinking (busy state) -- **Red** - No connection with agent (error state) -- **Pulsing Orange** - Attempting to establish connection (connecting state) +### Session States (color-coded) +- **Green** - Ready/idle +- **Yellow** - Agent thinking/busy +- **Red** - No connection/error +- **Pulsing Orange** - Connecting ## 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 (Command Terminal vs AI Terminal), and remote web access capabilities. +Maestro is an Electron desktop app for managing multiple AI coding assistants (Claude Code, Aider, Qwen Coder) simultaneously with a keyboard-first interface. -## Development Commands - -### Running the Application +## Quick Commands ```bash -# Development mode with hot reload -npm run dev - -# Build and run production -npm run build -npm start +npm run dev # Development with hot reload +npm run build # Full production build +npm run clean # Clean build artifacts +npm run package # Package for all platforms ``` -### Building +## Architecture at a Glance -```bash -# 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 +``` +src/ +├── main/ # Electron main process (Node.js) +│ ├── index.ts # Entry point, IPC handlers +│ ├── process-manager.ts # Process spawning (PTY + child_process) +│ ├── preload.ts # Secure IPC bridge +│ └── utils/execFile.ts # Safe command execution +│ +└── renderer/ # React frontend + ├── App.tsx # Main coordinator + ├── components/ # UI components + ├── hooks/ # Custom React hooks + ├── services/ # IPC wrappers (git.ts, process.ts) + ├── constants/ # Themes, shortcuts, priorities + └── contexts/ # Layer stack context ``` -### Packaging +### Key Files for Common Tasks -```bash -# Package for all platforms -npm run package +| Task | Primary Files | +|------|---------------| +| Add IPC handler | `src/main/index.ts`, `src/main/preload.ts` | +| Add UI component | `src/renderer/components/` | +| Add keyboard shortcut | `src/renderer/constants/shortcuts.ts`, `App.tsx` | +| Add theme | `src/renderer/constants/themes.ts` | +| Add slash command | `src/renderer/slashCommands.ts` | +| Add modal | Component + `src/renderer/constants/modalPriorities.ts` | +| Add setting | `src/renderer/hooks/useSettings.ts`, `src/main/index.ts` | -# 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) -``` +## Core Patterns -### Utilities +### 1. Process Management -```bash -# 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 management -- `process-manager.ts` - Core primitive for spawning and managing CLI processes -- `web-server.ts` - Fastify-based HTTP/WebSocket server for remote access -- `agent-detector.ts` - Auto-detects available AI tools (Claude Code, Aider, etc.) via PATH -- `preload.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 coordinator (~1,650 lines, continuously being refactored) -- `main.tsx` - Renderer entry point -- `components/` - React components (modals, panels, UI elements) - - `SessionList.tsx` - Left Bar with sessions and groups - - `MainPanel.tsx` - Main Window (AI Terminal, Command Terminal, System Log Viewer, Agent Sessions Browser) - - `RightPanel.tsx` - Right Bar (files, history, scratchpad) - - `LogViewer.tsx` - System Log Viewer with filtering and search - - `AgentSessionsBrowser.tsx` - Browse and resume Claude Code sessions from `~/.claude/projects/` - - `SettingsModal.tsx`, `NewInstanceModal.tsx`, `Scratchpad.tsx`, `FilePreview.tsx` - Other UI components -- `hooks/` - Custom React hooks for reusable state logic - - `useSettings.ts` - Settings management and persistence - - `useSessionManager.ts` - Session and group CRUD operations - - `useFileExplorer.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: - -1. **PTY Processes** (via `node-pty`) - For terminal sessions with full shell emulation - - Used for `toolType: 'terminal'` - - Supports resize, ANSI escape codes, interactive shell - -2. **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()` with `shell: false` to prevent command injection - -All process operations go through IPC handlers in `src/main/index.ts`: -- `process:spawn` - Start a new process -- `process:write` - Send data to stdin -- `process:kill` - Terminate a process -- `process:resize` - Resize PTY terminal (Command Terminal only) - -Events are emitted back to renderer via: -- `process:data` - Stdout/stderr output -- `process:exit` - Process exit code -- `process:usage` - Usage statistics from AI responses (tokens, cost, context window) - -### Session Model - -Each "session" is a unified abstraction running **two processes simultaneously** (dual-process architecture): -- `sessionId` - Unique identifier (suffixed with `-ai` or `-terminal` for each process) -- `toolType` - Agent type (claude-code, aider, terminal, custom) -- `cwd` - Working directory -- `state` - Current state (idle, busy, error) -- `inputMode` - Input routing mode ('terminal' or 'ai') -- `aiPid` - Process ID for the AI agent process -- `terminalPid` - Process ID for the terminal process -- `usageStats` - Token usage and cost statistics from AI responses (optional) - -This dual-process model allows seamless switching between AI and terminal modes without restarting processes. Input is routed to the appropriate process based on `inputMode`. - -### 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 -- Claude sessions (browse/resume Claude Code sessions) - -### 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 status -- `git:diff` - Get diff for files -- `git: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:8000` for LAN access - -### Agent Detection - -`AgentDetector` class auto-discovers CLI tools in PATH: -- Uses `which` (Unix) or `where` (Windows) via `execFileNoThrow` -- Caches results for performance -- Pre-configured agents: Claude Code, Aider, Qwen Coder, CLI Terminal -- Extensible via `AGENT_DEFINITIONS` array in `src/main/agent-detector.ts` - -### Claude Sessions API - -Maestro can browse and resume Claude Code sessions stored in `~/.claude/projects/`. Sessions are JSONL files containing conversation history. - -**IPC Handlers:** -- `claude:listSessions` - Lists all sessions for a project path -- `claude:readSessionMessages` - Reads messages from a session (with lazy loading via offset/limit) -- `claude:searchSessions` - Searches session content by title, user messages, assistant messages, or all - -**Session Path Encoding:** -Claude Code encodes project paths by replacing `/` with `-`. For example: -- `/Users/pedram/Projects/Maestro` → `-Users-pedram-Projects-Maestro` -- Sessions stored at: `~/.claude/projects/-Users-pedram-Projects-Maestro/*.jsonl` - -**Usage in Renderer:** -```typescript -// List sessions for current project -const sessions = await window.maestro.claude.listSessions(activeSession.cwd); - -// Read messages with pagination (reads from end) -const { messages, total, hasMore } = await window.maestro.claude.readSessionMessages( - projectPath, - sessionId, - { offset: 0, limit: 20 } -); - -// Search across sessions -const results = await window.maestro.claude.searchSessions( - projectPath, - 'search query', - 'all' // 'title' | 'user' | 'assistant' | 'all' -); -``` - -**UI Integration:** -- Open with `Cmd+Shift+L` (configurable in Settings > Shortcuts) -- Available via Quick Actions (`Cmd+K` → "View Agent Sessions") -- Button in main panel header (List icon) -- Shows session list with search, message preview, and resume functionality - -## Key Design Patterns - -### Slash Commands System - -Maestro implements an extensible slash command system in `src/renderer/slashCommands.ts`: +Each session runs **two processes** simultaneously: +- AI agent process (Claude Code, etc.) - spawned with `-ai` suffix +- Terminal process (PTY shell) - spawned with `-terminal` suffix ```typescript -export interface SlashCommand { - command: string; // The command string (e.g., "/clear") - description: string; // Human-readable description - terminalOnly?: boolean; // Only show in terminal mode (optional) - execute: (context: SlashCommandContext) => void; // Command handler -} +// Session stores both PIDs +session.aiPid // AI agent process +session.terminalPid // Terminal process ``` -**Architecture:** -- Commands are defined in a single registry (`slashCommands` array) -- Autocomplete UI appears when user types `/` -- Keyboard navigation with arrow keys, Tab/Enter to select -- Commands receive execution context (activeSessionId, sessions, setSessions, currentMode) -- Commands can modify session state, trigger actions, or interact with IPC -- Commands can be mode-specific using `terminalOnly: true` flag +### 2. Security Requirements -**Adding new commands:** -1. Add new entry to `slashCommands` array in `src/renderer/slashCommands.ts` -2. Implement `execute` function with desired behavior -3. Optionally set `terminalOnly: true` to restrict to terminal mode -4. Command automatically appears in autocomplete (filtered by mode) - -**Current commands:** -- `/clear` - Clears output history for current mode (AI or terminal) -- `/jump` - Jumps to current working directory in file tree (terminal mode only) - -### Layer Stack System - -Maestro uses a centralized layer stack system for managing modals, overlays, and search layers with predictable Escape key handling and priority-based ordering. - -**Problem Solved:** -- Previously had 9+ scattered Escape handlers competing for events -- Brittle modal detection with massive boolean checks -- Manual priority management via if-else chains (50+ lines) -- Inconsistent focus management - -**Architecture:** -- `useLayerStack` hook (`src/renderer/hooks/useLayerStack.ts`) - Core layer management -- `LayerStackContext` (`src/renderer/contexts/LayerStackContext.tsx`) - Global Escape handler via capture-phase listener -- `MODAL_PRIORITIES` (`src/renderer/constants/modalPriorities.ts`) - Explicit z-index/priority values -- `Layer` types (`src/renderer/types/layer.ts`) - Discriminated union (ModalLayer, OverlayLayer) - -**Key Features:** -- Single global Escape handler delegates to topmost layer -- Priority-based ordering (higher number = higher priority) -- Automatic layer registration/unregistration on mount/unmount -- Performance-optimized (handler updates don't trigger re-sorts) -- Type-safe discriminated unions -- Built-in dev tools for debugging layer stack - -**Modal Priority Hierarchy:** +**Always use `execFileNoThrow`** for external commands: ```typescript -CONFIRM: 1000 // Highest - confirmation dialogs -RENAME_INSTANCE: 900 -RENAME_GROUP: 850 -CREATE_GROUP: 800 -NEW_INSTANCE: 750 -QUICK_ACTION: 700 // Command palette (Cmd+K) -AGENT_SESSIONS: 680 // Agent sessions browser (Cmd+Shift+L) -SHORTCUTS_HELP: 650 -ABOUT: 600 -PROCESS_MONITOR: 550 -LOG_VIEWER: 500 -SETTINGS: 450 -GIT_DIFF: 200 -LIGHTBOX: 150 -FILE_PREVIEW: 100 -SLASH_AUTOCOMPLETE: 50 -FILE_TREE_FILTER: 30 // Lowest - inline search +import { execFileNoThrow } from './utils/execFile'; +const result = await execFileNoThrow('git', ['status'], cwd); +// Returns: { stdout, stderr, exitCode } - never throws ``` -**Adding a New Modal:** +**Never use shell-based command execution** - it creates injection vulnerabilities. The `execFileNoThrow` utility is the safe alternative. + +### 3. Settings Persistence + +Add new settings in `useSettings.ts`: +```typescript +// 1. Add state +const [mySetting, setMySettingState] = useState(defaultValue); + +// 2. Add wrapper that persists +const setMySetting = (value) => { + setMySettingState(value); + window.maestro.settings.set('mySetting', value); +}; + +// 3. Load in useEffect +const saved = await window.maestro.settings.get('mySetting'); +if (saved !== undefined) setMySettingState(saved); +``` + +### 4. Adding Modals + +1. Create component in `src/renderer/components/` +2. Add priority in `src/renderer/constants/modalPriorities.ts` +3. Register with layer stack: -1. **Choose priority** - Select appropriate value from `MODAL_PRIORITIES` or add new constant -2. **Import layer stack hook**: ```typescript import { useLayerStack } from '../contexts/LayerStackContext'; import { MODAL_PRIORITIES } from '../constants/modalPriorities'; -``` -3. **Register layer on mount** (use ref pattern to avoid re-registration on callback changes): -```typescript -const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack(); -const layerIdRef = useRef(); - -// Store onClose in ref to avoid re-registering layer when callback identity changes +const { registerLayer, unregisterLayer } = useLayerStack(); const onCloseRef = useRef(onClose); onCloseRef.current = onClose; useEffect(() => { - if (modalOpen) { + if (isOpen) { const id = registerLayer({ - type: 'modal', // or 'overlay' + type: 'modal', priority: MODAL_PRIORITIES.YOUR_MODAL, - blocksLowerLayers: true, - capturesFocus: true, - focusTrap: 'strict', // or 'lenient', 'none' - ariaLabel: 'Your Modal Name', - onEscape: () => onCloseRef.current(), // Use ref to get latest callback + onEscape: () => onCloseRef.current(), }); - layerIdRef.current = id; return () => unregisterLayer(id); } -}, [modalOpen, registerLayer, unregisterLayer]); // Note: onClose NOT in deps +}, [isOpen, registerLayer, unregisterLayer]); ``` -4. **Update handler when dependencies change** (only needed if handler has other dependencies): +### 5. Theme Colors + +Themes have 12 required colors. Use inline styles for theme colors: ```typescript -useEffect(() => { - if (modalOpen && layerIdRef.current) { - updateLayerHandler(layerIdRef.current, () => onCloseRef.current()); - } -}, [modalOpen, updateLayerHandler]); // Use ref for callbacks +style={{ color: theme.colors.textMain }} // Correct +className="text-gray-500" // Wrong for themed text ``` -**Why use the ref pattern?** Parent components often create new callback instances on every render. Without the ref pattern, this would cause the layer to be unregistered and re-registered unnecessarily, which can cause flickering or focus issues. - -5. **Add ARIA attributes**: -```typescript -
el?.focus()} // Auto-focus on mount -> -``` - -6. **Remove local Escape handlers** - Let layer stack handle it - -**Layer Types:** -```typescript -// Modal - full-screen overlay that blocks interaction -type ModalLayer = { - type: 'modal'; - priority: number; - blocksLowerLayers: boolean; - capturesFocus: boolean; - focusTrap: 'strict' | 'lenient' | 'none'; - ariaLabel?: string; - onEscape: () => void; - onBeforeClose?: () => Promise; // Optional confirmation - isDirty?: boolean; - parentModalId?: string; -}; - -// Overlay - semi-transparent overlay (file preview, search) -type OverlayLayer = { - type: 'overlay'; - priority: number; - blocksLowerLayers: boolean; - capturesFocus: boolean; - focusTrap: 'strict' | 'lenient' | 'none'; - ariaLabel?: string; - onEscape: () => void; - allowClickOutside: boolean; -}; -``` - -**Internal Search Layers:** -Components like FilePreview, TerminalOutput, and LogViewer handle internal search state in their `onEscape` handler: -```typescript -onEscape: () => { - if (searchOpen) { - setSearchOpen(false); // First Escape closes search - } else { - closePreview(); // Second Escape closes preview - } -} -``` - -**Benefits:** -- No more manual modal priority if-else chains -- Predictable, testable behavior -- Easy to add new modals (just set priority) -- Single source of truth for layer ordering -- Removed 100+ lines of brittle modal management code - -**Debugging:** -In development mode, use `LayerStackDevTools` component (bottom-right overlay) or browser console: -```javascript -window.__MAESTRO_DEBUG__.layers.list() // Show all layers -window.__MAESTRO_DEBUG__.layers.top() // Show top layer -window.__MAESTRO_DEBUG__.layers.simulate.escape() // Simulate Escape key -``` - -### Dual-Mode Input Router - -Sessions toggle between two input modes: -1. **Terminal Mode** - Raw shell commands via PTY -2. **AI Interaction Mode** - Direct communication with AI assistant - -This is implemented via the `isTerminal` flag in `ProcessManager`. - -### Event-Driven Output Streaming - -ProcessManager extends EventEmitter: -```typescript -processManager.on('data', (sessionId, data) => { ... }) -processManager.on('exit', (sessionId, code) => { ... }) -``` - -Events are forwarded to renderer via IPC: -```typescript -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`: - -```typescript -// 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:** -```typescript -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:** -```typescript -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 session -- `deleteSession(id, showConfirmation)` - Delete with confirmation -- `toggleInputMode()` - Switch between AI and terminal mode -- `updateScratchPad(content)` - Update session scratchpad -- `createNewGroup(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:** -```typescript -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 app -- `loadFileTree(dirPath, maxDepth?)` - Load directory tree -- `toggleFolder(path, activeSessionId, setSessions)` - Toggle folder expansion -- `expandAllFolders()` / `collapseAllFolders()` - Bulk operations -- `updateSessionWorkingDirectory()` - 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:** -```typescript -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:** -```typescript -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: -1. **Left Sidebar** - Session list, groups, new instance button -2. **Main Panel** - Terminal/AI output, input area, toolbar -3. **Right Panel** - Files, History, Scratchpad tabs - -### Key Components - -#### MainPanel (`src/renderer/components/MainPanel.tsx`) -- Center workspace container that handles three states: - - LogViewer when system logs are open - - Empty state when no session is active - - Normal session view (top bar, terminal output, input area, file preview) -- Encapsulates all main panel UI logic outside of App.tsx - -#### LogViewer (`src/renderer/components/LogViewer.tsx`) -- System logs viewer accessible via Cmd+K → "View System Logs" -- Color-coded log levels (Debug, Info, Warn, Error) -- Searchable with `/` key, filterable by log level -- Export logs to file, clear all logs -- Keyboard navigation (arrows to scroll, Cmd+arrows to jump) - -#### 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 -- Log level selector with color-coded buttons (defaults to "info") - -#### 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 - -#### GitStatusWidget (`src/renderer/components/GitStatusWidget.tsx`) -- GitHub-style file change tracking widget in the main panel header -- Displays counts of additions (green +), deletions (red -), and modifications (orange) -- Hover tooltip shows list of all changed files with their status -- Click to view full git diff in modal overlay -- Automatically polls git status every 5 seconds -- Only renders when session is in a Git repository -- Integration: Used in MainPanel.tsx between LIVE button and Context Window - -### Keyboard Navigation Patterns - -The app is keyboard-first with these patterns: - -**Focus Management:** -- Cmd+. → Jump to input field (from anywhere in main interface) -- Cmd+Shift+A → Focus left sidebar (expands if collapsed) - configurable -- 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}` and `outline-none` for programmatic focus - -**Output Window:** -- `/` → Open search/filter -- Arrow Up/Down → Scroll output (~100px) -- Option/Alt + Arrow Up/Down → Page up/down (viewport height) -- 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 `src/renderer/constants/themes.ts` with structure: -```typescript -{ - 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 - accentText: string; // Accent text color - success: string; // Success state - warning: string; // Warning state - error: string; // Error state - } -} -``` - -**Available themes:** -- **Dark mode**: Dracula, Monokai, Nord, Tokyo Night, Catppuccin Mocha, Gruvbox Dark -- **Light mode**: GitHub, Solarized, One Light, Gruvbox Light, Catppuccin Latte, Ayu Light - -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-3` for consistency -- **Focus states**: Always add `outline-none` when using `tabIndex` -- **Sticky elements**: Use `sticky top-0 z-10` with solid background -- **Overlays**: Use `fixed inset-0` with backdrop blur and high z-index - -### State Management Per Session - -Each session stores: -- `cwd` - Current working directory -- `fileTree` - File tree structure -- `fileExplorerExpanded` - Expanded folder paths -- `fileExplorerScrollPos` - Scroll position in file tree -- `aiLogs` / `shellLogs` - Output history -- `inputMode` - '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 +- Strict mode enabled - Interface definitions for all data structures -- Type exports via `preload.ts` for renderer types +- Types exported via `preload.ts` for renderer -### Component Extraction Pattern +### React Components +- Functional components with hooks +- Tailwind for layout, inline styles for theme colors +- `tabIndex={-1}` + `outline-none` for programmatic focus -**Principle**: Keep App.tsx minimal by extracting UI sections into dedicated components. - -When adding new features that would add significant complexity to App.tsx: - -1. **Create a new component** in `src/renderer/components/` that encapsulates the entire UI section -2. **Pass only necessary props** - state, setters, refs, and callback functions -3. **Handle all conditional logic** within the component (e.g., empty states, different views) -4. **Keep App.tsx as a coordinator** - it should orchestrate state and wire components together, not contain UI logic - -**Example - MainPanel component:** -```typescript -// App.tsx - Minimal integration - - -// MainPanel.tsx - Contains all UI logic -export function MainPanel(props: MainPanelProps) { - // Handles: log viewer, empty state, normal session view - if (logViewerOpen) return ; - if (!activeSession) return ; - return ; -} +### Commit Messages +``` +feat: new feature +fix: bug fix +docs: documentation +refactor: code refactoring ``` -**Benefits:** -- App.tsx stays manageable and readable -- Components are self-contained and testable -- Changes to UI sections don't bloat App.tsx -- Easier code review and maintenance +## Session Interface -### Commit Message Format - -Use conventional commits: -- `feat:` - New features -- `fix:` - Bug fixes -- `docs:` - Documentation changes -- `refactor:` - Code refactoring -- `test:` - Test additions/changes -- `chore:` - Build process or tooling changes - -### Security Requirements - -1. **Use `execFileNoThrow` for all external commands** - Located in `src/main/utils/execFile.ts` -2. **Context isolation** - Keep enabled in BrowserWindow -3. **Input sanitization** - Validate all user inputs -4. **Minimal preload exposure** - Only expose necessary APIs via contextBridge -5. **Process spawning** - Use `spawn()` with `shell: false` flag - -### Error Handling Patterns - -Maestro uses different error handling strategies depending on the architectural layer: - -#### IPC Handler Errors (Main Process) - -IPC handlers in `src/main/index.ts` should handle errors gracefully but may throw for critical failures: - -**Pattern 1: Throw for critical failures** +Key fields on the Session object: ```typescript -ipcMain.handle('process:spawn', async (_, config) => { - if (!processManager) throw new Error('Process manager not initialized'); - return processManager.spawn(config); -}); -``` -- Use for: Initialization failures, missing required services -- Effect: Renderer will receive rejected promise - -**Pattern 2: Try-catch with boolean return** -```typescript -ipcMain.handle('git:isRepo', async (_, cwd: string) => { - try { - const result = await execFileNoThrow('git', ['rev-parse', '--is-inside-work-tree'], cwd); - return result.exitCode === 0; - } catch { - return false; - } -}); -``` -- Use for: Optional operations where false is a valid answer -- Effect: Graceful degradation (e.g., git features disabled) - -**Pattern 3: Return error in result object** -```typescript -ipcMain.handle('git:status', async (_, cwd: string) => { - const result = await execFileNoThrow('git', ['status', '--porcelain'], cwd); - return { stdout: result.stdout, stderr: result.stderr }; -}); -``` -- Use for: Operations where both success and error info are valuable -- Effect: Caller can inspect both stdout and stderr - -#### Service Layer Errors (Renderer) - -Services in `src/renderer/services/` wrap IPC calls and should never throw: - -```typescript -export const gitService = { - async isRepo(cwd: string): Promise { - try { - const result = await window.maestro.git.isRepo(cwd); - return result; - } catch (error) { - console.error('Git isRepo error:', error); - return false; - } - }, - - async getStatus(cwd: string): Promise { - try { - const result = await window.maestro.git.status(cwd); - return result; - } catch (error) { - console.error('Git status error:', error); - return { files: [] }; // Safe default - } - } -}; -``` - -**Pattern**: Try-catch with safe default return -- Always catch errors from IPC calls -- Log error to console (or use logger utility) -- Return safe default value (empty array, empty string, false, etc.) -- Never throw - let the UI continue functioning - -#### ProcessManager Errors (Main Process) - -`ProcessManager` extends `EventEmitter` and uses events for runtime errors: - -```typescript -// Spawn errors - throw immediately -spawn(config: ProcessConfig): { pid: number; success: boolean } { - try { - // ... spawn logic - return { pid: ptyProcess.pid, success: true }; - } catch (error) { - console.error('Failed to spawn process:', error); - throw error; // Propagate to IPC handler - } -} - -// Runtime errors - emit events -ptyProcess.onExit(({ exitCode }) => { - this.emit('exit', sessionId, exitCode); - this.processes.delete(sessionId); -}); -``` - -**Pattern**: Throw on spawn failure, emit events for runtime errors -- Spawn failures: Throw error (critical, can't continue) -- Process exit: Emit 'exit' event with exit code -- Process output: Emit 'data' event -- Let renderer handle process lifecycle errors - -#### React Component Errors (Renderer) - -React components use Error Boundaries to catch rendering errors: - -```typescript -// Wrap major UI sections in ErrorBoundary - - - -``` - -**Pattern**: Use Error Boundaries for component isolation -- Error boundaries defined in `src/renderer/components/ErrorBoundary.tsx` -- Wrap major UI sections (sidebar, main panel, right panel) -- Display fallback UI with error details and recovery options -- Prevents one component crash from taking down the entire app - -**Component-level error handling**: -```typescript -const handleFileLoad = async (path: string) => { - try { - const content = await window.maestro.fs.readFile(path); - setFileContent(content); - } catch (error) { - console.error('Failed to load file:', error); - setError('Failed to load file'); // Show user-friendly message - } -}; -``` -- Use try-catch for async operations -- Display user-friendly error messages in UI -- Don't crash the component - maintain partial functionality - -#### Custom Hook Errors (Renderer) - -Custom hooks should handle errors internally and expose error state: - -```typescript -const useFileExplorer = (activeSession, setActiveFocus) => { - const [error, setError] = useState(null); - - const loadFileTree = async (dirPath: string) => { - try { - setError(null); - const tree = await buildFileTree(dirPath); - setFileTree(tree); - } catch (err) { - console.error('Failed to load file tree:', err); - setError('Failed to load directory'); - setFileTree([]); // Safe default - } - }; - - return { loadFileTree, error, /* ... */ }; -}; -``` - -**Pattern**: Internal try-catch with error state -- Expose error state so components can display messages -- Reset error state before retry -- Return safe defaults to keep UI functional - -#### Utility Function Errors - -Utility functions should document their error behavior clearly: - -**Option 1: Return default value (no throw)** -```typescript -// src/main/utils/execFile.ts -export async function execFileNoThrow( - command: string, - args: string[], - cwd?: string -): Promise<{ stdout: string; stderr: string; exitCode: number }> { - // Never throws - returns exitCode !== 0 for failures - try { - // ... execution logic - } catch (error) { - return { stdout: '', stderr: String(error), exitCode: 1 }; - } -} -``` - -**Option 2: Throw errors (document clearly)** -```typescript -/** - * Load file tree recursively - * @throws {Error} If directory cannot be read - */ -export async function buildFileTree(dirPath: string): Promise { - // Throws on filesystem errors - caller must handle - const entries = await fs.readdir(dirPath); - // ... -} -``` - -#### Summary of Patterns - -| Layer | Pattern | Example | -|-------|---------|---------| -| **IPC Handlers** | Throw critical failures, try-catch optional ops | `git:isRepo` returns false on error | -| **Services** | Never throw, return safe defaults | `gitService.getStatus()` returns `{ files: [] }` | -| **ProcessManager** | Throw spawn failures, emit runtime events | `emit('exit', sessionId, code)` | -| **React Components** | Try-catch async ops, show UI errors | Display "Failed to load" message | -| **Error Boundaries** | Catch render errors, show fallback UI | Wrap major sections | -| **Custom Hooks** | Internal try-catch, expose error state | Return `{ error, ... }` | -| **Utilities** | Document throw behavior clearly | JSDoc with `@throws` | - -## 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 -- ansi-to-html - ANSI escape code rendering for terminal output -- 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 preferences -- `maestro-sessions.json` - Session persistence (planned) -- `maestro-groups.json` - Session groups (planned) - -### Adding New Persistent Settings - -To add a new setting that persists across sessions: - -1. **Define state variable** in App.tsx: -```typescript -const [mySetting, setMySettingState] = useState(defaultValue); -``` - -2. **Create wrapper function** that persists: -```typescript -const setMySetting = (value: MyType) => { - setMySettingState(value); - window.maestro.settings.set('mySetting', value); -}; -``` - -3. **Load in useEffect**: -```typescript -// Inside the loadSettings useEffect -const savedMySetting = await window.maestro.settings.get('mySetting'); -if (savedMySetting !== undefined) setMySettingState(savedMySetting); -``` - -4. **Pass wrapper to child components**, not the direct setState - -**Current Persistent Settings:** -- `llmProvider`, `modelSlug`, `apiKey` - LLM configuration -- `tunnelProvider`, `tunnelApiKey` - Tunnel configuration -- `defaultAgent` - Default AI agent selection -- `defaultShell` - Default terminal shell (zsh, bash, sh, fish, tcsh) -- `fontFamily`, `fontSize`, `customFonts` - UI font settings -- `enterToSendAI` - Input behavior for AI mode (Enter vs Command+Enter to send, defaults to Command+Enter) -- `enterToSendTerminal` - Input behavior for Terminal mode (Enter vs Command+Enter to send, defaults to Enter) -- `activeThemeId` - Selected theme - -## Common Development Tasks - -### Adding a New UI Feature - -1. **Plan the state** - Determine if it's per-session or global -2. **Add state management** - In App.tsx or component -3. **Create persistence** - Use wrapper function pattern if global -4. **Implement UI** - Follow Tailwind + theme color pattern -5. **Add keyboard shortcuts** - Integrate with existing keyboard handler -6. **Test focus flow** - Ensure Escape key navigation works - -### Adding a New Modal - -1. Create component in `src/renderer/components/` -2. Add state in App.tsx: `const [myModalOpen, setMyModalOpen] = useState(false)` -3. Add Escape handler in keyboard shortcuts -4. Use `fixed inset-0` overlay with `z-[50]` or higher -5. Include close button and backdrop click handler -6. Use `ref={(el) => el?.focus()}` for immediate keyboard control - -### Adding Keyboard Shortcuts - -Maestro has a configurable keyboard shortcut system defined in `src/renderer/constants/shortcuts.ts`. - -**To add a new shortcut:** - -1. Add the shortcut definition to `DEFAULT_SHORTCUTS` in `src/renderer/constants/shortcuts.ts`: -```typescript -myShortcut: { id: 'myShortcut', label: 'My Action', keys: ['Meta', 'k'] }, -``` - -2. Add the handler in App.tsx keyboard event listener (around line 750): -```typescript -else if (isShortcut(e, 'myShortcut')) { - e.preventDefault(); - // Your handler code here -} -``` - -**Supported modifiers:** -- `Meta` - Command (macOS) / Windows key -- `Ctrl` - Control key -- `Alt` - Option (macOS) / Alt key -- `Shift` - Shift key - -**Arrow keys:** -- Use `ArrowLeft`, `ArrowRight`, `ArrowUp`, `ArrowDown` as key names -- Example: `{ keys: ['Alt', 'Meta', 'ArrowLeft'] }` for Opt+Cmd+← - -**Shortcut Customization UI:** -- Users can customize shortcuts in Settings → Shortcuts tab -- Click a shortcut button to record new keys -- Press Escape to cancel recording (won't close the modal) -- ShortcutEditor component handles recording and validation - -### Working with File Tree - -File tree structure is stored per-session as `fileTree` array of nodes: -```typescript -{ +interface Session { + id: string; name: string; - type: 'file' | 'folder'; - path: string; - children?: FileTreeNode[]; + toolType: ToolType; // 'claude-code' | 'aider' | 'terminal' | etc. + state: SessionState; // 'idle' | 'busy' | 'error' | 'connecting' + inputMode: 'ai' | 'terminal'; // Which process receives input + cwd: string; // Working directory + aiPid: number; // AI process ID + terminalPid: number; // Terminal process ID + aiLogs: LogEntry[]; // AI output history + shellLogs: LogEntry[]; // Terminal output history + usageStats?: UsageStats; // Token usage and cost + claudeSessionId?: string; // For conversation continuity + isGitRepo: boolean; // Git features enabled + fileTree: any[]; // File explorer tree + fileExplorerExpanded: string[]; // Expanded folder paths } ``` -Expanded folders tracked in session's `fileExplorerExpanded: string[]` as full paths. +## IPC API Surface -### Modifying Themes +The `window.maestro` API exposes: +- `settings` - Get/set app settings +- `sessions` / `groups` - Persistence +- `process` - Spawn, write, kill, resize +- `git` - Status, diff, isRepo, numstat +- `fs` - readDir, readFile +- `agents` - Detect, get, config +- `claude` - List/read/search Claude Code sessions +- `logger` - System logging +- `dialog` - Folder selection +- `shells` - Detect available shells -1. Find `THEMES` constant in App.tsx -2. Add new theme or modify existing one -3. All color keys must be present (11 required colors) -4. Test in both light and dark mode contexts -5. Theme ID is stored in settings and persists across sessions +## Available Agents -### Adding to Settings Modal +| ID | Name | Notes | +|----|------|-------| +| `claude-code` | Claude Code | Batch mode with `--print` | +| `aider-gemini` | Aider (Gemini) | Uses gemini-2.0-flash-exp | +| `qwen-coder` | Qwen Coder | If installed | +| `terminal` | CLI Terminal | PTY shell session | -1. Add tab if needed in SettingsModal.tsx -2. Create state in App.tsx with wrapper function -3. Add to loadSettings useEffect -4. Pass wrapper function (not setState) to SettingsModal props -5. Add UI in appropriate tab section - -## Important File Locations - -### Main Process Entry Points -- `src/main/index.ts:81-109` - IPC handler setup -- `src/main/index.ts:272-282` - Process event listeners -- `src/main/process-manager.ts:30-116` - Process spawning logic - -### Security-Critical Code -- `src/main/preload.ts:5` - Context bridge API exposure -- `src/main/process-manager.ts:75` - Shell disabled for spawn -- `src/main/utils/execFile.ts` - Safe command execution wrapper - -### Configuration -- `package.json:26-86` - electron-builder config -- `tsconfig.json` - Renderer TypeScript config -- `tsconfig.main.json` - Main process TypeScript config -- `vite.config.mts` - Vite bundler config - -## Debugging & Common Issues +## Debugging ### Focus Not Working - -If keyboard shortcuts aren't working: -1. Check element has `tabIndex={0}` or `tabIndex={-1}` -2. Add `outline-none` class to hide focus ring -3. Use `ref={(el) => el?.focus()}` or `useEffect` to auto-focus -4. Check for `e.stopPropagation()` blocking events +1. Add `tabIndex={0}` or `tabIndex={-1}` +2. Add `outline-none` class +3. Use `ref={(el) => el?.focus()}` for auto-focus ### Settings Not Persisting +1. Check wrapper function calls `window.maestro.settings.set()` +2. Check loading code in `useSettings.ts` useEffect -If settings don't save across sessions: -1. Ensure you created a wrapper function with `window.maestro.settings.set()` -2. Check the wrapper is passed to child components, not the direct setState -3. Verify loading code exists in the `loadSettings` useEffect -4. Use direct state setter (e.g., `setMySettingState`) in the loading code - -### Modal Escape Key Not Working - -If Escape doesn't close a modal: -1. Modal overlay needs `tabIndex={0}` -2. Use `ref={(el) => el?.focus()}` to focus on mount -3. Add `e.stopPropagation()` in onKeyDown handler -4. Check z-index is higher than other modals - -### Theme Colors Not Applying - -If colors appear hardcoded: -1. Replace fixed colors with `style={{ color: theme.colors.textMain }}` -2. Never use hardcoded hex colors for text/borders -3. Use inline styles for theme colors, Tailwind for layout -4. Check theme prop is being passed down correctly - -### Scroll Position Not Saving - -Per-session scroll position: -1. Container needs a ref: `useRef(null)` -2. Add `onScroll` handler that updates session state -3. Add useEffect to restore scroll on session change -4. Use `ref.current.scrollTop` to get/set position - -## Running Tests - -Currently no test suite implemented. When adding tests, use the `test` script in package.json. - +### Modal Escape Not Working +1. Register with layer stack (don't handle Escape locally) +2. Check priority is set correctly diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9b70abf7..3b977cff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,19 +1,20 @@ # Contributing to Maestro -Thank you for your interest in contributing to Maestro! This document provides guidelines, setup instructions, and architectural information for developers. +Thank you for your interest in contributing to Maestro! This document provides guidelines, setup instructions, and practical guidance for developers. + +For architecture details, see [ARCHITECTURE.md](ARCHITECTURE.md). For quick reference while coding, see [CLAUDE.md](CLAUDE.md). ## Table of Contents - [Development Setup](#development-setup) - [Project Structure](#project-structure) -- [Tech Stack](#tech-stack) - [Development Scripts](#development-scripts) -- [Architecture](#architecture) +- [Common Development Tasks](#common-development-tasks) - [Code Style](#code-style) +- [Debugging Guide](#debugging-guide) - [Commit Messages](#commit-messages) - [Pull Request Process](#pull-request-process) - [Building for Release](#building-for-release) -- [GitHub Actions Workflow](#github-actions-workflow) ## Development Setup @@ -43,162 +44,254 @@ npm run dev maestro/ ├── src/ │ ├── main/ # Electron main process (Node.js backend) -│ │ ├── utils/ # Shared utilities -│ │ └── ... # Process management, IPC, web server +│ │ ├── index.ts # Entry point, IPC handlers +│ │ ├── process-manager.ts +│ │ ├── preload.ts # Secure IPC bridge +│ │ └── utils/ # Shared utilities │ └── renderer/ # React frontend (UI) -│ ├── components/ # React components (UI elements, modals, panels) -│ ├── hooks/ # Custom React hooks (reusable state logic) -│ ├── services/ # Business logic services (git, process management) +│ ├── App.tsx # Main coordinator +│ ├── components/ # React components +│ ├── hooks/ # Custom React hooks +│ ├── services/ # IPC wrappers (git, process) +│ ├── contexts/ # React contexts +│ ├── constants/ # Themes, shortcuts, priorities │ ├── types/ # TypeScript definitions -│ ├── utils/ # Frontend utilities -│ └── constants/ # App constants (themes, shortcuts, emojis) +│ └── utils/ # Frontend utilities ├── build/ # Application icons ├── .github/workflows/ # CI/CD automation └── dist/ # Build output (generated) ``` -## Tech Stack - -### Backend (Electron Main Process) - -- **Electron 28+** - Desktop application framework -- **TypeScript** - Type-safe JavaScript -- **node-pty** - Terminal emulation for shell sessions -- **Fastify** - High-performance web server for remote access -- **electron-store** - Persistent settings storage - -### Frontend (Renderer Process) - -- **React 18** - UI framework -- **TypeScript** - Type-safe JavaScript -- **Tailwind CSS** - Utility-first CSS framework -- **Vite** - Fast build tool and dev server -- **Lucide React** - Icon library -- **marked** - Markdown rendering -- **react-syntax-highlighter** - Code syntax highlighting -- **ansi-to-html** - Terminal ANSI escape code rendering -- **dompurify** - HTML sanitization for XSS prevention -- **emoji-mart** - Emoji picker component - ## Development Scripts ```bash -# Start dev server with hot reload -npm run dev - -# Build main process only (Electron backend) -npm run build:main - -# Build renderer only (React frontend) -npm run build:renderer - -# Full production build -npm run build - -# Start built application -npm start - -# Clean build artifacts and cache -npm run clean +npm run dev # Start dev server with hot reload +npm run build # Full production build +npm run build:main # Build main process only +npm run build:renderer # Build renderer only +npm start # Start built application +npm run clean # Clean build artifacts +npm run package # Package for all platforms +npm run package:mac # Package for macOS +npm run package:win # Package for Windows +npm run package:linux # Package for Linux ``` -## Architecture +## Common Development Tasks -### Process Management +### Adding a New UI Feature -Maestro uses a dual-process architecture where **each session runs two processes simultaneously**: +1. **Plan the state** - Determine if it's per-session or global +2. **Add state management** - In `useSettings.ts` (global) or session state +3. **Create persistence** - Use wrapper function pattern for global settings +4. **Implement UI** - Follow Tailwind + theme color pattern +5. **Add keyboard shortcuts** - In `shortcuts.ts` and `App.tsx` +6. **Test focus flow** - Ensure Escape key navigation works -1. **AI Agent Process** - Runs Claude Code as a child process -2. **Terminal Process** - Runs a PTY shell session for command execution +### Adding a New Modal -This architecture enables seamless switching between AI and terminal modes without process restarts. All processes are managed through IPC (Inter-Process Communication) with secure context isolation. +1. Create component in `src/renderer/components/` +2. Add priority in `src/renderer/constants/modalPriorities.ts`: + ```typescript + MY_MODAL: 600, + ``` +3. Register with layer stack (see [ARCHITECTURE.md](ARCHITECTURE.md#layer-stack-system)) +4. Use proper ARIA attributes: + ```typescript +
+ ``` -### Security Model +### Adding Keyboard Shortcuts -Maestro implements strict security measures: +1. Add definition in `src/renderer/constants/shortcuts.ts`: + ```typescript + myShortcut: { id: 'myShortcut', label: 'My Action', keys: ['Meta', 'k'] }, + ``` -- **Context isolation enabled** - Renderer has no direct Node.js access -- **No node integration in renderer** - No `require()` in renderer process -- **Secure IPC via preload script** - Minimal API exposed via `contextBridge` -- **No shell injection** - Uses `execFile` instead of `exec` -- **Input sanitization** - All user inputs are validated +2. Add handler in `App.tsx` keyboard event listener: + ```typescript + else if (isShortcut(e, 'myShortcut')) { + e.preventDefault(); + // Handler code + } + ``` -### Main Process (Backend) +**Supported modifiers:** `Meta` (Cmd/Win), `Ctrl`, `Alt`, `Shift` +**Arrow keys:** `ArrowLeft`, `ArrowRight`, `ArrowUp`, `ArrowDown` -Located in `src/main/`: +### Adding a New Setting -- `index.ts` - Application entry point, IPC handler registration, window management -- `process-manager.ts` - Core primitive for spawning and managing CLI processes -- `web-server.ts` - Fastify-based HTTP/WebSocket server for remote access -- `agent-detector.ts` - Auto-detects available AI tools via PATH -- `preload.ts` - Secure IPC bridge via contextBridge +1. Add state in `useSettings.ts`: + ```typescript + const [mySetting, setMySettingState] = useState(defaultValue); + ``` -### Renderer Process (Frontend) +2. Create wrapper function: + ```typescript + const setMySetting = (value) => { + setMySettingState(value); + window.maestro.settings.set('mySetting', value); + }; + ``` -Located in `src/renderer/`: +3. Load in useEffect: + ```typescript + const saved = await window.maestro.settings.get('mySetting'); + if (saved !== undefined) setMySettingState(saved); + ``` -- `App.tsx` - Main UI coordinator -- `main.tsx` - Renderer entry point -- `components/` - React components (modals, panels, UI elements) -- `hooks/` - Custom React hooks for reusable state logic -- `services/` - Business logic services (clean wrappers around IPC calls) -- `constants/` - Application constants (themes, shortcuts, etc.) +4. Add to return object and export. + +### Adding a Slash Command + +Add to `src/renderer/slashCommands.ts`: + +```typescript +{ + command: '/mycommand', + description: 'Does something useful', + terminalOnly: false, // Optional: restrict to terminal mode + execute: (context) => { + const { activeSessionId, setSessions } = context; + // Your logic + } +} +``` + +### Adding a New Theme + +Add to `src/renderer/constants/themes.ts`: + +```typescript +'my-theme': { + id: 'my-theme', + name: 'My Theme', + mode: 'dark', // or 'light' + colors: { + bgMain: '#...', + bgSidebar: '#...', + bgActivity: '#...', + border: '#...', + textMain: '#...', + textDim: '#...', + accent: '#...', + accentDim: 'rgba(...)', + accentText: '#...', + success: '#...', + warning: '#...', + error: '#...', + } +} +``` + +Then add the ID to `ThemeId` type in `src/renderer/types/index.ts`. + +### Adding an IPC Handler + +1. Add handler in `src/main/index.ts`: + ```typescript + ipcMain.handle('myNamespace:myAction', async (_, arg1, arg2) => { + // Implementation + return result; + }); + ``` + +2. Expose in `src/main/preload.ts`: + ```typescript + myNamespace: { + myAction: (arg1, arg2) => ipcRenderer.invoke('myNamespace:myAction', arg1, arg2), + }, + ``` + +3. Add types to `MaestroAPI` interface in preload.ts. ## Code Style ### TypeScript -- All code must be TypeScript with strict mode enabled -- Define interfaces for all data structures -- Export types via `preload.ts` for renderer types +- Strict mode enabled +- Interface definitions for all data structures +- Export types via `preload.ts` for renderer ### React Components -- Use functional components with hooks -- Keep components small and focused -- Use Tailwind CSS for styling +- Functional components with hooks +- Keep components focused and small +- Use Tailwind for layout, inline styles for theme colors - Maintain keyboard accessibility -- Use inline styles for theme colors, Tailwind for layout +- Use `tabIndex={-1}` + `outline-none` for programmatic focus -### Architecture Guidelines +### Security -**Main Process:** -- Keep IPC handlers simple and focused -- Use TypeScript interfaces for all data structures -- Handle errors gracefully -- No blocking operations - -**Renderer Process:** -- Use React hooks -- Keep components small and focused -- Use Tailwind for styling -- Maintain keyboard accessibility - -**Security:** -- Never expose Node.js APIs to renderer +- **Always use `execFileNoThrow`** for external commands (never shell-based execution) +- Keep context isolation enabled - Use preload script for all IPC - Sanitize all user inputs -- Use `execFile` instead of `exec` +- Use `spawn()` with `shell: false` + +## Debugging Guide + +### Focus Not Working + +1. Add `tabIndex={0}` or `tabIndex={-1}` to element +2. Add `outline-none` class to hide focus ring +3. Use `ref={(el) => el?.focus()}` for auto-focus +4. Check for `e.stopPropagation()` blocking events + +### Settings Not Persisting + +1. Ensure wrapper function calls `window.maestro.settings.set()` +2. Check loading code in `useSettings.ts` useEffect +3. Verify the key name matches in both save and load + +### Modal Escape Not Working + +1. Register modal with layer stack (don't handle Escape locally) +2. Check priority in `modalPriorities.ts` +3. Use ref pattern to avoid re-registration: + ```typescript + const onCloseRef = useRef(onClose); + onCloseRef.current = onClose; + ``` + +### Theme Colors Not Applying + +1. Use `style={{ color: theme.colors.textMain }}` instead of Tailwind color classes +2. Check theme prop is passed to component +3. Never use hardcoded hex colors for themed elements + +### Process Output Not Showing + +1. Check session ID matches (with `-ai` or `-terminal` suffix) +2. Verify `onData` listener is registered +3. Check process spawned successfully (check pid > 0) +4. Look for errors in DevTools console + +### DevTools + +Open via Quick Actions (`Cmd+K` → "Toggle DevTools") or set `DEBUG=true` env var. ## Commit Messages Use conventional commits: -- `feat:` - New features -- `fix:` - Bug fixes -- `docs:` - Documentation changes -- `refactor:` - Code refactoring -- `test:` - Test additions/changes -- `chore:` - Build process or tooling changes +``` +feat: new feature +fix: bug fix +docs: documentation changes +refactor: code refactoring +test: test additions/changes +chore: build process or tooling changes +``` Example: `feat: add context usage visualization` ## Pull Request Process 1. Create a feature branch from `main` -2. Make your changes -3. Add tests if applicable -4. Update documentation +2. Make your changes following the code style +3. Test thoroughly (keyboard navigation, themes, focus) +4. Update documentation if needed 5. Submit PR with clear description 6. Wait for review @@ -206,16 +299,14 @@ Example: `feat: add context usage visualization` ### 1. Prepare Icons -Place your application icons in the `build/` directory: - +Place icons in `build/` directory: - `icon.icns` - macOS (512x512 or 1024x1024) - `icon.ico` - Windows (256x256) - `icon.png` - Linux (512x512) ### 2. Update Version -Update version in `package.json`: - +Update in `package.json`: ```json { "version": "0.1.0" @@ -225,42 +316,25 @@ Update version in `package.json`: ### 3. Build Distributables ```bash -# Build for all platforms -npm run package - -# Platform-specific builds -npm run package:mac # Creates .dmg and .zip -npm run package:win # Creates .exe installer -npm run package:linux # Creates .AppImage, .deb, .rpm +npm run package # All platforms +npm run package:mac # macOS (.dmg, .zip) +npm run package:win # Windows (.exe) +npm run package:linux # Linux (.AppImage, .deb, .rpm) ``` -Output will be in the `release/` directory. +Output in `release/` directory. -## GitHub Actions Workflow +### GitHub Actions -The project includes automated builds via GitHub Actions: - -1. **Create a release tag:** - ```bash - git tag v0.1.0 - git push origin v0.1.0 - ``` - -2. **GitHub Actions will automatically:** - - Build for macOS, Windows, and Linux - - Create release artifacts - - Publish a GitHub Release with downloads - -## Testing +Create a release tag to trigger automated builds: ```bash -# Run tests (when available) -npm test - -# Type checking -npm run build:main +git tag v0.1.0 +git push origin v0.1.0 ``` +GitHub Actions will build for all platforms and create a release. + ## Questions? -Open a GitHub Discussion or reach out in Issues. +Open a GitHub Discussion or create an Issue.