# Parallel State Manager: Add Without Breaking
## The Problem
Even a "shell" state manager requires changing all 57 `set_mode()` calls. That's a big bang!
## The Solution: Run in Parallel
Add the StateManager **alongside** existing state, not replacing it. The existing code continues to work while we observe and learn.
## Phase 1: Shadow State Manager
```rust
/// Shadows the existing state system - doesn't control anything yet
pub struct ShadowStateManager {
state: AppState,
history: VecDeque<(Instant, AppState, String)>,
// Track discrepancies between our state and actual state
discrepancies: Vec<String>,
}
impl ShadowStateManager {
/// Called AFTER the existing set_mode() - just observes
pub fn observe_mode_change(&mut self, mode: AppMode, trigger: &str) {
let new_state = self.mode_to_state(mode);
info!(target: "shadow_state",
"Observed: {:?} -> {:?} ({})",
self.state, new_state, trigger
);
self.state = new_state;
self.history.push_back((Instant::now(), self.state.clone(), trigger.to_string()));
}
/// Called when we observe search state changes
pub fn observe_search_start(&mut self, search_type: &str) {
info!(target: "shadow_state", "Observed search start: {}", search_type);
// Update our shadow state based on observation
}
/// Check if our state matches reality
pub fn verify_state(&mut self, actual: &ActualState) {
if !self.matches_actual(actual) {
let msg = format!(
"State mismatch! Shadow: {:?}, Actual: {:?}",
self.state, actual
);
warn!(target: "shadow_state", "{}", msg);
self.discrepancies.push(msg);
}
}
/// Get what we THINK the state should be
pub fn predicted_state(&self) -> &AppState {
&self.state
}
}
```
## Integration: Minimal Touch Points
### Step 1: Add to EnhancedTuiApp
```rust
pub struct EnhancedTuiApp {
// ... existing fields unchanged
#[cfg(feature = "shadow-state")]
shadow_state: ShadowStateManager, // NEW - only when testing
}
```
### Step 2: Add observation calls (not replacement!)
```rust
// EXISTING CODE UNCHANGED:
self.buffer_mut().set_mode(AppMode::Results);
// ADD AFTER (doesn't affect existing):
#[cfg(feature = "shadow-state")]
self.shadow_state.observe_mode_change(AppMode::Results, "execute_query");
```
### Step 3: Add verification in render
```rust
fn render_status_line(&self, f: &mut Frame, area: Rect) {
// ... existing rendering code ...
#[cfg(feature = "shadow-state")]
{
// Show shadow state in status line for comparison
let shadow_display = format!("[Shadow: {}]",
self.shadow_state.predicted_state());
// Render it in corner for debugging
}
}
```
## Even More Incremental: Wrapper Pattern
```rust
/// Wraps existing buffer to intercept state changes
pub struct StateTrackingBuffer<'a> {
inner: &'a mut Buffer,
state_tracker: &'a mut ShadowStateManager,
}
impl<'a> StateTrackingBuffer<'a> {
pub fn set_mode(&mut self, mode: AppMode) {
// Call the real implementation
self.inner.set_mode(mode.clone());
// Track the state change
self.state_tracker.observe_mode_change(mode, "wrapped_call");
}
// Delegate everything else unchanged
pub fn get_mode(&self) -> AppMode {
self.inner.get_mode()
}
}
// Usage - wrap only where we want to observe:
let mut tracking_buffer = StateTrackingBuffer {
inner: self.buffer_mut(),
state_tracker: &mut self.shadow_state,
};
tracking_buffer.set_mode(AppMode::Results);
```
## Incremental Migration Path
### Phase 1: Pure Observation (No Risk!)
1. Add `ShadowStateManager` with feature flag
2. Add ~5 observation points at key locations:
- Execute query
- Start search
- Exit search
- Return to command
- Switch modes
3. Run and observe logs - learn the patterns
4. No functionality changes - can't break anything!
### Phase 2: Verification (Find Discrepancies)
1. Add verification checks
2. Compare shadow state with actual state
3. Log mismatches to understand missing transitions
4. Still no functionality changes
### Phase 3: Single Source Experiment
1. Pick ONE feature (like the N key mapping)
2. Use shadow state for just that decision:
```rust
let should_search = if cfg!(feature = "use-shadow-state") {
self.shadow_state.is_search_active() } else {
!buffer.get_search_pattern().is_empty()
|| self.vim_search_manager.borrow().is_active()
|| self.state_container.column_search().is_active
};
```
3. If it works, expand usage
4. If not, feature flag off!
### Phase 4: Gradual Takeover
1. One by one, switch decisions to use shadow state
2. Each behind a feature flag initially
3. Once all decisions use shadow state, make it primary
4. Remove old state code
## Actual Starting Code
```rust
// src/ui/shadow_state.rs - NEW FILE
use crate::buffer::AppMode;
#[derive(Debug, Clone)]
pub enum AppState {
Command,
Results,
Search,
// Start simple!
}
pub struct ShadowStateManager {
state: AppState,
transition_count: usize,
}
impl ShadowStateManager {
pub fn new() -> Self {
Self {
state: AppState::Command,
transition_count: 0,
}
}
pub fn observe(&mut self, mode: AppMode) {
let new_state = match mode {
AppMode::Command => AppState::Command,
AppMode::Results => AppState::Results,
AppMode::Search | AppMode::ColumnSearch => AppState::Search,
_ => return, // Ignore others for now
};
if !matches!((&self.state, &new_state), (AppState::Search, AppState::Search)) {
self.transition_count += 1;
info!(target: "shadow",
"[#{}] {} -> {}",
self.transition_count,
format!("{:?}", self.state),
format!("{:?}", new_state)
);
}
self.state = new_state;
}
pub fn is_search(&self) -> bool {
matches!(self.state, AppState::Search)
}
}
```
Then add just ONE line after existing code:
```rust
self.buffer_mut().set_mode(AppMode::Results);
#[cfg(feature = "shadow-state")]
self.shadow_state.observe(AppMode::Results); // Just observe!
```
## Benefits of This Approach
1. **Zero Risk**: Observation doesn't change behavior
2. **Learn First**: Understand patterns before changing
3. **Feature Flags**: Turn off instantly if issues
4. **Incremental**: Each step is tiny and reversible
5. **Parallel Running**: Old and new side by side
## The Absolutely Minimal Start
1. Create `shadow_state.rs` with basic enum
2. Add `ShadowStateManager` to app struct (feature flagged)
3. Add ONE observe call after execute_query
4. Log and watch
5. Add one more observe call
6. Repeat
This way we never need a big bang - we're just adding logging alongside existing code!