Files
initiative/specs/037-undo-redo/tasks.md
Lukas 17cc6ed72c Add undo/redo for all encounter actions
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>
2026-03-26 23:30:33 +01:00

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 UndoRedoState type and pure stack functions (pushUndo, undo, redo, clearHistory) in packages/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). Return DomainError for 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 UndoRedoStore port interface (get(): UndoRedoState, save(state: UndoRedoState): void) to packages/application/src/ports.ts.
  • T004 [P] Implement undoUseCase(encounterStore, undoRedoStore) in packages/application/src/undo-use-case.ts. Calls domain undo() with current encounter, saves resulting encounter to encounterStore and resulting state to undoRedoStore.
  • T005 [P] Implement redoUseCase(encounterStore, undoRedoStore) in packages/application/src/redo-use-case.ts. Same pattern as undo but calls domain redo().

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: add UndoRedoState to hook state (initialized empty), create makeUndoRedoStore() factory (same pattern as makeStore()), create a withUndo wrapper function that captures the pre-action encounter snapshot and calls pushUndo on the undo/redo state after a successful action. Wrap all existing action callbacks with withUndo.
  • T007 [US1] Add undo callback to apps/web/src/hooks/use-encounter.ts that calls undoUseCase and updates both encounter and undo/redo state. Expose canUndo: boolean (derived from undo stack length > 0).
  • T008 [US1] Update apps/web/src/contexts/encounter-context.tsx to expose undo, canUndo from 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. Use Undo2 icon from Lucide React. Button is disabled when canUndo is false. Calls undo() 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 redo callback to apps/web/src/hooks/use-encounter.ts that calls redoUseCase and updates both encounter and undo/redo state. Expose canRedo: boolean (derived from redo stack length > 0). Verify that pushUndo (called by withUndo wrapper 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.tsx to expose redo, canRedo from 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). Use Redo2 icon from Lucide React. Button is disabled when canRedo is false. Calls redo() 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 a keydown event listener on document. Detect Ctrl+Z / Cmd+Z (undo) and Ctrl+Shift+Z / Cmd+Shift+Z (redo). Before dispatching, check document.activeElement — suppress if tag is INPUT, TEXTAREA, SELECT, or element has contentEditable. Call preventDefault() only when handling the shortcut. Accept undo, redo, canUndo, canRedo as parameters.
  • T014 [US3] Wire useUndoRedoShortcuts into the encounter provider layer. Call the hook from inside EncounterProvider in apps/web/src/contexts/encounter-context.tsx (or from App.tsx if 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. Implement saveUndoRedoStacks(undoStack, redoStack) and loadUndoRedoStacks(). Use localStorage keys "initiative:encounter:undo" and "initiative:encounter:redo". Reuse existing encounter rehydration/validation logic from encounter-storage.ts for 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 from loadUndoRedoStacks() on mount. Add a useEffect that calls saveUndoRedoStacks() 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 the clearEncounter callback, call clearHistory() on the undo/redo state after clearing the encounter. Verify this also clears persisted stacks via the useEffect.
  • T018 Run pnpm check and 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 UndoRedoStore from 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)

  1. Complete Phase 1: Foundational (domain + application)
  2. Complete Phase 2: User Story 1 (undo via button)
  3. STOP and VALIDATE: Test undo independently with all encounter actions
  4. Demo if ready — undo alone delivers significant value

Incremental Delivery

  1. Foundational → Domain logic tested, ready for integration
  2. Add US1 (Undo) → Button works → Validate independently
  3. Add US2 (Redo) → Both buttons work → Validate independently
  4. Add US3 (Shortcuts) → Keyboard works → Validate independently
  5. Add US4 (Persistence) → Refresh-safe → Validate independently
  6. 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 withUndo wrapper 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