Deep technical documentation for Maestro's architecture and design patterns. For quick reference, see CLAUDE.md. For development setup, see CONTRIBUTING.md.
- Dual-Process Architecture
- IPC Security Model
- Process Manager
- Layer Stack System
- Custom Hooks
- Services Layer
- Custom AI Commands
- Theme System
- Settings Persistence
- Claude Sessions API
- Auto Run System
- Achievement System
- AI Tab System
- File Preview Tab System
- Execution Queue
- Navigation History
- Group Chat System
- Web/Mobile Interface
- CLI Tool
- Shared Module
- Remote Access & Tunnels
- Error Handling Patterns
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.
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
Maestro uses Electron's main/renderer split with strict context isolation.
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 |
React frontend with no direct Node.js access:
| Directory | Purpose |
|---|---|
components/ |
React UI components |
hooks/ |
Custom React hooks (15 hooks - see 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 |
Each session runs two processes simultaneously:
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.
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', ...)
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 },
}The ProcessManager class (src/main/process-manager.ts) handles two process types:
Used for terminal sessions with full shell emulation:
toolType: 'terminal'- Supports resize, ANSI escape codes, interactive shell
- Spawned with shell (zsh, bash, fish, etc.)
Used for AI assistants:
- All non-terminal tool types
- Direct stdin/stdout/stderr capture
- Security: Uses
spawn()withshell: false
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
When images are attached:
- Uses
--input-format stream-json --output-format stream-json - Message sent via stdin as JSONL
- Supports multimodal input
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) => { ... });Centralized modal/overlay management with predictable Escape key handling.
- Previously had 9+ scattered Escape handlers
- Brittle modal detection with massive boolean checks
- Inconsistent focus management
| 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 |
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
};import { useLayerStack } from '../contexts/LayerStackContext';
import { MODAL_PRIORITIES } from '../constants/modalPriorities';
const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack();
const layerIdRef = useRef<string>();
// 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 depstype ModalLayer = {
type: 'modal';
priority: number;
blocksLowerLayers: boolean;
capturesFocus: boolean;
focusTrap: 'strict' | 'lenient' | 'none';
ariaLabel?: string;
onEscape: () => void;
onBeforeClose?: () => Promise<boolean>;
isDirty?: boolean;
parentModalId?: string;
};
type OverlayLayer = {
type: 'overlay';
priority: number;
blocksLowerLayers: boolean;
capturesFocus: boolean;
focusTrap: 'strict' | 'lenient' | 'none';
ariaLabel?: string;
onEscape: () => void;
allowClickOutside: boolean;
};Components like FilePreview handle internal search in their onEscape:
onEscape: () => {
if (searchOpen) {
setSearchOpen(false); // First Escape closes search
} else {
closePreview(); // Second Escape closes preview
}
}Maestro uses 15 custom hooks for state management and functionality.
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
Manages sessions and groups with CRUD operations.
Key methods:
createNewSession(agentId, workingDir, name)- Creates new session with dual processesdeleteSession(id, showConfirmation)- Delete with confirmationtoggleInputMode()- Switch between AI and terminal modeupdateScratchPad(content)- Update session scratchpadcreateNewGroup(name, emoji, moveSession, activeSessionId)- Drag and drop handlers
Manages file tree refresh/filter state and git-related file metadata.
Key methods:
refreshFileTree(sessionId)- Reload directory tree and return change statsrefreshGitFileState(sessionId)- Refresh tree + git repo metadatafilteredFileTree- Derived tree based on filter string
Manages Auto Run batch execution logic.
Key methods:
startBatchRun(config)- Start batch document processingstopBatchRun()- Stop current batch runpauseBatchRun()/resumeBatchRun()- Pause/resume execution
Core layer management for modals and overlays.
Key methods:
registerLayer(config)- Register a modal/overlayunregisterLayer(id)- Remove a layerupdateLayerHandler(id, handler)- Update escape handler
Back/forward navigation through sessions and tabs. See Navigation History.
Handles @-mention autocomplete for file references in prompts.
Tab completion utilities for terminal-style input.
Template variable autocomplete (e.g., {{date}}, {{time}}).
Achievement/badge system for Auto Run usage. See Achievement System.
User activity tracking for session idle detection and status.
Mobile landscape orientation detection for responsive layouts.
Services provide clean wrappers around IPC calls.
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 }] }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) => { ... });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.
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
Commands are defined in Settings → Custom AI Commands:
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
}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.)
// 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
}Maestro also fetches slash commands from Claude Code CLI when available, making Claude Code's built-in commands accessible through the same autocomplete interface.
Themes defined in src/renderer/constants/themes.ts.
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)
};
}Dark themes: Dracula, Monokai, Nord, Tokyo Night, Catppuccin Mocha, Gruvbox Dark
Light themes: GitHub, Solarized, One Light, Gruvbox Light, Catppuccin Latte, Ayu Light
Use inline styles for theme colors:
style={{ color: theme.colors.textMain }} // CorrectUse Tailwind for layout:
className="flex items-center gap-2" // CorrectSettings stored via electron-store:
Locations:
- macOS:
~/Library/Application Support/maestro/ - Windows:
%APPDATA%/maestro/ - Linux:
~/.config/maestro/
Files:
maestro-settings.json- User preferencesmaestro-sessions.json- Session persistencemaestro-groups.json- Session groupsmaestro-agent-configs.json- Per-agent configuration
- Add state in
useSettings.ts:
const [mySetting, setMySettingState] = useState<MyType>(defaultValue);- Create wrapper function:
const setMySetting = (value: MyType) => {
setMySettingState(value);
window.maestro.settings.set('mySetting', value);
};- Load in useEffect:
const saved = await window.maestro.settings.get('mySetting');
if (saved !== undefined) setMySettingState(saved);- Add to return object and export.
Browse and resume Claude Code sessions from ~/.claude/projects/.
Claude Code encodes project paths by replacing / with -:
/Users/pedram/Projects/Maestro→-Users-pedram-Projects-Maestro
// 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- Shortcut:
Cmd+Shift+L - Quick Actions:
Cmd+K→ "View Agent Sessions" - Button in main panel header
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 | 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 |
// 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;
};
}Auto Run state is stored per-session:
// 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;// 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? }>When worktree is enabled, Auto Run operates in an isolated directory:
// 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;
}>- Setup: User selects Runner Docs folder via
AutoRunSetupModal - Document Selection: Documents appear in
AutoRunDocumentSelectordropdown - Editing:
AutoRuncomponent provides edit/preview modes with auto-save (5s debounce) - Batch Configuration:
BatchRunnerModalallows ordering documents, enabling loop/reset, configuring worktree - Playbooks: Save/load configurations for repeated batch runs
- Execution:
useBatchProcessorhook processes documents sequentially - Progress: RightPanel shows document and task-level progress
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
Gamification system that rewards Auto Run usage with conductor-themed badges.
| 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 |
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 |
When a new badge is unlocked:
StandingOvationOverlaydisplays with confetti animation- Badge details shown with celebration message
- Share functionality available
- Acknowledgment persisted to prevent re-showing
Multi-tab support within each session, allowing parallel conversations with separate Claude sessions.
- 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
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;
}// In Session interface
aiTabs: AITab[]; // Array of AI tabs
activeAITabId: string; // Currently active tab ID| 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 |
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.
- 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.)
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
}AI and file tabs share a unified tab order managed by unifiedTabOrder:
// 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 };// 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- 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+Trestores recently closed tabs (both AI and file) - Edit mode: Toggle edit mode in file preview; unsaved changes prompt on close
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 |
| 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 |
Sequential message processing system that prevents race conditions when multiple operations target the same session.
| Component | Purpose |
|---|---|
ExecutionQueueIndicator.tsx |
Shows queue status in tab bar |
ExecutionQueueBrowser.tsx |
Modal for viewing/managing queue |
interface QueuedItem {
id: string;
type: 'message' | 'command';
content: string;
tabId: string;
readOnlyMode: boolean;
timestamp: number;
source: 'user' | 'autorun';
}- 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
// In Session interface
executionQueue: QueuedItem[]; // Pending operations
isProcessingQueue: boolean; // Currently processingBack/forward navigation through sessions and tabs, similar to browser history.
useNavigationHistory hook maintains a stack of navigation entries:
interface NavigationEntry {
sessionId: string;
tabId?: string;
timestamp: number;
}- Maximum 50 entries in history
- Automatic cleanup of invalid entries (deleted sessions/tabs)
- Skips duplicate consecutive entries
| Shortcut | Action |
|---|---|
Cmd+Shift+, |
Navigate back |
Cmd+Shift+. |
Navigate forward |
Multi-agent coordination system where a moderator AI orchestrates conversations between multiple Maestro agents, synthesizing their responses into cohesive answers.
┌─────────────────────────────────────────────────────────────────────────┐
│ 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 │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
The moderator is the traffic controller. After agents respond, the moderator decides what happens next:
- Continue with agents - If responses are incomplete or unclear, @mention agents for follow-up
- 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.
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 |
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 |
// 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)When all agents finish responding:
markParticipantResponded()tracks pending responses- When last agent responds,
spawnModeratorSynthesis()is called - Moderator receives all agent responses in chat history
- 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:
// 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 userTwo 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
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)
// Group chat management
'groupchat:create': (name, moderatorAgentId) => Promise<GroupChat>
'groupchat:get': (id) => Promise<GroupChat | null>
'groupchat:list': () => Promise<GroupChat[]>
'groupchat:delete': (id) => Promise<void>
// Participant management
'groupchat:addParticipant': (chatId, name, agentId, cwd) => Promise<void>
'groupchat:removeParticipant': (chatId, name) => Promise<void>
// Messaging
'groupchat:sendMessage': (chatId, message, readOnly?) => Promise<void>
// State
'groupchat:getMessages': (chatId, limit?) => Promise<GroupChatMessage[]>
'groupchat:getHistory': (chatId) => Promise<GroupChatHistoryEntry[]>// 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~/Library/Application Support/maestro/group-chats/
├── {chatId}/
│ ├── chat.json # Group chat metadata
│ ├── log.jsonl # Append-only message log
│ └── history.json # Summarized history entries
Progressive Web App (PWA) for remote control of Maestro from mobile devices.
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)
| 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 |
| 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 |
The web interface communicates with the desktop app via WebSocket:
// 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 });- Installable as standalone app
- Service worker for offline support
- Push notifications
- Responsive design for phones and tablets
Command-line interface for headless Maestro operations.
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
| Command | Description |
|---|---|
maestro list-agents |
List available AI agents |
maestro list-groups |
List session groups |
maestro list-playbooks |
List saved playbooks |
maestro show-agent <id> |
Show agent details |
maestro show-playbook <id> |
Show playbook configuration |
maestro run-playbook <id> |
Execute a playbook |
- Human-readable: Formatted tables and text (default)
- JSONL: Streaming JSON lines for programmatic use (
--format jsonl)
Cross-platform code shared between main process, renderer, web, and CLI.
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
// Shared theme interface used by renderer, main (web server), and web client
interface Theme {
id: ThemeId;
name: string;
mode: 'light' | 'dark' | 'vibe';
colors: ThemeColors;
}Utilities for processing template variables in Custom AI Commands:
// Available variables
{{date}} // YYYY-MM-DD
{{time}} // HH:MM:SS
{{datetime}} // Combined
{{cwd}} // Working directory
{{session}} // Session name
{{agent}} // Agent typeSecure remote access to Maestro via Cloudflare Tunnels.
| Component | Purpose |
|---|---|
tunnel-manager.ts |
Manages cloudflared process lifecycle |
utils/cliDetection.ts |
Detects cloudflared installation |
utils/networkUtils.ts |
Local IP detection for LAN access |
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);- Local Network: Direct IP access on same network
- Cloudflare Tunnel: Secure public URL for remote access
- Tunnels require cloudflared CLI installed
- URLs are temporary and change on restart
- No authentication by default (consider network security)
Pattern 1: Throw for critical failures
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
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;
}
});Pattern: Never throw, return safe defaults
export const gitService = {
async isRepo(cwd: string): Promise<boolean> {
try {
return await window.maestro.git.isRepo(cwd);
} catch (error) {
console.error('Git isRepo error:', error);
return false;
}
},
};Pattern: Try-catch with user-friendly errors
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');
}
};| 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 |