Documentation Index
Fetch the complete documentation index at: https://docs.cedarcopilot.com/llms.txt
Use this file to discover all available pages before exploring further.
Spell Architecture
This guide provides a deep dive into the technical architecture of Cedar-OS’s spell system, explaining how spells work under the hood and how the various components interact.
Architecture Overview
The spell system consists of three main components working in harmony:
- useSpell Hook - React interface for components
- SpellSlice - State management and coordination
- SpellActivationManager - Event handling and routing
Component Deep Dive
1. SpellActivationManager
The SpellActivationManager is a singleton class that manages all low-level event handling for spells. It’s the bridge between DOM events and spell activations.
Key Responsibilities
- Centralized Event Listening: Maintains a single set of event listeners for all spells
- Event Routing: Routes events to the appropriate registered spells
- Activation Logic: Handles different activation modes (toggle, hold, trigger)
- Performance Optimization: Prevents duplicate listeners and manages cleanup
How It Works
class SpellActivationManager {
// Singleton pattern ensures one instance
private static instance: SpellActivationManager;
// Map of spell IDs to their configurations
private registrations = new Map<string, SpellRegistration>();
// Bound event handlers for proper cleanup
private boundHandlers = {
keydown: this.handleKeyDown.bind(this),
keyup: this.handleKeyUp.bind(this),
// ... other handlers
};
}
The manager listens to various DOM events and processes them:
- Event Detection: Captures keyboard, mouse, and selection events
- Spell Matching: Checks which registered spells should respond
- State Management: Tracks held keys, cooldowns, and active states
- Callback Execution: Triggers appropriate spell callbacks
Event Processing Flow
2. SpellSlice
The SpellSlice is a Zustand store slice that provides high-level spell management and integrates with Cedar’s state system.
Core State Structure
interface SpellSlice {
// Map of spell configurations and states
spells: Partial<SpellMap>;
// Registration and management methods
registerSpell: (registration: SpellRegistration) => void;
unregisterSpell: (spellId: string) => void;
activateSpell: (spellId: string, triggerData?: any) => void;
deactivateSpell: (spellId: string) => void;
toggleSpell: (spellId: string) => void;
clearSpells: () => void;
}
Integration Points
The SpellSlice acts as the coordination layer:
- Store Integration: Provides access to Cedar store for spell callbacks
- Lifecycle Management: Handles spell registration/unregistration
- State Synchronization: Keeps UI state in sync with activation manager
- Programmatic Control: Enables manual spell activation/deactivation
Registration Flow
When a spell is registered:
registerSpell: (registration) => {
// 1. Add to store state
set((state) => ({
spells: {
...state.spells,
[id]: { isActive: false, registration },
},
}));
// 2. Register with activation manager
manager.register(id, conditions, {
onActivate: (state) => {
// Update store state
// Call user callback
},
onDeactivate: () => {
// Update store state
// Call user callback
},
});
};
3. useSpell Hook
The useSpell hook provides the React-friendly interface for components to use spells.
Hook Lifecycle
function useSpell(options: UseSpellOptions) {
// 1. Get store methods
const { registerSpell, unregisterSpell, ... } = useSpells();
// 2. Use refs for callbacks to avoid re-registration
const onActivateRef = useRef(options.onActivate);
const onDeactivateRef = useRef(options.onDeactivate);
// 3. Register on mount, re-register on condition changes
useEffect(() => {
registerSpell({
id: options.id,
activationConditions: options.activationConditions,
onActivate: (state) => onActivateRef.current?.(state),
onDeactivate: () => onDeactivateRef.current?.()
});
// 4. Cleanup on unmount
return () => {
unregisterSpell(options.id);
};
}, [/* dependencies */]);
// 5. Return current state and controls
return { isActive, activate, deactivate, toggle };
}
- Ref-based Callbacks: Prevents unnecessary re-registrations
- Dependency Tracking: Only re-registers when conditions change
- Automatic Cleanup: Ensures no memory leaks
Activation Modes Explained
Toggle Mode
- User presses key/button → spell activates
- User presses again → spell deactivates
- Good for persistent UI elements
Hold Mode
- Activates on keydown/mousedown
- Deactivates on keyup/mouseup
- Perfect for temporary overlays
Trigger Mode
- Fires once when triggered
- Auto-deactivates after brief period
- Optional cooldown prevents spam
Event Processing Details
Keyboard Event Handling
The system handles both single keys and combinations:
private matchesHotkey(event: KeyboardEvent, hotkey: string): boolean {
if (hotkey.includes('+')) {
// Combination like "ctrl+k"
const combo = this.parseHotkeyCombo(hotkey);
return event.key === combo.key &&
event.ctrlKey === combo.modifiers.ctrl &&
event.metaKey === combo.modifiers.meta;
} else {
// Single key - no unexpected modifiers
return event.key.toLowerCase() === hotkey.toLowerCase() &&
!event.ctrlKey && !event.metaKey && !event.altKey;
}
}
Mouse Event Handling
Mouse events include position tracking:
private handleContextMenu(event: MouseEvent): void {
for (const registration of this.registrations.values()) {
if (mouseEvents.includes(RIGHT_CLICK)) {
this.activateSpell(registration, {
type: 'mouse',
event: RIGHT_CLICK,
mousePosition: { x: event.clientX, y: event.clientY },
originalEvent: event
});
}
}
}
Selection Event Handling
Text selection is debounced for performance:
private handleSelectionChange(): void {
// Clear previous timeout
if (this.selectionTimeout) {
clearTimeout(this.selectionTimeout);
}
// Debounce selection events
this.selectionTimeout = setTimeout(() => {
const selection = window.getSelection();
const selectedText = selection?.toString().trim();
if (selectedText && selectedText.length > 0) {
// Activate selection-based spells
}
}, 200); // 200ms debounce
}
Event Listener Management
The system uses a single set of listeners for all spells:
// ✅ Efficient: One listener for all spells
window.addEventListener('keydown', this.boundHandlers.keydown);
// ❌ Inefficient: Listener per spell
spells.forEach((spell) => {
window.addEventListener('keydown', spell.handler);
});
Memory Management
- Singleton Pattern: One manager instance for entire app
- Automatic Cleanup: Listeners removed when no spells registered
- Ref-based Callbacks: Prevents closure memory leaks
React Integration
The hook optimizes React re-renders:
// Dependencies are stringified for deep comparison
useEffect(() => {
registerSpell({...});
}, [
options.id,
JSON.stringify(options.activationConditions), // Deep comparison
options.preventDefaultEvents,
options.ignoreInputElements
]);
Advanced Features
Spells can be configured to ignore input elements:
private isInputElement(target: EventTarget | null): boolean {
if (!ignoreInputElements) return false;
return target.closest('input, textarea, [contenteditable="true"]') !== null;
}
Cooldown System
Prevents rapid re-triggering:
if (mode === ActivationMode.TRIGGER) {
const now = Date.now();
if (now - registration.lastTriggerTime < cooldown) {
return; // Still on cooldown
}
registration.lastTriggerTime = now;
}
Multi-Event Support
Spells can respond to multiple triggers:
activationConditions: {
events: [Hotkey.SPACE, MouseEvent.RIGHT_CLICK, 'ctrl+enter'];
}
// Any of these will activate the spell
Data Flow Diagram
1. Use Appropriate Activation Modes
// Hold mode for temporary UI
mode: ActivationMode.HOLD; // Auto-cleanup on release
// Toggle for persistent features
mode: ActivationMode.TOGGLE; // User controls lifecycle
// Trigger for one-time actions
mode: ActivationMode.TRIGGER; // Auto-cleanup after execution
2. Optimize Spell IDs
// Use stable IDs to prevent re-registration
const SPELL_ID = 'my-feature-spell'; // Constant
// Avoid dynamic IDs that change
const spellId = `spell-${Date.now()}`; // ❌ Changes every render
3. Leverage Refs for Callbacks
// The hook already does this, but for custom implementations:
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
4. Consider Event Delegation
For many similar spells, consider a single parent spell:
// Instead of 100 tooltip spells
items.map(item => <TooltipSpell key={item.id} />)
// Use one spell with delegation
<TooltipManagerSpell items={items} />
Debugging Spells
Enable Debug Logging
const { isActive } = useSpell({
id: 'debug-spell',
onActivate: (state) => {
console.log('Spell activated:', {
triggerData: state.triggerData,
timestamp: Date.now(),
});
},
});
Monitor Registration
// Check registered spells in Cedar store
const { spells } = useCedarStore();
console.log('Active spells:', Object.keys(spells));
Track Event Flow
Add logging to understand event propagation:
onActivate: (state) => {
console.log('Trigger:', state.triggerData?.event);
console.log('Mouse position:', state.triggerData?.mousePosition);
console.log('Original event:', state.triggerData?.originalEvent);
};
Summary
The spell architecture provides a robust, performant system for gesture-based interactions:
- Centralized Management: Single manager handles all events efficiently
- React Integration: Hooks provide clean component interface
- State Synchronization: Zustand slice keeps everything in sync
- Performance Optimized: Singleton pattern, ref-based callbacks, debouncing
- Flexible Activation: Multiple modes and event types supported
This architecture ensures spells are both powerful for developers and performant for users, enabling truly magical interactions in your Cedar-OS applications.