|
| 1 | +# Reminder Completion Flow Implementation Plan |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
| 4 | +
|
| 5 | +**Goal:** Make a completed pet reminder advance directly to the next queued reminder or close immediately when the queue is empty. |
| 6 | + |
| 7 | +**Architecture:** `ActionCard` will expose an optional `onCompleted` callback that runs only after `controller.complete` succeeds. `CatReminderBubble` will use that callback to clear the action layer and inspect the controller's fresh queue snapshot, preserving the existing standalone confirmation behavior for every caller that omits the callback. |
| 8 | + |
| 9 | +**Tech Stack:** React 19, TypeScript, Vitest, Testing Library |
| 10 | + |
| 11 | +## Global Constraints |
| 12 | + |
| 13 | +- Do not render a completed-status layer inside the pet reminder bubble. |
| 14 | +- A queued successor must open at its first-level due actions. |
| 15 | +- The final completion must close the bubble and return focus to the pet. |
| 16 | +- A rejected completion must keep the current action available for retry. |
| 17 | +- Completion must remain single-flight. |
| 18 | +- Do not change standalone `ActionCard` behavior when `onCompleted` is omitted. |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +### Task 1: Successful-completion callback |
| 23 | + |
| 24 | +**Files:** |
| 25 | +- Modify: `src/features/reminders/components/ActionCard.tsx` |
| 26 | +- Test: `src/features/reminders/components/ActionCard.test.tsx` |
| 27 | + |
| 28 | +**Interfaces:** |
| 29 | +- Consumes: `controller.complete(reminder.id): Promise<void>` |
| 30 | +- Produces: optional prop `onCompleted?: (() => void) | undefined` |
| 31 | + |
| 32 | +- [ ] **Step 1: Write the failing callback tests** |
| 33 | + |
| 34 | +Add a render helper that accepts `onCompleted`, then cover success and rejection: |
| 35 | + |
| 36 | +```tsx |
| 37 | +test('hands successful completion to its host without rendering confirmation', async () => { |
| 38 | + const onCompleted = vi.fn(); |
| 39 | + const controller = createAppController(createFakeDependencies({ now: NOW })); |
| 40 | + controller.complete = vi.fn(async () => undefined); |
| 41 | + render(actionView(controller, reminder, 'zh-CN', onCompleted)); |
| 42 | + |
| 43 | + fireEvent.click(screen.getByRole('button', { name: '完成了' })); |
| 44 | + |
| 45 | + await waitFor(() => expect(onCompleted).toHaveBeenCalledTimes(1)); |
| 46 | + expect(screen.queryByRole('status')).not.toBeInTheDocument(); |
| 47 | +}); |
| 48 | + |
| 49 | +test('does not notify its host when completion fails', async () => { |
| 50 | + const onCompleted = vi.fn(); |
| 51 | + const controller = createAppController(createFakeDependencies({ now: NOW })); |
| 52 | + controller.complete = vi.fn(async () => { throw new Error('save failed'); }); |
| 53 | + render(actionView(controller, reminder, 'zh-CN', onCompleted)); |
| 54 | + |
| 55 | + fireEvent.click(screen.getByRole('button', { name: '完成了' })); |
| 56 | + |
| 57 | + await waitFor(() => expect(screen.getByRole('button', { name: '完成了' })).toBeEnabled()); |
| 58 | + expect(onCompleted).not.toHaveBeenCalled(); |
| 59 | +}); |
| 60 | +``` |
| 61 | + |
| 62 | +- [ ] **Step 2: Run the focused tests and verify RED** |
| 63 | + |
| 64 | +Run: |
| 65 | + |
| 66 | +```bash |
| 67 | +npm run test:run -- src/features/reminders/components/ActionCard.test.tsx |
| 68 | +``` |
| 69 | + |
| 70 | +Expected: FAIL because `ActionCard` does not accept or invoke `onCompleted`. |
| 71 | + |
| 72 | +- [ ] **Step 3: Implement the minimal callback** |
| 73 | + |
| 74 | +Extend `ActionCardProps` and the successful branch: |
| 75 | + |
| 76 | +```tsx |
| 77 | +interface ActionCardProps { |
| 78 | + reminder: Reminder; |
| 79 | + countdownEndsAt?: number | undefined; |
| 80 | + onCountdownStarted?: ((endsAt: number) => void) | undefined; |
| 81 | + onCompleted?: (() => void) | undefined; |
| 82 | + completionAction?: { |
| 83 | + label: string; |
| 84 | + onActivate: () => void; |
| 85 | + }; |
| 86 | +} |
| 87 | + |
| 88 | +await controller.complete(reminder.id); |
| 89 | +if (onCompleted === undefined) setCompleted(true); |
| 90 | +else onCompleted(); |
| 91 | +``` |
| 92 | + |
| 93 | +Do not invoke the callback from `catch` or `finally`. |
| 94 | + |
| 95 | +- [ ] **Step 4: Run the focused tests and verify GREEN** |
| 96 | + |
| 97 | +Run: |
| 98 | + |
| 99 | +```bash |
| 100 | +npm run test:run -- src/features/reminders/components/ActionCard.test.tsx |
| 101 | +``` |
| 102 | + |
| 103 | +Expected: all `ActionCard` tests pass. |
| 104 | + |
| 105 | +- [ ] **Step 5: Commit the callback unit** |
| 106 | + |
| 107 | +```bash |
| 108 | +git add src/features/reminders/components/ActionCard.tsx src/features/reminders/components/ActionCard.test.tsx |
| 109 | +git commit -m "feat: expose successful reminder completion" |
| 110 | +``` |
| 111 | + |
| 112 | +### Task 2: Direct pet reminder transition |
| 113 | + |
| 114 | +**Files:** |
| 115 | +- Modify: `src/features/cat/components/CatReminderBubble.tsx` |
| 116 | +- Test: `src/features/cat/components/CatReminderBubble.test.tsx` |
| 117 | + |
| 118 | +**Interfaces:** |
| 119 | +- Consumes: `ActionCard` prop `onCompleted?: (() => void) | undefined` |
| 120 | +- Consumes: `controller.getSnapshot().scheduler.dueQueue` |
| 121 | +- Produces: direct queue advancement or `onRequestClose()` |
| 122 | + |
| 123 | +- [ ] **Step 1: Replace the third-layer tests with failing direct-flow tests** |
| 124 | + |
| 125 | +Keep the existing stateful queue controller and assert the desired visible behavior: |
| 126 | + |
| 127 | +```tsx |
| 128 | +test('advances directly to the next queued reminder after completion', async () => { |
| 129 | + const user = userEvent.setup(); |
| 130 | + const controller = statefulQueueController(); |
| 131 | + renderBubble({ controller }); |
| 132 | + await user.click(screen.getByRole('button', { name: '现在做' })); |
| 133 | + |
| 134 | + await user.click(screen.getByRole('button', { name: '完成了' })); |
| 135 | + |
| 136 | + expect(screen.queryByRole('status')).not.toBeInTheDocument(); |
| 137 | + expect(screen.getByRole('dialog')).toHaveAccessibleName('喝水提醒'); |
| 138 | + expect(screen.getByRole('button', { name: '现在做' })).toHaveFocus(); |
| 139 | +}); |
| 140 | + |
| 141 | +test('closes immediately after completing the final queued reminder', async () => { |
| 142 | + const user = userEvent.setup(); |
| 143 | + const onRequestClose = vi.fn(); |
| 144 | + const controller = statefulQueueController([reminder('lookAway', 20)]); |
| 145 | + renderBubble({ controller, onRequestClose }); |
| 146 | + await user.click(screen.getByRole('button', { name: '现在做' })); |
| 147 | + |
| 148 | + await user.click(screen.getByRole('button', { name: '完成了' })); |
| 149 | + |
| 150 | + expect(screen.queryByRole('status')).not.toBeInTheDocument(); |
| 151 | + expect(onRequestClose).toHaveBeenCalledTimes(1); |
| 152 | + expect(screen.getByRole('button', { name: '猫咪' })).toHaveFocus(); |
| 153 | +}); |
| 154 | +``` |
| 155 | + |
| 156 | +Update the single-flight test to expect the bubble transition after its controlled promise resolves instead of expecting a status element. |
| 157 | + |
| 158 | +- [ ] **Step 2: Run the bubble tests and verify RED** |
| 159 | + |
| 160 | +Run: |
| 161 | + |
| 162 | +```bash |
| 163 | +npm run test:run -- src/features/cat/components/CatReminderBubble.test.tsx |
| 164 | +``` |
| 165 | + |
| 166 | +Expected: FAIL because the current implementation renders a status layer and explicit continuation control. |
| 167 | + |
| 168 | +- [ ] **Step 3: Implement direct transition and close** |
| 169 | + |
| 170 | +Remove `nextReminderIsWaiting`, `leaveCompletedAction`, and `completionAction`. Supply: |
| 171 | + |
| 172 | +```tsx |
| 173 | +onCompleted={() => { |
| 174 | + setActionOpen(false); |
| 175 | + setActionReminder(undefined); |
| 176 | + setActionOccurrenceKey(undefined); |
| 177 | + setCountdown(undefined); |
| 178 | + setSnoozeOpen(false); |
| 179 | + if (controller.getSnapshot().scheduler.dueQueue.length === 0) { |
| 180 | + onRequestClose(); |
| 181 | + returnFocusRef.current?.focus(); |
| 182 | + } |
| 183 | +}} |
| 184 | +``` |
| 185 | + |
| 186 | +When a successor remains, clearing `actionOpen` makes `displayedReminder` follow the queue head and the existing focus effect selects its first action. |
| 187 | + |
| 188 | +- [ ] **Step 4: Run both component suites and verify GREEN** |
| 189 | + |
| 190 | +Run: |
| 191 | + |
| 192 | +```bash |
| 193 | +npm run test:run -- \ |
| 194 | + src/features/reminders/components/ActionCard.test.tsx \ |
| 195 | + src/features/cat/components/CatReminderBubble.test.tsx |
| 196 | +``` |
| 197 | + |
| 198 | +Expected: both files pass with no unhandled rejection or React `act` warning. |
| 199 | + |
| 200 | +- [ ] **Step 5: Commit the pet flow** |
| 201 | + |
| 202 | +```bash |
| 203 | +git add src/features/cat/components/CatReminderBubble.tsx src/features/cat/components/CatReminderBubble.test.tsx |
| 204 | +git commit -m "fix: close completed pet reminder layers" |
| 205 | +``` |
| 206 | + |
| 207 | +### Task 3: Release verification and PR update |
| 208 | + |
| 209 | +**Files:** |
| 210 | +- No production files |
| 211 | +- Existing PR: `#3` |
| 212 | + |
| 213 | +**Interfaces:** |
| 214 | +- Consumes: the two committed component changes |
| 215 | +- Produces: verified `0.2.1` branch state ready for GitHub native packaging |
| 216 | + |
| 217 | +- [ ] **Step 1: Run the complete desktop release gate** |
| 218 | + |
| 219 | +Run: |
| 220 | + |
| 221 | +```bash |
| 222 | +npm run check:desktop-release |
| 223 | +``` |
| 224 | + |
| 225 | +Expected: |
| 226 | + |
| 227 | +- release configuration, workflow, Electron, signing, artifact, tag, and packaged-resource tests pass |
| 228 | +- all Vitest application tests pass |
| 229 | +- all 40 Playwright tests pass |
| 230 | +- web and desktop production builds pass |
| 231 | + |
| 232 | +- [ ] **Step 2: Push the implementation commits** |
| 233 | + |
| 234 | +```bash |
| 235 | +git push origin feat/cross-platform-desktop-release |
| 236 | +``` |
| 237 | + |
| 238 | +- [ ] **Step 3: Wait for PR `#3` CI** |
| 239 | + |
| 240 | +Confirm the `Build Desktop` pull-request run completes successfully before merging. |
| 241 | + |
| 242 | +- [ ] **Step 4: Merge and publish `v0.2.1`** |
| 243 | + |
| 244 | +Merge PR `#3`, create annotated tag `v0.2.1` on the resulting `main` commit, and push the tag. |
| 245 | + |
| 246 | +- [ ] **Step 5: Verify native release artifacts** |
| 247 | + |
| 248 | +Confirm all package jobs and the release job pass, then verify the public Release contains exactly: |
| 249 | + |
| 250 | +```text |
| 251 | +Codex-Pet-Pause-0.2.1-mac-arm64.dmg |
| 252 | +Codex-Pet-Pause-0.2.1-mac-x64.dmg |
| 253 | +Codex-Pet-Pause-0.2.1-windows-x64.exe |
| 254 | +Codex-Pet-Pause-0.2.1-linux-x64.AppImage |
| 255 | +Codex-Pet-Pause-0.2.1-linux-x64.deb |
| 256 | +``` |
0 commit comments