# Architecture Guide Deep technical documentation for Maestro's architecture and design patterns. For quick reference, see [CLAUDE.md](CLAUDE.md). For development setup, see [CONTRIBUTING.md](CONTRIBUTING.md). ## Table of Contents - [Dual-Process Architecture](#dual-process-architecture) - [IPC Security Model](#ipc-security-model) - [Process Manager](#process-manager) - [Layer Stack System](#layer-stack-system) - [Custom Hooks](#custom-hooks) - [Services Layer](#services-layer) - [Custom AI Commands](#custom-ai-commands) - [Theme System](#theme-system) - [Settings Persistence](#settings-persistence) - [Claude Sessions API](#claude-sessions-api) - [Auto Run System](#auto-run-system) - [Achievement System](#achievement-system) - [AI Tab System](#ai-tab-system) - [File Preview Tab System](#file-preview-tab-system) - [Execution Queue](#execution-queue) - [Navigation History](#navigation-history) - [Group Chat System](#group-chat-system) - [Web/Mobile Interface](#webmobile-interface) - [CLI Tool](#cli-tool) - [Shared Module](#shared-module) - [Remote Access & Tunnels](#remote-access--tunnels) - [Error Handling Patterns](#error-handling-patterns) --- ## Architecture Maestro organizes work into **Projects** (workspaces), each with a **CLI Terminal** and multiple **Agent Tabs**. Each tab can be connected to an **Agent Session** - either newly created or resumed from the session pool. ```mermaid graph LR subgraph Maestro["Maestro App"] subgraph ProjectA["Agent A (workspace)"] TermA[CLI Terminal] subgraph TabsA["Agent Tabs"] Tab1A[Tab 1] Tab2A[Tab 2] end end subgraph ProjectB["Agent B (workspace)"] TermB[CLI Terminal] subgraph TabsB["Agent Tabs"] Tab1B[Tab 1] Tab2B[Tab 2] end end end subgraph SessionPool["Agent Session Pool"] direction TB S1["Session α"] S2["Session β"] S3["Session γ"] S4["Session δ"] S5["..."] end Tab1A -.->|"resume"| S1 Tab2A -.->|"resume"| S2 Tab1B -.->|"resume"| S3 Tab2B -.->|"new"| S4 style Maestro fill:#9b8cd6,stroke:#6b5b95 style ProjectA fill:#87ceeb,stroke:#4682b4 style ProjectB fill:#87ceeb,stroke:#4682b4 style TermA fill:#90ee90,stroke:#228b22 style TermB fill:#90ee90,stroke:#228b22 style TabsA fill:#ffe4a0,stroke:#daa520 style TabsB fill:#ffe4a0,stroke:#daa520 style SessionPool fill:#ffb6c1,stroke:#dc143c ``` ## Dual-Process Architecture Maestro uses Electron's main/renderer split with strict context isolation. ### Main Process (`src/main/`) Node.js backend with full system access: | File | Purpose | |------|---------| | `index.ts` | App entry, IPC handlers, window management | | `process-manager.ts` | PTY and child process spawning | | `web-server.ts` | Fastify HTTP/WebSocket server for mobile remote control | | `agent-detector.ts` | Auto-detect CLI tools via PATH | | `preload.ts` | Secure IPC bridge via contextBridge | | `tunnel-manager.ts` | Cloudflare tunnel management for secure remote access | | `themes.ts` | Theme definitions for web interface (mirrors renderer themes) | | `utils/execFile.ts` | Safe command execution utility | | `utils/logger.ts` | System logging with levels | | `utils/shellDetector.ts` | Detect available shells | | `utils/terminalFilter.ts` | Strip terminal control sequences | | `utils/cliDetection.ts` | CLI tool detection (cloudflared, gh) | | `utils/networkUtils.ts` | Network utilities for local IP detection | ### Renderer Process (`src/renderer/`) React frontend with no direct Node.js access: | Directory | Purpose | |-----------|---------| | `components/` | React UI components | | `hooks/` | Custom React hooks (15 hooks - see [Custom Hooks](#custom-hooks)) | | `services/` | IPC wrappers (git.ts, process.ts) | | `contexts/` | React contexts (LayerStackContext, ToastContext) | | `constants/` | Themes, shortcuts, modal priorities | | `types/` | TypeScript definitions | | `utils/` | Frontend utilities | ### Session Model Each session runs **two processes simultaneously**: ```typescript interface Session { id: string; // Unique identifier aiPid: number; // AI agent process (suffixed -ai) terminalPid: number; // Terminal process (suffixed -terminal) inputMode: 'ai' | 'terminal'; // Which process receives input // ... other fields } ``` This enables seamless switching between AI and terminal modes without process restarts. --- ## IPC Security Model All renderer-to-main communication uses the preload script: - **Context isolation**: Enabled (renderer has no Node.js access) - **Node integration**: Disabled (no `require()` in renderer) - **Preload script**: Exposes minimal API via `contextBridge.exposeInMainWorld('maestro', ...)` ### The `window.maestro` API ```typescript window.maestro = { // Core persistence settings: { get, set, getAll }, sessions: { getAll, setAll }, groups: { getAll, setAll }, history: { getAll, setAll }, // Command history persistence // Process management process: { spawn, write, interrupt, kill, resize, runCommand, onData, onExit, onSessionId, onStderr, onCommandExit, onUsage }, // Git operations (expanded) git: { status, diff, isRepo, numstat, branches, tags, branch, log, show, showFile, // Worktree operations worktreeInfo, worktreeSetup, worktreeCheckout, getRepoRoot, // PR creation createPR, getDefaultBranch, checkGhCli }, // File system fs: { readDir, readFile }, // Agent management agents: { detect, get, getConfig, setConfig, getConfigValue, setConfigValue }, // Claude Code integration claude: { listSessions, readSessionMessages, searchSessions, getGlobalStats, onGlobalStatsUpdate }, // UI utilities dialog: { selectFolder }, fonts: { detect }, shells: { detect }, shell: { openExternal }, devtools: { open, close, toggle }, // Logging logger: { log, getLogs, clearLogs, setLogLevel, getLogLevel, setMaxLogBuffer, getMaxLogBuffer }, // Web/remote interface webserver: { getUrl, getClientCount }, web: { broadcastUserInput, broadcastAutoRunState, broadcastTabChange }, live: { setSessionLive, getSessionLive }, tunnel: { start, stop, getStatus, onStatusChange }, // Auto Run autorun: { listDocs, readDoc, writeDoc, saveImage, deleteImage, listImages }, playbooks: { list, create, update, delete }, // Attachments & temp files attachments: { save, list, delete, clear }, tempfile: { write, read, delete }, // Activity & notifications cli: { trackActivity, getActivity }, notification: { show, speak }, } ``` --- ## Process Manager The `ProcessManager` class (`src/main/process-manager.ts`) handles two process types: ### PTY Processes (via `node-pty`) Used for terminal sessions with full shell emulation: - `toolType: 'terminal'` - Supports resize, ANSI escape codes, interactive shell - Spawned with shell (zsh, bash, fish, etc.) ### Child Processes (via `child_process.spawn`) Used for AI assistants: - All non-terminal tool types - Direct stdin/stdout/stderr capture - **Security**: Uses `spawn()` with `shell: false` ### Batch Mode (Claude Code) Claude Code runs in batch mode with `--print --output-format json`: - Prompt passed as CLI argument - Process exits after response - JSON response parsed for result and usage stats ### Stream-JSON Mode (with images) When images are attached: - Uses `--input-format stream-json --output-format stream-json` - Message sent via stdin as JSONL - Supports multimodal input ### Process Events ```typescript processManager.on('data', (sessionId, data) => { ... }); processManager.on('exit', (sessionId, code) => { ... }); processManager.on('usage', (sessionId, usageStats) => { ... }); processManager.on('session-id', (sessionId, agentSessionId) => { ... }); processManager.on('stderr', (sessionId, data) => { ... }); processManager.on('command-exit', (sessionId, code) => { ... }); ``` --- ## Layer Stack System Centralized modal/overlay management with predictable Escape key handling. ### Problem Solved - Previously had 9+ scattered Escape handlers - Brittle modal detection with massive boolean checks - Inconsistent focus management ### Architecture | File | Purpose | |------|---------| | `hooks/useLayerStack.ts` | Core layer management hook | | `contexts/LayerStackContext.tsx` | Global Escape handler (capture phase) | | `constants/modalPriorities.ts` | Priority values for all modals | | `types/layer.ts` | Layer type definitions | ### Modal Priority Hierarchy ```typescript const MODAL_PRIORITIES = { STANDING_OVATION: 1100, // Achievement celebration overlay CONFIRM: 1000, // Highest - confirmation dialogs PLAYBOOK_DELETE_CONFIRM: 950, PLAYBOOK_NAME: 940, RENAME_INSTANCE: 900, RENAME_TAB: 880, RENAME_GROUP: 850, CREATE_GROUP: 800, NEW_INSTANCE: 750, AGENT_PROMPT_COMPOSER: 730, PROMPT_COMPOSER: 710, QUICK_ACTION: 700, // Command palette (Cmd+K) TAB_SWITCHER: 690, AGENT_SESSIONS: 680, EXECUTION_QUEUE_BROWSER: 670, BATCH_RUNNER: 660, SHORTCUTS_HELP: 650, HISTORY_HELP: 640, AUTORUNNER_HELP: 630, HISTORY_DETAIL: 620, ABOUT: 600, PROCESS_MONITOR: 550, LOG_VIEWER: 500, SETTINGS: 450, GIT_DIFF: 200, GIT_LOG: 190, LIGHTBOX: 150, FILE_PREVIEW: 100, SLASH_AUTOCOMPLETE: 50, TEMPLATE_AUTOCOMPLETE: 40, FILE_TREE_FILTER: 30, // Lowest }; ``` ### Registering a Modal ```typescript import { useLayerStack } from '../contexts/LayerStackContext'; import { MODAL_PRIORITIES } from '../constants/modalPriorities'; const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack(); const layerIdRef = useRef(); // Use ref to avoid re-registration when callback identity changes const onCloseRef = useRef(onClose); onCloseRef.current = onClose; useEffect(() => { if (modalOpen) { const id = registerLayer({ type: 'modal', priority: MODAL_PRIORITIES.YOUR_MODAL, blocksLowerLayers: true, capturesFocus: true, focusTrap: 'strict', // 'strict' | 'lenient' | 'none' ariaLabel: 'Your Modal Name', onEscape: () => onCloseRef.current(), }); layerIdRef.current = id; return () => unregisterLayer(id); } }, [modalOpen, registerLayer, unregisterLayer]); // onClose NOT in deps ``` ### Layer Types ```typescript type ModalLayer = { type: 'modal'; priority: number; blocksLowerLayers: boolean; capturesFocus: boolean; focusTrap: 'strict' | 'lenient' | 'none'; ariaLabel?: string; onEscape: () => void; onBeforeClose?: () => Promise; isDirty?: boolean; parentModalId?: string; }; 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 handle internal search in their onEscape: ```typescript onEscape: () => { if (searchOpen) { setSearchOpen(false); // First Escape closes search } else { closePreview(); // Second Escape closes preview } } ``` --- ## Custom Hooks Maestro uses 15 custom hooks for state management and functionality. ### Core Hooks #### useSettings (`src/renderer/hooks/useSettings.ts`) Manages all application settings with automatic persistence. **What it manages:** - LLM settings (provider, model, API key) - Agent settings (default agent, custom agent paths) - Shell settings (default shell) - Font settings (family, size, custom fonts) - UI settings (theme, enter-to-send modes, panel widths, markdown mode) - Terminal settings (width) - Logging settings (level, buffer size) - Output settings (max lines) - Keyboard shortcuts - Custom AI commands #### useSessionManager (`src/renderer/hooks/useSessionManager.ts`) Manages sessions and groups with CRUD operations. **Key methods:** - `createNewSession(agentId, workingDir, name)` - Creates new session with dual processes - `deleteSession(id, showConfirmation)` - Delete with confirmation - `toggleInputMode()` - Switch between AI and terminal mode - `updateScratchPad(content)` - Update session scratchpad - `createNewGroup(name, emoji, moveSession, activeSessionId)` - Drag and drop handlers #### useFileTreeManagement (`src/renderer/hooks/useFileTreeManagement.ts`) Manages file tree refresh/filter state and git-related file metadata. **Key methods:** - `refreshFileTree(sessionId)` - Reload directory tree and return change stats - `refreshGitFileState(sessionId)` - Refresh tree + git repo metadata - `filteredFileTree` - Derived tree based on filter string #### useBatchProcessor (`src/renderer/hooks/useBatchProcessor.ts`) Manages Auto Run batch execution logic. **Key methods:** - `startBatchRun(config)` - Start batch document processing - `stopBatchRun()` - Stop current batch run - `pauseBatchRun()` / `resumeBatchRun()` - Pause/resume execution ### UI Management Hooks #### useLayerStack (`src/renderer/hooks/useLayerStack.ts`) Core layer management for modals and overlays. **Key methods:** - `registerLayer(config)` - Register a modal/overlay - `unregisterLayer(id)` - Remove a layer - `updateLayerHandler(id, handler)` - Update escape handler #### useNavigationHistory (`src/renderer/hooks/useNavigationHistory.ts`) Back/forward navigation through sessions and tabs. See [Navigation History](#navigation-history). ### Input & Autocomplete Hooks #### useAtMentionCompletion (`src/renderer/hooks/useAtMentionCompletion.ts`) Handles @-mention autocomplete for file references in prompts. #### useTabCompletion (`src/renderer/hooks/useTabCompletion.ts`) Tab completion utilities for terminal-style input. #### useTemplateAutocomplete (`src/renderer/hooks/useTemplateAutocomplete.ts`) Template variable autocomplete (e.g., `{{date}}`, `{{time}}`). ### Feature Hooks #### useAchievements (`src/renderer/hooks/useAchievements.ts`) Achievement/badge system for Auto Run usage. See [Achievement System](#achievement-system). #### useActivityTracker (`src/renderer/hooks/useActivityTracker.ts`) User activity tracking for session idle detection and status. #### useMobileLandscape (`src/renderer/hooks/useMobileLandscape.ts`) Mobile landscape orientation detection for responsive layouts. --- ## Services Layer Services provide clean wrappers around IPC calls. ### Git Service (`src/renderer/services/git.ts`) ```typescript import { gitService } from '../services/git'; const isRepo = await gitService.isRepo(cwd); const status = await gitService.getStatus(cwd); // Returns: { files: [{ path: string, status: string }] } const diff = await gitService.getDiff(cwd, ['file1.ts']); // Returns: { diff: string } const numstat = await gitService.getNumstat(cwd); // Returns: { files: [{ path, additions, deletions }] } ``` ### Process Service (`src/renderer/services/process.ts`) ```typescript import { processService } from '../services/process'; await processService.spawn(config); await processService.write(sessionId, 'input\n'); await processService.interrupt(sessionId); // SIGINT/Ctrl+C await processService.kill(sessionId); await processService.resize(sessionId, cols, rows); const unsubscribe = processService.onData((sessionId, data) => { ... }); ``` --- ## Custom AI Commands User-defined prompt macros that expand when typed. The built-in slash commands (`/clear`, `/jump`, `/history`) have been deprecated in favor of fully customizable commands defined in Settings. ### Overview Custom AI Commands are prompt templates that: - Start with `/` prefix - Expand to full prompts when selected - Support template variables (e.g., `{{date}}`, `{{time}}`, `{{cwd}}`) - Can be AI-only, terminal-only, or both modes ### Configuration Commands are defined in Settings → Custom AI Commands: ```typescript interface CustomAICommand { command: string; // e.g., "/review" description: string; // Shown in autocomplete prompt: string; // The expanded prompt text aiOnly?: boolean; // Only show in AI mode terminalOnly?: boolean; // Only show in terminal mode } ``` ### Template Variables Commands support these template variables: - `{{date}}` - Current date (YYYY-MM-DD) - `{{time}}` - Current time (HH:MM:SS) - `{{datetime}}` - Combined date and time - `{{cwd}}` - Current working directory - `{{session}}` - Session name - `{{agent}}` - Agent type (claude-code, etc.) ### Example Commands ```typescript // Code review command { command: '/review', description: 'Review staged changes', prompt: 'Review the staged git changes and provide feedback on code quality, potential bugs, and improvements.', aiOnly: true } // Status check { command: '/status', description: 'Project status summary', prompt: 'Give me a brief status of this project as of {{datetime}}. What files have been modified recently?', aiOnly: true } ``` ### Claude Code CLI Commands Maestro also fetches slash commands from Claude Code CLI when available, making Claude Code's built-in commands accessible through the same autocomplete interface. --- ## Theme System Themes defined in `src/renderer/constants/themes.ts`. ### Theme Structure ```typescript interface Theme { id: ThemeId; name: string; mode: 'light' | 'dark' | 'vibe'; 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 accentForeground: string; // Text ON accent backgrounds (contrast) success: string; // Success state (green) warning: string; // Warning state (yellow) error: string; // Error state (red) }; } ``` ### Available Themes **Dark themes:** Dracula, Monokai, Nord, Tokyo Night, Catppuccin Mocha, Gruvbox Dark **Light themes:** GitHub, Solarized, One Light, Gruvbox Light, Catppuccin Latte, Ayu Light ### Usage Use inline styles for theme colors: ```typescript style={{ color: theme.colors.textMain }} // Correct ``` Use Tailwind for layout: ```typescript className="flex items-center gap-2" // Correct ``` --- ## Settings Persistence Settings stored via `electron-store`: **Locations:** - **macOS**: `~/Library/Application Support/maestro/` - **Windows**: `%APPDATA%/maestro/` - **Linux**: `~/.config/maestro/` **Files:** - `maestro-settings.json` - User preferences - `maestro-sessions.json` - Session persistence - `maestro-groups.json` - Session groups - `maestro-agent-configs.json` - Per-agent configuration ### Adding New Settings 1. Add state in `useSettings.ts`: ```typescript const [mySetting, setMySettingState] = useState(defaultValue); ``` 2. Create wrapper function: ```typescript const setMySetting = (value: MyType) => { setMySettingState(value); window.maestro.settings.set('mySetting', value); }; ``` 3. Load in useEffect: ```typescript const saved = await window.maestro.settings.get('mySetting'); if (saved !== undefined) setMySettingState(saved); ``` 4. Add to return object and export. --- ## Claude Sessions API Browse and resume Claude Code sessions from `~/.claude/projects/`. ### Path Encoding Claude Code encodes project paths by replacing `/` with `-`: - `/Users/pedram/Projects/Maestro` → `-Users-pedram-Projects-Maestro` ### IPC Handlers ```typescript // List sessions for a project const sessions = await window.maestro.claude.listSessions(projectPath); // Returns: [{ sessionId, projectPath, timestamp, modifiedAt, firstMessage, messageCount, sizeBytes }] // Read messages with pagination const { messages, total, hasMore } = await window.maestro.claude.readSessionMessages( projectPath, sessionId, { offset: 0, limit: 20 } ); // Search sessions const results = await window.maestro.claude.searchSessions( projectPath, 'query', 'all' // 'title' | 'user' | 'assistant' | 'all' ); // Get global stats across all Claude projects (with streaming updates) const stats = await window.maestro.claude.getGlobalStats(); // Returns: { totalSessions, totalMessages, totalInputTokens, totalOutputTokens, // totalCacheReadTokens, totalCacheCreationTokens, totalCostUsd, totalSizeBytes } // Subscribe to streaming updates during stats calculation const unsubscribe = window.maestro.claude.onGlobalStatsUpdate((stats) => { console.log(`Progress: ${stats.totalSessions} sessions, $${stats.totalCostUsd.toFixed(2)}`); if (stats.isComplete) console.log('Stats calculation complete'); }); // Call unsubscribe() to stop listening ``` ### UI Access - Shortcut: `Cmd+Shift+L` - Quick Actions: `Cmd+K` → "View Agent Sessions" - Button in main panel header --- ## Auto Run System File-based document runner for automating multi-step tasks. Users configure a folder of markdown documents containing checkbox tasks that are processed sequentially by AI agents. ### Component Architecture | Component | Purpose | |-----------|---------| | `AutoRun.tsx` | Main panel showing current document with edit/preview modes | | `AutoRunSetupModal.tsx` | First-time setup for selecting the Runner Docs folder | | `AutoRunDocumentSelector.tsx` | Dropdown for switching between markdown documents | | `BatchRunnerModal.tsx` | Configuration modal for running multiple Auto Run documents | | `PlaybookNameModal.tsx` | Modal for naming saved playbook configurations | | `PlaybookDeleteConfirmModal.tsx` | Confirmation modal for playbook deletion | | `useBatchProcessor.ts` | Hook managing batch execution logic | ### Data Types ```typescript // Document entry in the batch run queue (supports duplicates) interface BatchDocumentEntry { id: string; // Unique ID for drag-drop and duplicates filename: string; // Document filename (without .md) resetOnCompletion: boolean; // Uncheck all boxes when done isDuplicate: boolean; // True if this is a duplicate entry } // Git worktree configuration for parallel work interface WorktreeConfig { enabled: boolean; // Whether to use a worktree path: string; // Absolute path for the worktree branchName: string; // Branch name to use/create createPROnCompletion: boolean; // Create PR when Auto Run finishes } // Configuration for starting a batch run interface BatchRunConfig { documents: BatchDocumentEntry[]; // Ordered list of docs to run prompt: string; // Agent prompt template loopEnabled: boolean; // Loop back to first doc when done worktree?: WorktreeConfig; // Optional worktree configuration } // Runtime batch processing state interface BatchRunState { isRunning: boolean; isStopping: boolean; documents: string[]; // Document filenames in order currentDocumentIndex: number; // Which document we're on (0-based) currentDocTasksTotal: number; currentDocTasksCompleted: number; totalTasksAcrossAllDocs: number; completedTasksAcrossAllDocs: number; loopEnabled: boolean; loopIteration: number; // How many times we've looped folderPath: string; worktreeActive: boolean; worktreePath?: string; worktreeBranch?: string; } // Saved playbook configuration interface Playbook { id: string; name: string; createdAt: number; updatedAt: number; documents: PlaybookDocumentEntry[]; loopEnabled: boolean; prompt: string; worktreeSettings?: { branchNameTemplate: string; createPROnCompletion: boolean; }; } ``` ### Session Fields Auto Run state is stored per-session: ```typescript // In Session interface autoRunFolderPath?: string; // Persisted folder path for Runner Docs autoRunSelectedFile?: string; // Currently selected markdown filename autoRunMode?: 'edit' | 'preview'; // Current editing mode autoRunEditScrollPos?: number; // Scroll position in edit mode autoRunPreviewScrollPos?: number; // Scroll position in preview mode autoRunCursorPosition?: number; // Cursor position in edit mode batchRunnerPrompt?: string; // Custom batch runner prompt batchRunnerPromptModifiedAt?: number; ``` ### IPC Handlers ```typescript // List markdown files in a directory 'autorun:listDocs': (folderPath: string) => Promise<{ success, files, error? }> // Read a markdown document 'autorun:readDoc': (folderPath: string, filename: string) => Promise<{ success, content, error? }> // Write a markdown document 'autorun:writeDoc': (folderPath: string, filename: string, content: string) => Promise<{ success, error? }> // Save image to folder 'autorun:saveImage': (folderPath: string, docName: string, base64Data: string, extension: string) => Promise<{ success, relativePath, error? }> // Delete image 'autorun:deleteImage': (folderPath: string, relativePath: string) => Promise<{ success, error? }> // List images for a document 'autorun:listImages': (folderPath: string, docName: string) => Promise<{ success, images, error? }> // Playbook CRUD operations 'playbooks:list': (sessionId: string) => Promise<{ success, playbooks, error? }> 'playbooks:create': (sessionId: string, playbook) => Promise<{ success, playbook, error? }> 'playbooks:update': (sessionId: string, playbookId: string, updates) => Promise<{ success, playbook, error? }> 'playbooks:delete': (sessionId: string, playbookId: string) => Promise<{ success, error? }> ``` ### Git Worktree Integration When worktree is enabled, Auto Run operates in an isolated directory: ```typescript // Check if worktree exists and get branch info 'git:worktreeInfo': (worktreePath: string) => Promise<{ success: boolean; exists: boolean; isWorktree: boolean; currentBranch?: string; repoRoot?: string; }> // Create or reuse a worktree 'git:worktreeSetup': (mainRepoCwd: string, worktreePath: string, branchName: string) => Promise<{ success: boolean; created: boolean; currentBranch: string; branchMismatch: boolean; }> // Checkout a branch in a worktree 'git:worktreeCheckout': (worktreePath: string, branchName: string, createIfMissing: boolean) => Promise<{ success: boolean; hasUncommittedChanges: boolean; }> // Create PR from worktree branch 'git:createPR': (worktreePath: string, baseBranch: string, title: string, body: string) => Promise<{ success: boolean; prUrl?: string; }> ``` ### Execution Flow 1. **Setup**: User selects Runner Docs folder via `AutoRunSetupModal` 2. **Document Selection**: Documents appear in `AutoRunDocumentSelector` dropdown 3. **Editing**: `AutoRun` component provides edit/preview modes with auto-save (5s debounce) 4. **Batch Configuration**: `BatchRunnerModal` allows ordering documents, enabling loop/reset, configuring worktree 5. **Playbooks**: Save/load configurations for repeated batch runs 6. **Execution**: `useBatchProcessor` hook processes documents sequentially 7. **Progress**: RightPanel shows document and task-level progress ### Write Queue Integration Without worktree mode, Auto Run tasks queue through the existing execution queue: - Auto Run tasks are marked as write operations (`readOnlyMode: false`) - Manual write messages queue behind Auto Run (sequential) - Read-only operations from other tabs can run in parallel With worktree mode: - Auto Run operates in a separate directory - No queue conflicts with main workspace - True parallelization enabled --- ## Achievement System Gamification system that rewards Auto Run usage with conductor-themed badges. ### Components | Component | Purpose | |-----------|---------| | `conductorBadges.ts` | Badge definitions and progression levels | | `useAchievements.ts` | Achievement tracking and unlock logic | | `AchievementCard.tsx` | Individual badge display component | | `StandingOvationOverlay.tsx` | Celebration overlay with confetti animations | ### Badge Progression 15 conductor levels based on cumulative Auto Run time: | Level | Badge | Time Required | |-------|-------|---------------| | 1 | Apprentice Conductor | 1 minute | | 2 | Junior Conductor | 5 minutes | | 3 | Assistant Conductor | 15 minutes | | 4 | Associate Conductor | 30 minutes | | 5 | Conductor | 1 hour | | 6 | Senior Conductor | 2 hours | | 7 | Principal Conductor | 4 hours | | 8 | Master Conductor | 8 hours | | 9 | Chief Conductor | 16 hours | | 10 | Distinguished Conductor | 24 hours | | 11 | Elite Conductor | 48 hours | | 12 | Virtuoso Conductor | 72 hours | | 13 | Legendary Conductor | 100 hours | | 14 | Mythic Conductor | 150 hours | | 15 | Transcendent Maestro | 200 hours | ### Standing Ovation When a new badge is unlocked: 1. `StandingOvationOverlay` displays with confetti animation 2. Badge details shown with celebration message 3. Share functionality available 4. Acknowledgment persisted to prevent re-showing --- ## AI Tab System Multi-tab support within each session, allowing parallel conversations with separate Claude sessions. ### Features - **Multiple tabs per session**: Each tab maintains its own Claude session ID - **Tab management**: Create, close, rename, star tabs - **Read-only mode**: Per-tab toggle to prevent accidental input - **Save-to-history toggle**: Per-tab control over history persistence - **Tab switcher modal**: Quick navigation via `Alt+Cmd+T` - **Unread filtering**: Filter to show only tabs with unread messages ### Tab Interface ```typescript interface AITab { id: string; name: string; agentSessionId?: string; // Separate Claude session per tab aiLogs: LogEntry[]; // Tab-specific conversation history isStarred: boolean; readOnlyMode: boolean; saveToHistory: boolean; unreadCount: number; createdAt: number; } ``` ### Session Fields ```typescript // In Session interface aiTabs: AITab[]; // Array of AI tabs activeAITabId: string; // Currently active tab ID ``` ### Shortcuts | Shortcut | Action | |----------|--------| | `Cmd+T` | New tab | | `Cmd+W` | Close current tab | | `Alt+Cmd+T` | Open tab switcher | | `Cmd+Shift+]` | Next tab | | `Cmd+Shift+[` | Previous tab | --- ## File Preview Tab System In-tab file viewing that integrates file previews alongside AI conversation tabs. Files open as separate tabs within the tab bar, maintaining their own state and scroll positions. ### Features - **Unified tab bar**: File tabs appear alongside AI tabs with consistent styling - **Deduplication**: Same file opened twice within a session activates existing tab - **Multi-session support**: Same file can be open in multiple sessions simultaneously - **State persistence**: Scroll position, search query, and edit mode preserved across restarts - **SSH remote files**: Supports viewing files from remote SSH hosts with loading states - **Extension badges**: Color-coded file extension badges with theme-aware and colorblind-safe palettes - **Overlay menu**: Right-click or hover menu for file operations (copy path, reveal in finder, etc.) ### File Tab Interface ```typescript interface FilePreviewTab { id: string; // Unique tab ID (UUID) path: string; // Full file path name: string; // Filename without extension (tab display name) extension: string; // File extension with dot (e.g., '.md', '.ts') content: string; // File content (loaded on open) scrollTop: number; // Preserved scroll position searchQuery: string; // Preserved search query editMode: boolean; // Whether tab was in edit mode editContent?: string; // Unsaved edit content (if pending changes) createdAt: number; // Timestamp for ordering lastModified: number; // File modification time (for refresh detection) sshRemoteId?: string; // SSH remote ID for remote files isLoading?: boolean; // True while content is being fetched } ``` ### Unified Tab System AI and file tabs share a unified tab order managed by `unifiedTabOrder`: ```typescript // Reference to any tab type type UnifiedTabRef = { type: 'ai' | 'file'; id: string }; // Discriminated union for rendering type UnifiedTab = | { type: 'ai'; id: string; data: AITab } | { type: 'file'; id: string; data: FilePreviewTab }; ``` ### Session Fields ```typescript // In Session interface filePreviewTabs: FilePreviewTab[]; // Array of file preview tabs activeFileTabId: string | null; // Active file tab (null if AI tab active) unifiedTabOrder: UnifiedTabRef[]; // Visual order of all tabs closedUnifiedTabHistory: ClosedUnifiedTab[]; // Undo stack for Cmd+Shift+T ``` ### Behavior - **Opening files**: Double-click in file explorer, click file links in AI output, or use Go to File (`Cmd+P`) - **Tab switching**: Click tab or use `Cmd+Shift+[`/`]` to navigate - **Tab closing**: Click X button, use `Cmd+W`, or right-click → Close - **Restore closed**: `Cmd+Shift+T` restores recently closed tabs (both AI and file) - **Edit mode**: Toggle edit mode in file preview; unsaved changes prompt on close ### Extension Color Mapping File tabs display a colored badge based on file extension. Colors are theme-aware (light/dark) and support colorblind-safe palettes: | Extensions | Light Theme | Dark Theme | Colorblind (Wong palette) | |------------|-------------|------------|---------------------------| | .ts, .tsx, .js, .jsx | Blue | Light Blue | #0077BB | | .md, .mdx, .txt | Green | Light Green | #009988 | | .json, .yaml, .toml | Amber | Yellow | #EE7733 | | .css, .scss, .less | Purple | Light Purple | #AA4499 | | .html, .xml, .svg | Orange | Light Orange | #CC3311 | | .py | Teal | Cyan | #33BBEE | | .rs | Rust | Light Rust | #EE3377 | | .go | Cyan | Light Cyan | #44AA99 | | .sh, .bash, .zsh | Gray | Light Gray | #BBBBBB | ### Key Files | File | Purpose | |------|---------| | `TabBar.tsx` | Unified tab rendering with AI and file tabs | | `FilePreview.tsx` | File content viewer with edit mode | | `MainPanel.tsx` | Coordinates tab display and file loading | | `App.tsx` | File tab creation (`handleOpenFileTab`) | | `useDebouncedPersistence.ts` | Persists file tabs across sessions | --- ## Execution Queue Sequential message processing system that prevents race conditions when multiple operations target the same session. ### Components | Component | Purpose | |-----------|---------| | `ExecutionQueueIndicator.tsx` | Shows queue status in tab bar | | `ExecutionQueueBrowser.tsx` | Modal for viewing/managing queue | ### Queue Item Types ```typescript interface QueuedItem { id: string; type: 'message' | 'command'; content: string; tabId: string; readOnlyMode: boolean; timestamp: number; source: 'user' | 'autorun'; } ``` ### Behavior - **Write operations** (readOnlyMode: false) queue sequentially - **Read-only operations** can run in parallel - Auto Run tasks queue with regular messages - Queue visible via indicator in tab bar - Users can cancel pending items via queue browser ### Session Fields ```typescript // In Session interface executionQueue: QueuedItem[]; // Pending operations isProcessingQueue: boolean; // Currently processing ``` --- ## Navigation History Back/forward navigation through sessions and tabs, similar to browser history. ### Implementation `useNavigationHistory` hook maintains a stack of navigation entries: ```typescript interface NavigationEntry { sessionId: string; tabId?: string; timestamp: number; } ``` ### Behavior - Maximum 50 entries in history - Automatic cleanup of invalid entries (deleted sessions/tabs) - Skips duplicate consecutive entries ### Shortcuts | Shortcut | Action | |----------|--------| | `Cmd+Shift+,` | Navigate back | | `Cmd+Shift+.` | Navigate forward | --- ## Group Chat System Multi-agent coordination system where a moderator AI orchestrates conversations between multiple Maestro agents, synthesizing their responses into cohesive answers. ### Architecture Overview ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ GROUP CHAT FLOW │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ User sends message │ │ │ │ │ ▼ │ │ ┌─────────────┐ │ │ │ MODERATOR │ ◄─── Receives user message + chat history │ │ └──────┬──────┘ │ │ │ │ │ ▼ │ │ Has @mentions? ───No───► Return directly to user │ │ │ │ │ Yes │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────┐ │ │ │ Route to mentioned agents │ │ │ │ @AgentA @AgentB @AgentC ... │ │ │ └──────────────────────────────────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ Agent A │ │ Agent B │ │ Agent C │ (work in parallel) │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ └───────────┴───────────┘ │ │ │ │ │ ▼ │ │ All agents responded │ │ │ │ │ ▼ │ │ ┌─────────────────────────────┐ │ │ │ MODERATOR reviews results │ ◄─── Synthesis round │ │ └──────────────┬──────────────┘ │ │ │ │ │ ▼ │ │ Need more info? ───Yes───► @mention agents again (loop back) │ │ │ │ │ No │ │ │ │ │ ▼ │ │ ┌─────────────────────────────┐ │ │ │ Final synthesis to user │ ◄─── No @mentions = conversation ends │ │ └─────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Key Design Principle: Moderator Controls the Flow The moderator is the traffic controller. After agents respond, the **moderator decides** what happens next: 1. **Continue with agents** - If responses are incomplete or unclear, @mention agents for follow-up 2. **Return to user** - If the question is fully answered, provide a synthesis WITHOUT any @mentions This allows the moderator to go back and forth with agents as many times as needed until the user's question is properly answered. ### Component Architecture ``` src/main/group-chat/ ├── group-chat-storage.ts # Persistence (JSON files in app data) ├── group-chat-log.ts # Chat history logging ├── group-chat-moderator.ts # Moderator process management & prompts ├── group-chat-router.ts # Message routing logic └── group-chat-agent.ts # Participant agent management ``` | Component | Purpose | |-----------|---------| | `group-chat-storage.ts` | CRUD operations for group chats, participants, history entries | | `group-chat-log.ts` | Append-only log of all messages (user, moderator, agents) | | `group-chat-moderator.ts` | Spawns moderator AI, defines system prompts | | `group-chat-router.ts` | Routes messages between user, moderator, and agents | | `group-chat-agent.ts` | Adds/removes participant agents | ### Message Routing #### Session ID Patterns The router uses session ID patterns to identify message sources: | Pattern | Source | |---------|--------| | `group-chat-{chatId}-moderator-{timestamp}` | Moderator process | | `group-chat-{chatId}-participant-{name}-{timestamp}` | Agent participant | #### Routing Functions ```typescript // User → Moderator routeUserMessage(groupChatId, message, processManager, agentDetector, readOnly) // Moderator → Agents (extracts @mentions, spawns agent processes) routeModeratorResponse(groupChatId, message, processManager, agentDetector, readOnly) // Agent → Back to moderator (triggers synthesis when all agents respond) routeAgentResponse(groupChatId, participantName, message, processManager) // Synthesis round (after all agents respond) spawnModeratorSynthesis(groupChatId, processManager, agentDetector) ``` ### The Synthesis Flow When all agents finish responding: 1. `markParticipantResponded()` tracks pending responses 2. When last agent responds, `spawnModeratorSynthesis()` is called 3. Moderator receives all agent responses in chat history 4. Moderator either: - **@mentions agents** → Routes back to agents (loop continues) - **No @mentions** → Final response to user (loop ends) This is enforced by `routeModeratorResponse()` which checks for @mentions: ```typescript // Extract mentions and route to agents const mentions = extractMentions(message, participants); if (mentions.length > 0) { // Spawn agent processes, track pending responses for (const participantName of mentions) { // ... spawn batch process for agent participantsToRespond.add(participantName); } pendingParticipantResponses.set(groupChatId, participantsToRespond); } // If no mentions, message is logged but no agents are spawned // = conversation turn complete, ball is back with user ``` ### Moderator Prompts Two key prompts control moderator behavior: **MODERATOR_SYSTEM_PROMPT** - Base instructions for all moderator interactions: - Assist user directly for simple questions - Coordinate agents via @mentions when needed - Control conversation flow - Return to user only when answer is complete **MODERATOR_SYNTHESIS_PROMPT** - Used when reviewing agent responses: - Synthesize if responses are complete (NO @mentions) - @mention agents if more info needed - Keep going until user's question is answered ### Data Flow Example ``` User: "How does @Maestro and @RunMaestro.ai relate?" 1. routeUserMessage() - Logs message as "user" - Auto-adds @Maestro and @RunMaestro.ai as participants - Spawns moderator process with user message 2. Moderator responds: "Let me ask the agents. @Maestro @RunMaestro.ai explain..." routeModeratorResponse() - Logs message as "moderator" - Extracts mentions: ["Maestro", "RunMaestro.ai"] - Spawns batch processes for each agent - Sets pendingParticipantResponses = {"Maestro", "RunMaestro.ai"} 3. Agent "Maestro" responds routeAgentResponse() - Logs message as "Maestro" - markParticipantResponded() → pendingParticipantResponses = {"RunMaestro.ai"} - Not last agent, so no synthesis yet 4. Agent "RunMaestro.ai" responds routeAgentResponse() - Logs message as "RunMaestro.ai" - markParticipantResponded() → pendingParticipantResponses = {} (empty) - Last agent! Triggers spawnModeratorSynthesis() 5. Moderator synthesis - Receives all agent responses in chat history - Decision point: a) If needs more info: "@Maestro can you clarify..." → back to step 2 b) If satisfied: "Here's how they relate..." (no @mentions) → done 6. Final response to user (no @mentions = turn complete) ``` ### IPC Handlers ```typescript // Group chat management 'groupchat:create': (name, moderatorAgentId) => Promise 'groupchat:get': (id) => Promise 'groupchat:list': () => Promise 'groupchat:delete': (id) => Promise // Participant management 'groupchat:addParticipant': (chatId, name, agentId, cwd) => Promise 'groupchat:removeParticipant': (chatId, name) => Promise // Messaging 'groupchat:sendMessage': (chatId, message, readOnly?) => Promise // State 'groupchat:getMessages': (chatId, limit?) => Promise 'groupchat:getHistory': (chatId) => Promise ``` ### Events ```typescript // Real-time updates to renderer groupChatEmitters.emitMessage(chatId, message) // New message groupChatEmitters.emitStateChange(chatId, state) // 'idle' | 'agent-working' groupChatEmitters.emitParticipantsChanged(chatId, list) // Participant list updated groupChatEmitters.emitHistoryEntry(chatId, entry) // New history entry groupChatEmitters.emitModeratorUsage(chatId, usage) // Token usage stats ``` ### Storage Structure ``` ~/Library/Application Support/maestro/group-chats/ ├── {chatId}/ │ ├── chat.json # Group chat metadata │ ├── log.jsonl # Append-only message log │ └── history.json # Summarized history entries ``` --- ## Web/Mobile Interface Progressive Web App (PWA) for remote control of Maestro from mobile devices. ### Architecture ``` src/web/ ├── index.ts # Entry point ├── main.tsx # React entry ├── index.html # HTML template ├── index.css # Global styles ├── components/ # Shared components (Button, Card, Input, Badge) ├── hooks/ # Web-specific hooks ├── mobile/ # Mobile-optimized components ├── utils/ # Web utilities └── public/ # PWA assets (manifest, icons, service worker) ``` ### Mobile Components (`src/web/mobile/`) | Component | Purpose | |-----------|---------| | `App.tsx` | Main mobile app shell | | `TabBar.tsx` | Bottom navigation bar | | `SessionPillBar.tsx` | Horizontal session selector | | `CommandInputBar.tsx` | Message input with voice support | | `MessageHistory.tsx` | Conversation display | | `ResponseViewer.tsx` | AI response viewer | | `AutoRunIndicator.tsx` | Auto Run status display | | `MobileHistoryPanel.tsx` | Command history browser | | `QuickActionsMenu.tsx` | Quick action shortcuts | | `SlashCommandAutocomplete.tsx` | Command autocomplete | | `ConnectionStatusIndicator.tsx` | WebSocket connection status | | `OfflineQueueBanner.tsx` | Offline message queue indicator | ### Web Hooks (`src/web/hooks/`) | Hook | Purpose | |------|---------| | `useWebSocket.ts` | WebSocket connection management | | `useSessions.ts` | Session state synchronization | | `useCommandHistory.ts` | Command history management | | `useSwipeGestures.ts` | Touch gesture handling | | `usePullToRefresh.ts` | Pull-to-refresh functionality | | `useOfflineQueue.ts` | Offline message queuing | | `useNotifications.ts` | Push notification handling | | `useDeviceColorScheme.ts` | System theme detection | | `useUnreadBadge.ts` | Unread message badge | | `useSwipeUp.ts` | Swipe-up gesture detection | ### Communication The web interface communicates with the desktop app via WebSocket: ```typescript // Desktop broadcasts to web clients window.maestro.web.broadcastUserInput(sessionId, input); window.maestro.web.broadcastAutoRunState(sessionId, state); window.maestro.web.broadcastTabChange(sessionId, tabId); // Web client sends commands back websocket.send({ type: 'command', sessionId, content }); ``` ### PWA Features - Installable as standalone app - Service worker for offline support - Push notifications - Responsive design for phones and tablets --- ## CLI Tool Command-line interface for headless Maestro operations. ### Architecture ``` src/cli/ ├── index.ts # CLI entry point ├── commands/ # Command implementations │ ├── list-agents.ts │ ├── list-groups.ts │ ├── list-playbooks.ts │ ├── run-playbook.ts │ ├── show-agent.ts │ └── show-playbook.ts ├── output/ # Output formatters │ ├── formatter.ts # Human-readable output │ └── jsonl.ts # JSONL streaming output └── services/ # CLI-specific services ├── agent-spawner.ts # Agent process management ├── batch-processor.ts # Batch execution logic ├── playbooks.ts # Playbook operations └── storage.ts # Data persistence ``` ### Available Commands | Command | Description | |---------|-------------| | `maestro list-agents` | List available AI agents | | `maestro list-groups` | List session groups | | `maestro list-playbooks` | List saved playbooks | | `maestro show-agent ` | Show agent details | | `maestro show-playbook ` | Show playbook configuration | | `maestro run-playbook ` | Execute a playbook | ### Output Formats - **Human-readable**: Formatted tables and text (default) - **JSONL**: Streaming JSON lines for programmatic use (`--format jsonl`) --- ## Shared Module Cross-platform code shared between main process, renderer, web, and CLI. ### Files ``` src/shared/ ├── index.ts # Re-exports ├── types.ts # Shared type definitions ├── theme-types.ts # Theme interface shared across platforms ├── cli-activity.ts # CLI activity tracking types └── templateVariables.ts # Template variable utilities ``` ### Theme Types ```typescript // Shared theme interface used by renderer, main (web server), and web client interface Theme { id: ThemeId; name: string; mode: 'light' | 'dark' | 'vibe'; colors: ThemeColors; } ``` ### Template Variables Utilities for processing template variables in Custom AI Commands: ```typescript // Available variables {{date}} // YYYY-MM-DD {{time}} // HH:MM:SS {{datetime}} // Combined {{cwd}} // Working directory {{session}} // Session name {{agent}} // Agent type ``` --- ## Remote Access & Tunnels Secure remote access to Maestro via Cloudflare Tunnels. ### Architecture | Component | Purpose | |-----------|---------| | `tunnel-manager.ts` | Manages cloudflared process lifecycle | | `utils/cliDetection.ts` | Detects cloudflared installation | | `utils/networkUtils.ts` | Local IP detection for LAN access | ### Tunnel Manager ```typescript interface TunnelStatus { active: boolean; url?: string; // Public tunnel URL error?: string; } // IPC API window.maestro.tunnel.start(); window.maestro.tunnel.stop(); window.maestro.tunnel.getStatus(); window.maestro.tunnel.onStatusChange(callback); ``` ### Access Methods 1. **Local Network**: Direct IP access on same network 2. **Cloudflare Tunnel**: Secure public URL for remote access ### Security - Tunnels require cloudflared CLI installed - URLs are temporary and change on restart - No authentication by default (consider network security) --- ## Error Handling Patterns ### IPC Handlers (Main Process) **Pattern 1: Throw for critical failures** ```typescript ipcMain.handle('process:spawn', async (_, config) => { if (!processManager) throw new Error('Process manager not initialized'); return processManager.spawn(config); }); ``` **Pattern 2: Try-catch with boolean return** ```typescript ipcMain.handle('git:isRepo', async (_, cwd) => { try { const result = await execFileNoThrow('git', ['rev-parse', '--is-inside-work-tree'], cwd); return result.exitCode === 0; } catch { return false; } }); ``` ### Services (Renderer) **Pattern: Never throw, return safe defaults** ```typescript export const gitService = { async isRepo(cwd: string): Promise { try { return await window.maestro.git.isRepo(cwd); } catch (error) { console.error('Git isRepo error:', error); return false; } }, }; ``` ### React Components **Pattern: Try-catch with user-friendly errors** ```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'); } }; ``` ### Summary | Layer | Pattern | |-------|---------| | IPC Handlers | Throw critical, catch optional | | Services | Never throw, safe defaults | | ProcessManager | Throw spawn failures, emit runtime events | | Components | Try-catch async, show UI errors | | Hooks | Internal catch, expose error state |