Memento-based undo/redo with full encounter snapshots. Undo stack capped at 50 entries, persisted to localStorage. Triggered via buttons in the top bar (inboard of turn navigation) and keyboard shortcuts (Ctrl+Z / Ctrl+Shift+Z, Cmd on Mac, case-insensitive key matching). Clear encounter resets both stacks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10 KiB
Tasks: Undo/Redo
Input: Design documents from /specs/037-undo-redo/
Prerequisites: plan.md, spec.md, research.md, data-model.md, quickstart.md
Tests: Domain tests included (pure function testing is standard for this project per CLAUDE.md).
Organization: Tasks are grouped by user story to enable independent implementation and testing of each story.
Format: [ID] [P?] [Story] Description
- [P]: Can run in parallel (different files, no dependencies)
- [Story]: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions
Phase 1: Foundational (Domain + Application Layer)
Purpose: Pure domain logic and application ports that all user stories depend on
CRITICAL: No user story work can begin until this phase is complete
- T001 Define
UndoRedoStatetype and pure stack functions (pushUndo,undo,redo,clearHistory) inpackages/domain/src/undo-redo.ts. All functions take and return immutable data per data-model.md state transitions. Cap undo stack at 50 entries (drop oldest). ReturnDomainErrorfor empty-stack operations. - T002 Add unit tests for all stack functions in
packages/domain/src/__tests__/undo-redo.test.ts. Cover: push adds to stack, cap at 50 drops oldest, push clears redo stack, undo pops and moves to redo, redo pops and moves to undo, undo on empty returns error, redo on empty returns error, clearHistory empties both, undo-then-redo roundtrip returns exact same encounter. Application use case tests are not needed separately — the use cases are thin orchestration and their logic is fully covered by domain tests + integration via the hook. - T003 [P] Add
UndoRedoStoreport interface (get(): UndoRedoState,save(state: UndoRedoState): void) topackages/application/src/ports.ts. - T004 [P] Implement
undoUseCase(encounterStore, undoRedoStore)inpackages/application/src/undo-use-case.ts. Calls domainundo()with current encounter, saves resulting encounter to encounterStore and resulting state to undoRedoStore. - T005 [P] Implement
redoUseCase(encounterStore, undoRedoStore)inpackages/application/src/redo-use-case.ts. Same pattern as undo but calls domainredo().
Checkpoint: Domain logic and use cases complete. All pure functions tested. Ready for adapter layer.
Phase 2: User Story 1 - Undo a Mistake (Priority: P1) MVP
Goal: User can undo any encounter action via a button in the top bar.
Independent Test: Perform any encounter action (add combatant, adjust HP, advance turn), click Undo, verify encounter returns to pre-action state. Undo button is disabled when stack is empty.
Implementation for User Story 1
- T006 [US1] Add undo/redo state management to
apps/web/src/hooks/use-encounter.ts: addUndoRedoStateto hook state (initialized empty), createmakeUndoRedoStore()factory (same pattern asmakeStore()), create awithUndowrapper function that captures the pre-action encounter snapshot and callspushUndoon the undo/redo state after a successful action. Wrap all existing action callbacks withwithUndo. - T007 [US1] Add
undocallback toapps/web/src/hooks/use-encounter.tsthat callsundoUseCaseand updates both encounter and undo/redo state. ExposecanUndo: boolean(derived from undo stack length > 0). - T008 [US1] Update
apps/web/src/contexts/encounter-context.tsxto exposeundo,canUndofrom the encounter hook return type. - T009 [US1] Add Undo button to
apps/web/src/components/turn-navigation.tsx, placed inboard of (to the right of) the Previous Turn button. UseUndo2icon from Lucide React. Button is disabled whencanUndois false. Callsundo()from encounter context on click.
Checkpoint: Undo works for all encounter actions via button. Redo not yet available.
Phase 3: User Story 2 - Redo an Undone Action (Priority: P2)
Goal: User can redo an undone action via a button. New actions clear the redo stack.
Independent Test: Perform an action, undo it, click Redo, verify state matches post-action. Then perform a new action and verify Redo button becomes disabled.
Implementation for User Story 2
- T010 [US2] Add
redocallback toapps/web/src/hooks/use-encounter.tsthat callsredoUseCaseand updates both encounter and undo/redo state. ExposecanRedo: boolean(derived from redo stack length > 0). Verify thatpushUndo(called bywithUndowrapper from T006) already clears the redo stack per domain logic — no additional work needed for FR-005. - T011 [US2] Update
apps/web/src/contexts/encounter-context.tsxto exposeredo,canRedofrom the encounter hook return type. - T012 [US2] Add Redo button to
apps/web/src/components/turn-navigation.tsx, placed next to the Undo button (both inboard of turn step buttons). UseRedo2icon from Lucide React. Button is disabled whencanRedois false. Callsredo()from encounter context on click.
Checkpoint: Full undo/redo via buttons. Keyboard shortcuts and persistence not yet available.
Phase 4: User Story 3 - Keyboard Shortcuts (Priority: P3)
Goal: Ctrl+Z / Cmd+Z triggers undo; Ctrl+Shift+Z / Cmd+Shift+Z triggers redo. Suppressed when text input has focus.
Independent Test: Press Ctrl+Z with no input focused — encounter undoes. Focus an input field, press Ctrl+Z — browser native text undo fires. Press Ctrl+Shift+Z — encounter redoes.
Implementation for User Story 3
- T013 [US3] Create
apps/web/src/hooks/use-undo-redo-shortcuts.ts. Register akeydownevent listener ondocument. Detect Ctrl+Z / Cmd+Z (undo) and Ctrl+Shift+Z / Cmd+Shift+Z (redo). Before dispatching, checkdocument.activeElement— suppress if tag isINPUT,TEXTAREA,SELECT, or element hascontentEditable. CallpreventDefault()only when handling the shortcut. Acceptundo,redo,canUndo,canRedoas parameters. - T014 [US3] Wire
useUndoRedoShortcutsinto the encounter provider layer. Call the hook from insideEncounterProviderinapps/web/src/contexts/encounter-context.tsx(or fromApp.tsxif context structure makes that cleaner), passing undo/redo callbacks and flags from the encounter hook.
Checkpoint: Full undo/redo via buttons and keyboard shortcuts.
Phase 5: User Story 4 - Undo History Survives Refresh (Priority: P4)
Goal: Undo/redo stacks persist to localStorage and restore on page load.
Independent Test: Perform 5 actions, refresh the page, verify undo button is enabled and clicking it 5 times restores each previous state.
Implementation for User Story 4
- T015 [P] [US4] Create
apps/web/src/persistence/undo-redo-storage.ts. ImplementsaveUndoRedoStacks(undoStack, redoStack)andloadUndoRedoStacks(). Use localStorage keys"initiative:encounter:undo"and"initiative:encounter:redo". Reuse existing encounter rehydration/validation logic fromencounter-storage.tsfor each stack entry. Silently swallow write errors (quota exceeded). Return empty stacks on corrupt/invalid data. - T016 [US4] Integrate persistence into
apps/web/src/hooks/use-encounter.ts: initialize undo/redo state fromloadUndoRedoStacks()on mount. Add auseEffectthat callssaveUndoRedoStacks()whenever undo/redo state changes (same pattern as existing encounter persistence).
Checkpoint: Full feature complete — undo/redo via buttons, keyboard shortcuts, persisted across refresh.
Phase 6: Polish & Cross-Cutting Concerns
Purpose: Edge cases and quality gates
- T017 Ensure clear encounter resets undo/redo stacks in
apps/web/src/hooks/use-encounter.ts. In theclearEncountercallback, callclearHistory()on the undo/redo state after clearing the encounter. Verify this also clears persisted stacks via the useEffect. - T018 Run
pnpm checkand fix any lint, type, coverage, or unused-code issues. Ensure layer boundary check passes (domain must not import from web/application, application must not import from web). - T019 Update README.md to document undo/redo capability (buttons + keyboard shortcuts). Per constitution, user-facing feature changes MUST be reflected in README.
Dependencies & Execution Order
Phase Dependencies
- Foundational (Phase 1): No dependencies — can start immediately
- US1 Undo (Phase 2): Depends on Foundational completion
- US2 Redo (Phase 3): Depends on US1 (undo must exist before redo makes sense)
- US3 Shortcuts (Phase 4): Depends on US2 (needs both undo and redo callbacks)
- US4 Persistence (Phase 5): Depends on US1 (needs undo/redo state to exist). Can run in parallel with US2/US3 if needed.
- Polish (Phase 6): Depends on all user stories being complete
Within Foundational Phase
- T001 (domain functions) must complete before T002 (tests)
- T003 (port) must complete before T004/T005 (use cases import
UndoRedoStorefrom ports.ts) - T004 (undo use case) and T005 (redo use case) can run in parallel after T001 + T003
Parallel Opportunities
- T004, T005 can run in parallel (different files, depend on T001 + T003)
- T015 (persistence storage) can run in parallel with any Phase 3-4 work (different file)
Implementation Strategy
MVP First (User Story 1 Only)
- Complete Phase 1: Foundational (domain + application)
- Complete Phase 2: User Story 1 (undo via button)
- STOP and VALIDATE: Test undo independently with all encounter actions
- Demo if ready — undo alone delivers significant value
Incremental Delivery
- Foundational → Domain logic tested, ready for integration
- Add US1 (Undo) → Button works → Validate independently
- Add US2 (Redo) → Both buttons work → Validate independently
- Add US3 (Shortcuts) → Keyboard works → Validate independently
- Add US4 (Persistence) → Refresh-safe → Validate independently
- Polish → Quality gates pass → Ready to merge
Notes
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability
- US2 (Redo) depends on US1 (Undo) — redo is meaningless without undo
- US3 (Shortcuts) and US4 (Persistence) are independent of each other but both need US1
- The
withUndowrapper in T006 is the key integration point — it captures snapshots for ALL existing actions in one place - Domain tests (T002) validate all invariants from data-model.md
- Commit after each phase checkpoint