# Research: Undo/Redo for Encounter Actions **Feature**: 006-undo-redo **Date**: 2026-03-26 ## Decision 1: Undo/Redo Strategy — Memento (Snapshots) vs Command (Events) **Decision**: Memento pattern — store full `Encounter` snapshots. **Rationale**: - The `Encounter` type is small (tens of combatants, ~2-5 KB serialized). Storing 50 snapshots costs ~100-250 KB in memory and localStorage — negligible. - The codebase already serializes/deserializes full encounters for localStorage persistence. Reuse is straightforward. - All domain state transitions are pure functions returning new `Encounter` objects. Each action naturally produces a "before" snapshot (the current state) and an "after" snapshot (the result). - The command/event approach would require inverse operations for every domain function, including compound operations like initiative re-sorting. This complexity is not justified at encounter scale. - The existing event system captures `previousValue`/`newValue` pairs but lacks full combatant snapshots for structural changes (add/remove with initiative reordering). Extending events would be more work than snapshots for no practical benefit. **Alternatives considered**: - **Command pattern (inverse events)**: More memory-efficient per entry but significantly more complex. Requires implementing and testing inverse operations for all 18+ domain transitions. Rejected because the complexity outweighs the memory savings at encounter scale. - **Hybrid (events for simple, snapshots for structural)**: Rejected because mixed strategies increase implementation and debugging complexity. ## Decision 2: Stack Storage Location **Decision**: Store undo/redo stacks in React state within `useEncounter`, persisted to localStorage via dedicated keys. **Rationale**: - Matches the existing persistence pattern: encounter state lives in React state and is synced to localStorage via `useEffect`. - Using `useState` (not `useRef`) ensures React re-renders when stack emptiness changes, keeping button disabled states reactive. - Dedicated localStorage keys (`"initiative:encounter:undo"`, `"initiative:encounter:redo"`) avoid coupling stack persistence with encounter persistence. **Alternatives considered**: - **useRef for stacks**: Would avoid re-renders on every push/pop, but then button disabled states wouldn't update reactively. Would need manual `forceUpdate` or separate boolean state — more complex for no clear benefit. - **Single localStorage key with encounter**: Rejected because it couples concerns and makes the encounter storage format backward-incompatible. ## Decision 3: Snapshot Capture Point **Decision**: Capture the pre-action encounter snapshot inside each action callback in `useEncounter`, before calling the use case. **Rationale**: - Each action callback in `useEncounter` already calls `makeStore()` which accesses the current encounter via `encounterRef.current`. The snapshot is naturally available at this point. - Capturing at the hook level (not the use case level) keeps the domain and application layers unchanged — undo/redo is purely an adapter concern. - Failed actions (domain errors) should NOT push to the undo stack, so the capture must happen conditionally after confirming the action succeeded. **Alternatives considered**: - **Capture inside use cases**: Would require changing the application layer API to return the pre-action state. Violates layered architecture — use cases shouldn't know about undo. - **Capture via store wrapper**: Could intercept `store.save()` to capture the previous state. Elegant but makes the flow harder to follow and debug. Rejected in favor of explicit capture. ## Decision 4: Keyboard Shortcut Suppression Strategy **Decision**: Check `document.activeElement` tag name and `contentEditable` attribute. Suppress encounter undo/redo when focus is on `INPUT`, `TEXTAREA`, or `contentEditable` elements. **Rationale**: - Simple and reliable. The app uses standard HTML form elements for text input. - No `contentEditable` elements currently exist, but checking for them is defensive and low-cost. - The check happens in the `keydown` event handler before dispatching to undo/redo. **Alternatives considered**: - **Capture phase with stopPropagation**: Overly complex for this use case. - **Custom focus tracking via context**: Would require every input to register/unregister. Too invasive. ## Decision 5: UI Placement for Undo/Redo Buttons **Decision**: Place undo/redo buttons in the `TurnNavigation` component (top bar), inboard of the turn step buttons. Turn navigation (Previous/Next Turn) stays as the outermost buttons; Undo/Redo sits between Previous Turn and the center info area. **Rationale**: - `TurnNavigation` is the primary command bar for encounter-level actions (advance/retreat turn, clear encounter). Undo/redo are encounter-level actions. - Placing them in the top bar keeps them always visible when the encounter is active. - Turn navigation stays outermost because it's the most frequently used control during live combat. Undo/redo is secondary. **Alternatives considered**: - **ActionBar (bottom bar)**: Already crowded with combatant management controls (search, add, roll initiative). Undo/redo would be buried. - **Floating buttons**: Unconventional for this app's design language. ## Decision 6: Clear Encounter and Undo Stack **Decision**: Clearing the encounter resets both undo and redo stacks. Clear is not undoable. **Rationale**: - Per spec (FR-010 and edge case). Clear is an intentionally destructive "reset" action. Making it undoable would create confusion about what "fresh start" means. - The existing clear encounter flow already has a confirmation dialog, providing sufficient protection against accidents. **Alternatives considered**: - **Make clear undoable**: Would require keeping the pre-clear state in a special recovery slot. Adds complexity for a scenario already guarded by confirmation. Rejected per spec.