Skip to content

Commit 12dc7ab

Browse files
authored
Merge pull request #3 from ccofallen/feat/cross-platform-desktop-release
Fix packaged settings and macOS ARM64 launch
2 parents 9e4981d + 161b9af commit 12dc7ab

24 files changed

Lines changed: 510 additions & 78 deletions

.github/workflows/build-desktop.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
- run: npm run test:brand-assets
2626
- run: npm run test:desktop-workflow
2727
- run: npm run test:electron
28+
- run: npm run test:mac-signing
2829
- run: npm run test:release-artifacts
2930
- run: npm run test:release-tag
3031
- run: npm run test:packaged-resources
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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+
```
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Reminder Completion Flow Design
2+
3+
## Goal
4+
5+
Remove the extra completion-confirmation layer from the desktop pet reminder flow.
6+
Completing a reminder should either advance directly to the next queued reminder or
7+
close the reminder bubble when the queue is empty.
8+
9+
## Behavior
10+
11+
- The user opens a due reminder from the pet and chooses "Do it now."
12+
- The action card keeps countdown and completion controls unchanged.
13+
- Clicking "Complete" remains single-flight while the controller command is pending.
14+
- After a successful completion:
15+
- If another reminder is queued, the bubble immediately shows that reminder's
16+
first-level due actions.
17+
- If the queue is empty, the bubble closes and focus returns to the pet.
18+
- The flow never renders a completed-status layer, a "Continue" button, or a
19+
"Close" button inside the pet reminder bubble.
20+
- If completion fails, the current action remains visible and the completion button
21+
becomes available for retry.
22+
23+
## Component Boundary
24+
25+
`ActionCard` will accept an optional successful-completion callback. When supplied,
26+
it invokes the callback after `controller.complete` resolves instead of entering its
27+
local completed-status state. Existing callers that omit the callback retain the
28+
current completion confirmation.
29+
30+
`CatReminderBubble` will supply that callback, clear its action state, inspect the
31+
controller's fresh queue snapshot, and either reveal the next reminder or request
32+
that the bubble close.
33+
34+
## Accessibility
35+
36+
- Advancing the queue focuses the next reminder's first action.
37+
- Closing the final reminder returns focus to the pet.
38+
- A pending completion remains disabled to prevent duplicate commands.
39+
40+
## Tests
41+
42+
- A single completed reminder closes immediately without rendering a status layer.
43+
- A completed reminder with a queued successor advances directly to the successor.
44+
- A rejected completion keeps the current action available for retry.
45+
- Existing single-flight completion behavior remains covered.

e2e/accessibility.spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,8 @@ test('completes a reminder and reaches all four views with keyboard only', async
100100
await page.keyboard.press('Enter');
101101
await expect(page.getByRole('button', { name: '完成了' })).toBeFocused();
102102
await page.keyboard.press('Enter');
103-
await expect(page.getByText('喝水已完成')).toBeFocused();
104-
await page.keyboard.press('Tab');
105-
await page.keyboard.press('Enter');
103+
await expect(page.getByRole('dialog')).toHaveCount(0);
104+
await expect(page.getByTestId('cat-stage')).toBeFocused();
106105

107106
for (const [buttonName, headingName] of [
108107
['提醒', '提醒'], ['宠物', '宠物库'], ['设置', '设置'], ['陪伴', '团子 在陪你'],

e2e/localization.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ test('supports English settings navigation and a reminder action with the keyboa
253253
await page.keyboard.press('Enter');
254254
await expect(page.getByRole('button', { name: 'Done' })).toBeFocused();
255255
await page.keyboard.press('Enter');
256-
await expect(page.getByText('Drink water completed')).toBeFocused();
256+
await expect(page.getByRole('dialog')).toHaveCount(0);
257+
await expect(page.getByTestId('cat-stage')).toBeFocused();
257258
});
258259

259260
for (const viewport of [

e2e/persistence.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ test('clears local settings and IndexedDB history after confirmation', async ({
361361
await openWaitingCat(page);
362362
await page.getByRole('button', { name: '现在做' }).click();
363363
await page.getByRole('button', { name: '完成了' }).click();
364-
await page.getByRole('button', { name: '关闭' }).click();
364+
await expect(page.getByRole('dialog')).toHaveCount(0);
365365
await page.getByRole('button', { name: '设置', exact: true }).click();
366366
await page.getByRole('button', { name: '清除全部本地数据' }).click();
367367
await page.getByLabel('我了解本机数据将被删除').check();

e2e/reminders.spec.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ test('supports complete, snooze with 10-minute focus, and skip', async ({ page }
9595
await expect(page.getByRole('dialog', { name: '目视远方提醒' })).toBeVisible();
9696
await page.getByRole('button', { name: '现在做' }).click();
9797
await page.getByRole('button', { name: '完成了' }).click();
98-
await expect(page.getByRole('status', { name: '' }).filter({ hasText: '目视远方已完成' })).toBeVisible();
99-
await page.getByRole('button', { name: '继续下一项' }).click();
10098

10199
await expect(page.getByRole('dialog', { name: '喝水提醒' })).toBeVisible();
102100
await page.getByRole('button', { name: '稍后提醒' }).click();

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "codex-pet-pause",
33
"private": true,
4-
"version": "0.2.0",
4+
"version": "0.2.1",
55
"description": "A playful, local-first break reminder PWA with interactive and Codex-compatible pets.",
66
"license": "MIT",
77
"repository": {
@@ -28,6 +28,7 @@
2828
"test": "vitest",
2929
"test:run": "vitest run",
3030
"test:electron": "vitest run --config vitest.electron.config.js",
31+
"test:mac-signing": "node --test scripts/after-pack.test.mjs",
3132
"test:e2e": "playwright test",
3233
"check:release-dist": "node scripts/verify-release-dist.mjs",
3334
"test:verify-release-dist": "node scripts/test-verify-release-dist.mjs",
@@ -62,7 +63,7 @@
6263
"test:release-tag": "node --test scripts/verify-release-tag.test.mjs",
6364
"test:packaged-resources": "node --test scripts/verify-packaged-resources.test.mjs",
6465
"test:packaged-app": "node --test scripts/verify-packaged-app.test.mjs",
65-
"check:desktop-release": "npm run test:desktop-release-config && npm run test:brand-assets && npm run test:desktop-workflow && npm run test:electron && npm run test:release-artifacts && npm run test:release-tag && npm run test:packaged-resources && npm run test:packaged-app && npm run check:desktop-release-config && npm run check:desktop-workflow && npm run typecheck && npm run test:run && npm run build && npm run test:e2e && npm run desktop:build && npm run desktop:smoke:packaged-resources"
66+
"check:desktop-release": "npm run test:desktop-release-config && npm run test:brand-assets && npm run test:desktop-workflow && npm run test:electron && npm run test:mac-signing && npm run test:release-artifacts && npm run test:release-tag && npm run test:packaged-resources && npm run test:packaged-app && npm run check:desktop-release-config && npm run check:desktop-workflow && npm run typecheck && npm run test:run && npm run build && npm run test:e2e && npm run desktop:build && npm run desktop:smoke:packaged-resources"
6667
},
6768
"dependencies": {
6869
"@zip.js/zip.js": "^2.8.34",
@@ -100,6 +101,7 @@
100101
"appId": "io.elevenlabs.codexpetpause",
101102
"productName": "Codex Pet Pause",
102103
"asar": true,
104+
"afterPack": "scripts/after-pack.mjs",
103105
"files": [
104106
"dist/**/*",
105107
"electron/**/*",

0 commit comments

Comments
 (0)