# 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 - [x] 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. - [x] 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. - [x] T003 [P] Add `UndoRedoStore` port interface (`get(): UndoRedoState`, `save(state: UndoRedoState): void`) to `packages/application/src/ports.ts`. - [x] 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. - [x] 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 - [x] 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`. - [x] 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). - [x] T008 [US1] Update `apps/web/src/contexts/encounter-context.tsx` to expose `undo`, `canUndo` from the encounter hook return type. - [x] 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 - [x] 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. - [x] T011 [US2] Update `apps/web/src/contexts/encounter-context.tsx` to expose `redo`, `canRedo` from the encounter hook return type. - [x] 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 - [x] 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. - [x] 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 - [x] 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. - [x] 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 - [x] 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. - [x] 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). - [x] 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