Created by Stravu
Crystal is a fully-implemented Electron desktop application for managing multiple Claude Code instances against a single directory using git worktrees. It provides a streamlined interface for running parallel Claude Code sessions with different approaches to the same problem.
Use these reference pages for more information:
- How to invoke Claude Code through the command line as an SDK: https://docs.anthropic.com/en/docs/claude-code/sdk
- How to run multiple Claude Code instances with Git Worktrees: https://docs.anthropic.com/en/docs/claude-code/tutorials#run-parallel-claude-code-sessions-with-git-worktrees
All core features have been successfully implemented with significant enhancements beyond the original requirements.
- Multi-session support: Run multiple Claude Code instances simultaneously
- Session templates: Create single or multiple sessions with numbered templates
- Session persistence: SQLite database for persistent sessions across restarts
- Session archiving: Archive sessions instead of permanent deletion
- Conversation continuation: Resume conversations with full history context
- Real-time status tracking: initializing, running, waiting, stopped, error
- Automatic session naming: AI-powered session name generation based on prompts
- Isolated development: Each Claude Code session operates in its own git worktree
- Conflict prevention: Prevents conflicts between parallel development efforts
- Automatic cleanup: Worktree cleanup when sessions are deleted
- Branch management: Support for existing branches or creation of new branches
- Empty repo handling: Automatic initial commit for repositories with no commits
- Rebase from main: Pull latest changes from main branch into worktree
- Squash and rebase to main: Combine all commits and rebase onto main
- Diff visualization: View all changes with syntax highlighting
- Commit tracking: History with statistics (additions, deletions, files changed)
- Uncommitted changes: Detection and display of uncommitted changes
- Command preview: Git command tooltips for transparency
- Error handling: Detailed error dialogs with full git output
- Multiple projects: Support for multiple projects with easy switching
- Auto-initialization: Automatic directory creation and git initialization
- Project settings: Custom prompts, run scripts, main branch configuration
- Active project: Persistent active project selection
- Professional terminal: XTerm.js with 50,000 line scrollback buffer
- Multiple view modes:
- Output View: Formatted terminal output with syntax highlighting
- Messages View: Raw JSON message inspection for debugging
- View Diff View: Git diff viewer with file statistics
- Terminal View: Dedicated terminal for running project scripts
- Sidebar navigation: Session list, project selector, prompt history
- Real-time updates: IPC-based live output streaming
- Status indicators: Color-coded badges with animations
- Unread indicators: Activity tracking across views
- Prompt history: Complete history of all prompts across sessions
- Search functionality: Search prompts and session names
- Quick reuse: One-click prompt reuse for new sessions
- Prompt navigation: Jump to specific prompts within session output
- Clipboard support: Copy prompts to clipboard
- Multi-line input: Auto-resizing textarea with keyboard shortcuts
- Smart formatting: Automatic formatting of JSON messages
- Tool call display: Clear visual structure for Claude's tool usage
- Script execution: Run project scripts with real-time output
- Process management: Start/stop script processes
- Global settings:
- Verbose logging toggle
- Anthropic API key configuration
- Global system prompt additions
- Custom Claude executable path
- Notification settings:
- Desktop notifications toggle
- Sound notifications with Web Audio API
- Customizable triggers (status changes, waiting, completion, errors)
- Project-specific settings:
- Custom system prompts per project
- Run scripts for testing/building
- Main branch customization
- SQLite Database:
projects: Project configurations and pathssessions: Core session metadatasession_outputs: Terminal output historyconversation_messages: Conversation history for continuationsexecution_diffs: Git diff tracking per executionprompt_markers: Navigation markers for prompts
- Automatic initialization:
~/.crystaldirectory created on first run - Migration system: SQL migrations for schema evolution
- Electron Store: Application configuration persistence
- Task Queue: Bull queue with optional Redis support
- Process Management: node-pty for Claude Code instances
- Error handling: Comprehensive error reporting and recovery
- Performance optimizations: Lazy loading, debounced updates, caching
- Keyboard shortcuts: Cmd/Ctrl+Enter for input submission
- Main Process: Electron main process with IPC communication
- Window management with native OS integration
- Electron Store for configuration persistence
- IPC handlers for renderer communication
- Framework: React 19 with TypeScript
- State Management: Zustand for reactive state management
- UI Styling: Tailwind CSS utility-first framework
- Terminal: @xterm/xterm professional terminal emulator
- Build Tool: Vite for fast development
- Icons: Lucide React for consistent iconography
- Runtime: Node.js with TypeScript
- IPC Server: Direct IPC communication with renderer process
- Database: Better-SQLite3 for synchronous operations
- Task Queue: Bull with in-memory queue for Electron
- Claude Integration: @anthropic-ai/claude-code SDK
- Process Management: node-pty for PTY processes
- Git Integration: Command-line git worktree management
- Electron IPC: Secure inter-process communication for all operations
- Event System: IPC-based event handling for real-time updates
┌─────────────────────────────────────────────────────────┐
│ Electron Desktop Application │
├─────────────────────────────────────────────────────────┤
│ Renderer Process (Frontend) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────┐ │
│ │ Sidebar │ │ Terminal │ │ Help │ │
│ │ (Sessions) │ │ (XTerm.js) │ │ Dialog │ │
│ └─────────────────┘ └─────────────────┘ └────────────┘ │
├─────────────────────────────────────────────────────────┤
│ IPC Communication │
├─────────────────────────────────────────────────────────┤
│ Main Process (Electron + Node.js) │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ Task Queue │ │ Session │ │ Config │ │
│ │ (Bull) │ │ Manager │ │ Manager │ │
│ └──────────────┘ └──────────────┘ └───────────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ Worktree │ │ Claude Code │ │ Config │ │
│ │ Manager │ │ Manager │ │ Manager │ │
│ └──────────────┘ └──────────────┘ └───────────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ IPC Handlers │ │ Event │ │ Git Diff │ │
│ │(git,session) │ │ Manager │ │ Manager │ │
│ └──────────────┘ └──────────────┘ └───────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Better-SQLite3 Database │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ sessions │ │session_ │ │conversation_ │ │
│ │ table │ │outputs │ │messages │ │
│ └─────────────┘ └─────────────────┘ └─────────────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │execution_ │ │prompt_ │ │ projects │ │
│ │diffs │ │markers │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Claude Code SDK Instances (node-pty) │
│ ┌─────────────────────────────┐ │
│ │ Git Worktrees │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
The main process has been refactored from a monolithic index.ts file (previously 2,705 lines) into a modular structure:
index.ts(414 lines): Core Electron setup and initializationipc/git.ts(843 lines): All git-related IPC handlersipc/session.ts(428 lines): Session management IPC handlersevents.ts(359 lines): Event handling and coordination
The frontend has also been modularized:
useSessionView.ts(941 lines): Extracted session view logic from the previous monolithic SessionView component
This modular structure improves maintainability and makes it easier to locate and modify specific functionality.
-
Database Storage:
- Raw JSON messages from Claude are stored as-is in the database
- Stdout/stderr outputs are stored directly
- No formatting or transformation happens at storage time
-
Real-time Streaming:
- When Claude outputs data, it's saved to the database immediately
- For JSON messages, a formatted stdout version is sent to the frontend for the Output view
- The original JSON is also sent for the Messages view
- This provides immediate visual feedback during active sessions
-
Session Loading:
- When navigating to a session, outputs are loaded from the database
- The
sessions:get-outputhandler transforms JSON messages to formatted stdout on-the-fly - Uses
setSessionOutputsfor atomic updates to prevent race conditions
-
Frontend Display:
- The useSessionView hook manages session view logic and state (extracted from SessionView component)
- A mutex lock (
loadingRef) prevents concurrent loads - Timing is carefully managed with
requestAnimationFrameand delays - The
formattedOutputstate is NOT cleared on session switch - it updates naturally
-
Key Principles:
- Database is the single source of truth
- Transformations happen on-the-fly, not at storage time
- Real-time updates supplement but don't replace database data
- Session switches always reload from database to ensure consistency
- Duplicate messages: Usually caused by sending both formatted and raw versions
- Disappearing content: Often due to clearing output states at the wrong time
- Black screens: Typically from race conditions during session switching
- Content only loads once: Results from improper state management or missing dependencies
The current implementation carefully balances real-time updates with data persistence to provide a smooth user experience.
Crystal uses timestamps throughout the application for tracking session activity, prompt execution times, and displaying time-based information. Due to the mix of SQLite database storage and JavaScript Date objects, special care must be taken to ensure timezone consistency.
- Database Storage: All timestamps are stored in UTC using SQLite's
CURRENT_TIMESTAMPordatetime()functions - Frontend Display: Timestamps are parsed as UTC and converted to local time only for display
- Consistency: Always use the timestamp utility functions instead of manual date parsing
- Validation: Always validate timestamps before using them in calculations
- SQLite DATETIME:
YYYY-MM-DD HH:MM:SS(stored in UTC without timezone indicator) - ISO 8601:
YYYY-MM-DDTHH:MM:SS.sssZ(with explicit UTC timezone) - JavaScript Date: Local timezone by default (be careful!)
Crystal provides timestamp utilities in both frontend and backend:
Backend (main/src/utils/timestampUtils.ts):
import { formatForDatabase, getCurrentTimestamp } from '../utils/timestampUtils';
// For database storage
const timestamp = formatForDatabase(); // Returns ISO string
const now = getCurrentTimestamp(); // Alias for formatForDatabase()
// For display formatting
const displayTime = formatForDisplay(timestamp);Frontend (frontend/src/utils/timestampUtils.ts):
import { parseTimestamp, formatDuration, getTimeDifference } from '../utils/timestampUtils';
// Parse SQLite timestamps correctly
const date = parseTimestamp("2024-01-01 12:00:00"); // Handles UTC conversion
// Calculate durations
const durationMs = getTimeDifference(startTime, endTime);
const formatted = formatDuration(durationMs); // "2m 34s"
// Display relative time
const ago = formatDistanceToNow(timestamp); // "5 minutes ago"When working with timestamps in SQLite:
-- Use datetime() for UTC timestamps
INSERT INTO prompt_markers (timestamp) VALUES (datetime('now'));
-- When selecting, append 'Z' for proper UTC parsing
SELECT datetime(timestamp) || 'Z' as timestamp FROM prompt_markers;
-- For completion timestamps with NULL handling
SELECT
CASE
WHEN completion_timestamp IS NOT NULL
THEN datetime(completion_timestamp) || 'Z'
ELSE NULL
END as completion_timestamp
FROM prompt_markers;Creating a new timestamp:
// Backend - for database storage
const timestamp = formatForDatabase();
// Frontend - for immediate use
const now = new Date();Tracking prompt execution time:
// When prompt starts
db.addPromptMarker(sessionId, promptText, outputIndex);
// When prompt completes
db.updatePromptMarkerCompletion(sessionId);Calculating duration:
// With completion timestamp
if (prompt.completion_timestamp) {
const duration = getTimeDifference(prompt.timestamp, prompt.completion_timestamp);
return formatDuration(duration);
}
// For ongoing prompts
const duration = getTimeDifference(prompt.timestamp); // Uses current time as end
return formatDuration(duration) + ' (ongoing)';-
Never parse SQLite timestamps directly with
new Date():// ❌ WRONG - treats UTC as local time const date = new Date("2024-01-01 12:00:00"); // ✅ CORRECT - uses parseTimestamp utility const date = parseTimestamp("2024-01-01 12:00:00");
-
Always validate timestamps before calculations:
if (!isValidTimestamp(timestamp)) { return 'Unknown duration'; }
-
Be careful with timezone conversions:
// Database stores UTC, display shows local const dbTime = "2024-01-01 12:00:00"; // UTC const parsed = parseTimestamp(dbTime); // Correctly handled as UTC const display = formatForDisplay(parsed); // Converts to local for display
-
Handle negative durations gracefully:
const duration = endTime - startTime; if (duration < 0) { console.warn('Negative duration detected'); return 'Invalid duration'; }
When testing timestamp-related features:
- Test with different timezones (especially negative UTC offsets)
- Test with daylight saving time transitions
- Test with very old and future timestamps
- Test with invalid/malformed timestamps
- Verify duration calculations are always positive
Crystal uses a combination of Zustand stores, IPC events, and targeted updates to manage application state efficiently. The application prioritizes specific, targeted updates over global refreshes to improve performance and user experience.
- Targeted Updates: Always update only the specific data that changed
- Event-Driven Updates: Use IPC events to communicate changes between processes
- Avoid Global Refreshes: Never reload entire lists when only one item changes
- Database as Source of Truth: Frontend state should reflect backend state, not override it
Session Updates:
// ❌ BAD: Global refresh
const handleSessionCreated = () => {
loadProjectsWithSessions(); // Reloads everything
};
// ✅ GOOD: Targeted update
const handleSessionCreated = (newSession: Session) => {
setProjectsWithSessions(prevProjects => {
return prevProjects.map(project => {
if (project.id === newSession.projectId) {
return {
...project,
sessions: [...project.sessions, newSession]
};
}
return project;
});
});
};Project Updates:
// ❌ BAD: Reload all projects
const handleProjectDeleted = () => {
fetchProjects(); // Network request for all projects
};
// ✅ GOOD: Remove from local state
const handleProjectDeleted = () => {
setProjects(prev => prev.filter(p => p.id !== deletedId));
};The application uses IPC events to synchronize state between the main process and renderer:
-
Session Events:
session:created- Add new session to appropriate projectsession:updated- Update specific session propertiessession:deleted- Remove session from project list
-
Project Events (if implemented):
project:created- Add new project to listproject:updated- Update specific project propertiesproject:deleted- Remove project from list
- Initial Load: When component mounts for the first time
- User-Triggered Refresh: When user explicitly requests a refresh
- Error Recovery: After connection loss or critical errors
- Complex State Changes: When multiple interdependent items change
DraggableProjectTreeView.tsx:
- Uses targeted updates for session creation, update, and deletion
- Only reloads all data on initial mount or when critical errors occur
- Maintains local state synchronized with backend through IPC events
ProjectSelector.tsx:
- Updates project list locally when projects are deleted
- Falls back to refresh only when necessary (e.g., complex updates)
- Use State Setters with Callbacks: Always use the callback form of setState to ensure you're working with the latest state
- Merge Updates: When updating objects, spread existing properties to preserve data
- Handle Edge Cases: Always check if the item exists before updating
- Log State Changes: Add console logs for debugging state updates in development
- Validate IPC Data: Ensure IPC events contain expected data structure
If the diff viewer content is cut off and scrollbars don't appear:
- DO NOT add complex CSS overrides - This often makes the problem worse
- Check parent containers for
overflow-hidden- This is usually the root cause - Use simple
overflow: 'auto'on the immediate diff container - Remove any forced widths or min-widths unless absolutely necessary
// In DiffViewer.tsx - Keep it simple!
<div className="border border-t-0 border-gray-600 rounded-b-lg" style={{ overflow: 'auto', maxHeight: '600px' }}>
<ReactDiffViewer
oldValue={file.oldValue || ''}
newValue={file.newValue || ''}
splitView={viewType === 'split'}
useDarkTheme={isDarkMode}
styles={currentStyles}
// Don't add complex style overrides here
/>
</div>- Don't add multiple wrapper divs with conflicting overflow settings
- Don't use CSS-in-JS to override react-diff-viewer's internal styles
- Don't add global CSS selectors targeting generated class names
- Don't use JavaScript hacks to force reflows
The issue is typically caused by parent containers having overflow-hidden which prevents child scrollbars from appearing. Check these files:
SessionView.tsx- Look foroverflow-hiddenclassesCombinedDiffView.tsx- Check both the main container and flex containersApp.tsx- Sometimes the issue starts at the app root level
The react-diff-viewer-continued library uses emotion/styled-components internally, which makes CSS overrides unreliable. The best approach is to ensure proper overflow handling in parent containers and keep the diff viewer wrapper simple.
GET /api/sessions- List all sessions with statusPOST /api/sessions- Create new session(s) with templatesGET /api/sessions/:id- Get specific session detailsDELETE /api/sessions/:id- Archive session and cleanup worktree
POST /api/sessions/:id/input- Send input to Claude Code instancePOST /api/sessions/:id/continue- Continue conversation with full historyGET /api/sessions/:id/output- Retrieve session output historyGET /api/sessions/:id/conversation- Get conversation message history
GET /api/config- Get current application configurationPOST /api/config- Update configuration settings
GET /api/projects- List all projectsPOST /api/projects- Create new project (with automatic directory/git init)GET /api/projects/:id- Get project detailsPUT /api/projects/:id- Update project settingsPOST /api/projects/:id/activate- Set active projectDELETE /api/projects/:id- Delete project
GET /api/prompts- Get all prompts with associated sessionsGET /api/prompts/:sessionId/:lineNumber- Navigate to specific prompt
- Session Creation: User provides prompt and worktree template via dialog
- Worktree Setup: Backend creates new git worktree using
git worktree add - Claude Instance: Spawns Claude Code process in worktree using node-pty
- Database Storage: Session metadata and output stored in SQLite
- Real-time Updates: IPC streams session status and terminal output
- Session Management: Users can switch between sessions, continue conversations
# One-time setup (install, build, and rebuild native modules)
pnpm run setup
# Run as Electron app in development mode
pnpm electron-dev
# Or use the shorthand:
pnpm run dev
# Run frontend only (without Electron shell)
pnpm dev
# Build for production
pnpm build
# Type checking
pnpm typecheck
# Linting
pnpm lintNote: You must run pnpm run build:main at least once before running pnpm electron-dev to compile the main process.
# Build for macOS
pnpm build:mac # macOS (only works on macOS)crystal/
├── frontend/ # React renderer process
│ ├── src/
│ │ ├── components/ # React components
│ │ │ ├── Help.tsx # Help dialog
│ │ │ └── ... # Other UI components
│ │ ├── hooks/ # Custom React hooks
│ │ │ └── useSessionView.ts # Session view logic (941 lines)
│ │ ├── stores/ # Zustand state stores
│ │ └── utils/ # Utility functions
├── main/ # Electron main process
│ ├── src/
│ │ ├── index.ts # Main entry point (reduced to 414 lines)
│ │ ├── preload.ts # Preload script
│ │ ├── events.ts # Event handling (359 lines)
│ │ ├── database/ # SQLite database
│ │ ├── ipc/ # IPC handlers (modular)
│ │ │ ├── git.ts # Git operation handlers (843 lines)
│ │ │ ├── session.ts # Session operation handlers (428 lines)
│ │ │ └── ... # Other IPC handlers
│ │ ├── services/ # Business logic services
│ │ │ ├── taskQueue.ts # Bull queue for async tasks
│ │ │ └── ... # Other service modules
│ │ ├── routes/ # API routes
│ │ └── types/ # TypeScript types
│ └── dist/ # Compiled output
├── shared/ # Shared TypeScript types
├── dist-electron/ # Packaged Electron app
├── package.json # Root workspace configuration
└── pnpm-workspace.yaml
- Create/Select Project: Choose a project directory or create a new one
- Create Session: Click "Create Session" and enter a prompt
- Parallel Sessions: Run multiple sessions for different approaches
- View Results: Switch between Output, View Diff, and Terminal views
- Click the ? button in the sidebar to open the comprehensive help dialog
- The help dialog covers all features, keyboard shortcuts, and tips
- 🟢 Initializing: Setting up git worktree
- 🟢 Running: Claude is actively processing
- 🟡 Waiting: Needs your input
- ⚪ Completed: Task finished successfully
- 🔵 New Activity: Session has new unviewed results
- 🔴 Error: Something went wrong
- Rebase from main: Updates your worktree with latest main branch changes
- Squash and rebase: Combines all commits and rebases onto main
- Always preview commands with tooltips before executing
- Use descriptive prompts for better AI-generated session names
- Create multiple sessions to explore different solutions
- Review View Diff tab before git operations
- Use Terminal tab to run tests after changes
- Archive completed sessions to keep the list manageable
- Set up project-specific prompts for consistency
- Session won't start: Check if git repository is initialized
- Git operations fail: Ensure no uncommitted changes conflict
- Terminal not responding: Check if Claude Code is installed correctly
- Notifications not working: Grant permission when prompted
Enable verbose logging in Settings to see detailed logs for troubleshooting.
In development mode, Crystal automatically captures all frontend console logs and writes them to a file that Claude Code can read for debugging purposes.
Location: crystal-frontend-debug.log in the project root directory
What gets logged:
- All
console.log,console.warn,console.error,console.info, andconsole.debugcalls from the React frontend - Automatic console messages from Electron's webContents (warnings, errors)
- Timestamp, log level, and source information for each message
Example log format:
[2024-07-21T15:30:45.123Z] [RENDERER LOG] User clicked session creation button
[2024-07-21T15:30:45.234Z] [FRONTEND WARNING] Session validation failed: missing prompt
[2024-07-21T15:30:45.345Z] [FRONTEND ERROR] Failed to create session (SessionView.tsx:123)
Usage for Claude Code debugging:
- Add debug console.log statements to frontend components
- Reproduce the issue in the Crystal app
- Read
crystal-frontend-debug.logto see what happened - No need to manually check Chrome DevTools or ask humans to copy logs
File rotation: The log file grows continuously during development. Delete or truncate it manually if it gets too large.
Note: This feature is only active in development mode and will not affect production builds.
Crystal is an independent project created by Stravu. Claude™ is a trademark of Anthropic, PBC. Crystal is not affiliated with, endorsed by, or sponsored by Anthropic. This tool is designed to work with Claude Code, which must be installed separately.
Do what has been asked; nothing more, nothing less. NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.