State Management (app/state/)
Manages application state persistence, synchronization between UI and engine, and git-based undo/redo.
For the comprehensive deep-dive into state management, see documentation/state-management.md.
Architecture
Files
| File | Purpose |
|---|---|
StateSync.cpp | Bidirectional Zustand ↔ Tracktion Engine state sync. handleStateUpdate() dispatches incoming state, applyMixerState() writes to engine, describeMixerChange() generates human-readable commit messages. |
SessionState.cpp | Session-specific state handling: transport, chat, and Lyria state (gitignored — not part of undo/redo). |
ProjectState.cpp/.h | Git-based undo/redo using libgit2. commit(), undo(), redo(), revertLastLLM(), getHistory(). Operates on an in-process git repository — no fork, no exec. |
ValueTreeJSON.h | Helper utilities for converting JUCE ValueTrees to/from JSON. |
Git Undo/Redo
Uses libgit2 for zero-overhead in-process git operations:refs/heads/main— current position (HEAD), moves forward on commit, backward on undorefs/redo-tip— created on first undo, points to the newest undone commit (deleted when a new commit is made, invalidating the redo chain)
[auto] (system), [mixer] (fader/knob), [LLM] (AI copilot), [user] (manual save).
Echo Prevention
Seven mechanisms prevent feedback loops when state crosses the React ↔ C++ boundary:- String comparison —
handleStateUpdate()skips if incoming JSON == cached JSON - JSON normalization — round-trip through JUCE parser after commit so echoes match
undoRedoInProgressflag — blocks mixer commits during undo/redo (cleared after 200ms)isLoadFinishedgate — blocks ALL mixer commits until initial project load completeshasUncommittedChanges()guard — skip commit if git working tree is cleansuppressMixerEchoflag — blocksTrackStateWatcherduringapplyMixerState()- Integer rounding — volume/pan rounded to integers to avoid float precision diffs
Design Principles
- Git-tracked vs session — Only
daw.bird,daw.state.json, anddaw.edit.jsonparticipate in undo/redo. Session state (daw.session.json) is ephemeral and gitignored. - Commit gating — Never commit before
isLoadFinishedis true. The loading sequence must complete (plugins settle, React hydrates) before commits are enabled. - Debounced session saves — Non-mixer stores trigger a 500ms debounce timer before writing to disk (transport/chat changes don’t need instant persistence).
- Descriptive commits —
describeMixerChange()generates readable messages like'drums' vol 80→65by diffing old vs new JSON. This powers the HistoryPanel UI.