Skip to content

[codex] animal timer growth and docs cleanup - #59

Merged
archae0pteryx merged 3 commits into
mainfrom
codex/post-049-doc-cleanup
May 15, 2026
Merged

[codex] animal timer growth and docs cleanup#59
archae0pteryx merged 3 commits into
mainfrom
codex/post-049-doc-cleanup

Conversation

@archae0pteryx

@archae0pteryx archae0pteryx commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Implement issue 030: timed Focus animals scale from 1x to 3x, expired animals get a distinct visual, and PigDetail shows remaining/expired timer state.
  • Add scaled hit rectangles and scaled PigDetail positioning so click-through behavior matches the visible animal size.
  • Add an Expired tray section; selecting an expired focus emits an overlay event that opens that focus detail.
  • Clean up planning docs and issue files so v1.2/v1.3 scope and the active issue queue are current.

Closes #30.
Updates #31.

Validation

  • task check
  • git diff --check

Notes

The implementation keeps new shared timer-scale naming animal-neutral (ranchAnimalScale / useRanchAnimalScale) while leaving PigSprite and PigDetail as the current concrete UI components.

Summary by CodeRabbit

  • New Features

    • Per-animal size scaling and updated expired visuals for timer-driven growth
    • Tray menu opens focus details for expired items
    • New notification toggles (timer_expired, focuses_over_cap, tasks_over_cap) and widget confirm-delete
    • Timer status display and improved title-commit behavior in focus detail
  • Documentation

    • Updated README/PRD/CONTEXT with new notifications, widget, and display enablement settings and v1.3 /checkpoint notes

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

PigDetail component now accepts an optional animalSize prop for scaled positioning, displays timer status (remaining time or "Expired" label) in the UI, and closes after starting a timer. Tests verify timer-state rendering and closeout behavior on preset selection.

Changes

PigDetail Timer Status & Animal Sizing

Layer / File(s) Summary
Movement / hit-rect scaling
src/hooks/usePigMovement.ts, tests
buildHitRects accepts animalScales; usePigMovement gains animalScales param and stores it in a ref to thread scaled hit-rects through drag start/end and rAF sync; existing pig name reconciliation now updates when focus title changes.
PigDetail props, positioning & timer logic
src/components/PigDetail.tsx, tests
PigDetailProps adds animalSize?; horizontal positioning uses pigX + animalSize + 8; handleStartTimer calls onClose(); commitTitle returns boolean and Enter/blur handling updated; describeTimerStatus returns MM:SS or "Expired" and is rendered when present.
Expired visuals & detail styling
src/styles.css
Adds .pig-sprite--expired animation/keyframes, updates .pig-detail background/border-radius, .pig-detail-title-input focus styling, and adds .pig-detail-timer / .pig-detail-timer-status rules.
PigDetail tests (title, timers, presets)
src/components/PigDetail.test.tsx
Adds tests covering Enter commit success/empty-error, running and expired timer rendering (with Date.now mocking), asserts onClose called for named presets and not called for rejected custom zero input.
usePigMovement tests & buildHitRects
src/hooks/usePigMovement.test.ts
Adds vi.mock for pig API, tests buildHitRects with per-pig scale, and a hook-level test verifying stable positions across title updates while rAF-driven movement runs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

🐰 A rabbit hops when timers chime,
small pigs grow tall with passing time,
expired sprites pulse, the detail sighs—
click the tray and watch focus rise. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[codex] animal timer growth and docs cleanup' accurately reflects the main change: implementing timer-based animal scaling (issue #30) and updating planning documentation.
Linked Issues check ✅ Passed All acceptance criteria from issue #30 are implemented: useRanchAnimalScale hook returns 1.0 for no-timer; pigs scale 1×–3× with elapsed time; expired visuals render with hue-rotate filter and pulse animation; PigDetail shows timer status; tray menu includes 'Expired' section; docs updated for v1.2/v1.3 scope.
Out of Scope Changes check ✅ Passed All changes align with issue #30 scope (timer growth visuals, expired styling, tray section) and documentation updates to reflect completed v1.2 features and deferred v1.3 items; no unrelated code changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/post-049-doc-cleanup

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

@archae0pteryx
archae0pteryx marked this pull request as ready for review May 15, 2026 00:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CONTEXT.md`:
- Line 157: The ordered list item starting with "In-session agent via
`/checkpoint` — HTTP POST `/proposals`, then user accepts/rejects." uses the
prefix "2." but the repo's markdownlint config expects all ordered-list prefixes
to use "1."; update that list item prefix from "2." to "1." so the entry matches
the surrounding ordered-list style and satisfies markdownlint.

In `@issues/030-pig-growth-expired-visual-tray-list.md`:
- Line 19: The comment referencing pig-specific terminology should be made
animal-neutral: update the line that reads "Returns 1.0 if no timer. Otherwise
pig_scale(elapsed, duration) clamped 1.0–3.0." to use the neutral function/term
used in the code (e.g., "animal_scale(elapsed, duration)" or simply
"scale(elapsed, duration)") so it matches the useRanchAnimalScale naming; keep
the semantics about returning 1.0 if no timer and clamping the result to
1.0–3.0.

In `@README.md`:
- Around line 51-60: The fenced code block in README.md for the storage layout
is missing a language hint; update the opening triple-backtick for that block to
include a language token (e.g., change ``` to ```text) so the block becomes
fenced as ```text, preserving the existing block contents; locate the block
showing "~/.adhd-ranch/" and update its opening fence accordingly.

In `@src/components/App.tsx`:
- Around line 46-55: The tray-event subscription logic in App's useEffect should
be moved into a dedicated hook (e.g., useOpenFocusSubscription) so App remains
view-only; create a hook in src/hooks that accepts the focuses array and a
callback (or setter) for selection, calls subscribeOpenFocusDetail(focusId)
inside an effect, handles the promise/unsubscribe correctly (catching errors and
returning the unsubscribe function), and ensures cleanup by invoking the
resolved unsubscribe on unmount; then replace the useEffect in App with a call
to this new hook (passing focuses and setSelectedId) and remove any direct I/O
from App.

In `@src/components/PigDetail.tsx`:
- Line 115: The timer status shown by describeTimerStatus(focus.timer ?? null)
in the PigDetail component will not update because describeTimerStatus uses
Date.now() but the component doesn't re-render; add a useEffect in PigDetail
that, when focus.timer is present and running, sets up a setInterval to call a
state setter (e.g., a local tick state or forceUpdate) every 1000ms to trigger
re-renders, and ensure you clearInterval in the cleanup to avoid leaks; update
the effect dependencies to watch focus.timer (or its running state) so the
interval starts/stops correctly—apply the same pattern for the other occurrence
noted around describeTimerStatus usage (lines ~244-254).

In `@src/components/PigSprite.tsx`:
- Line 46: The bob offset is currently read from BOB_OFFSETS and applied as a
raw pixel value (const bob = BOB_OFFSETS[frame % BOB_OFFSETS.length]) which
ignores the sprite's scale; change the calculation to multiply the selected
offset by the sprite's scale (use the existing scale variable) so the bob =
BOB_OFFSETS[frame % BOB_OFFSETS.length] * scale and subsequent code that uses
bob will produce a visually consistent hop at different sizes.

In `@src/hooks/useRanchAnimalScale.test.ts`:
- Around line 4-17: Add a test to verify the non-positive duration edge case for
useRanchAnimalScale: when durationSecs is 0 or negative the hook currently
returns 3, so add an it block in the existing describe("useRanchAnimalScale")
that calls useRanchAnimalScale(1_000, 0, someNow) and useRanchAnimalScale(1_000,
-10, someNow) (or separate expectations) and asserts they each return 3; place
the new it next to the other behavior tests to document this edge case.

In `@src/hooks/useRanchAnimalScale.ts`:
- Line 7: The early-return in useRanchAnimalScale that currently returns 3 for
non-positive durations (the line checking durationSecs <= 0) is unclear and
should be changed to return 1 (default scale) for invalid/edge-case durations;
update the conditional in useRanchAnimalScale to return 1 instead of 3, add a
short inline comment explaining that non-positive durations default to normal
scale, and add/update a unit test for useRanchAnimalScale to cover durationSecs
<= 0 behavior.
- Line 13: The export uses a `use` prefix for a pure function; rename the export
to remove the hook-style prefix by exporting ranchAnimalScale directly instead
of useRanchAnimalScale (or add a non-hook alias `ranchAnimalScale` that
references the existing ranchAnimalScale function for backward compatibility),
and update any imports that relied on useRanchAnimalScale to use the new name;
locate the symbol useRanchAnimalScale in the file and change the exported
identifier to ranchAnimalScale (or export both names with the non-hook name as
primary).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 71f3111a-3a11-4529-8c26-7ec22c4cba39

📥 Commits

Reviewing files that changed from the base of the PR and between fa26793 and fa4c203.

📒 Files selected for processing (29)
  • CONTEXT.md
  • PRD.md
  • README.md
  • issues/030-pig-growth-expired-visual-tray-list.md
  • issues/031-notification-settings-tray.md
  • issues/031-notification-source-settings.md
  • issues/README.md
  • issues/done/029-timer-expiry-and-notification-interface.md
  • issues/done/037-collapse-focus-reader.md
  • issues/done/038-collapse-proposal-reader.md
  • issues/done/039-unify-caps-on-polled-reader.md
  • issues/done/040-generic-tauri-reader-factory.md
  • issues/done/041-focus-writer-write-outcome.md
  • issues/done/043-proposal-lifecycle-atomicity-design.md
  • issues/done/044-domain-timer-ticker.md
  • issues/done/048-focus-document-mutation-module.md
  • issues/done/049-display-space-pig-movement-seam.md
  • src-tauri/src/app/tray.rs
  • src/api/pig.ts
  • src/components/App.test.tsx
  • src/components/App.tsx
  • src/components/PigDetail.test.tsx
  • src/components/PigDetail.tsx
  • src/components/PigSprite.tsx
  • src/hooks/usePigMovement.test.ts
  • src/hooks/usePigMovement.ts
  • src/hooks/useRanchAnimalScale.test.ts
  • src/hooks/useRanchAnimalScale.ts
  • src/styles.css
💤 Files with no reviewable changes (1)
  • issues/031-notification-settings-tray.md

Comment thread CONTEXT.md

Deferred v1.3 writer:

2. **In-session agent via `/checkpoint`** — HTTP POST `/proposals`, then user accepts/rejects.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix ordered-list prefix to satisfy markdownlint.

Line 157 uses 2. where the configured ordered-list style expects 1. prefixes.

Suggested patch
-2. **In-session agent via `/checkpoint`** — HTTP POST `/proposals`, then user accepts/rejects.
+1. **In-session agent via `/checkpoint`** — HTTP POST `/proposals`, then user accepts/rejects.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
2. **In-session agent via `/checkpoint`** — HTTP POST `/proposals`, then user accepts/rejects.
1. **In-session agent via `/checkpoint`** — HTTP POST `/proposals`, then user accepts/rejects.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 157-157: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CONTEXT.md` at line 157, The ordered list item starting with "In-session
agent via `/checkpoint` — HTTP POST `/proposals`, then user accepts/rejects."
uses the prefix "2." but the repo's markdownlint config expects all ordered-list
prefixes to use "1."; update that list item prefix from "2." to "1." so the
entry matches the surrounding ordered-list style and satisfies markdownlint.

```ts
export function usePigScale(startedAt: number | null, durationSecs: number | null): number
export function useRanchAnimalScale(startedAt: number | null, durationSecs: number | null): number
// Returns 1.0 if no timer. Otherwise pig_scale(elapsed, duration) clamped 1.0–3.0.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Minor terminology inconsistency in code comment.

The function signature correctly uses the animal-neutral name useRanchAnimalScale, but the comment still references pig_scale(elapsed, duration). For consistency with the animal-neutral naming guidance added in lines 9-11, consider updating the comment to use a neutral term like animal_scale or simply scale.

📝 Suggested update for consistency
   export function useRanchAnimalScale(startedAt: number | null, durationSecs: number | null): number
-  // Returns 1.0 if no timer. Otherwise pig_scale(elapsed, duration) clamped 1.0–3.0.
+  // Returns 1.0 if no timer. Otherwise scale(elapsed, duration) clamped 1.0–3.0.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Returns 1.0 if no timer. Otherwise pig_scale(elapsed, duration) clamped 1.0–3.0.
// Returns 1.0 if no timer. Otherwise scale(elapsed, duration) clamped 1.0–3.0.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@issues/030-pig-growth-expired-visual-tray-list.md` at line 19, The comment
referencing pig-specific terminology should be made animal-neutral: update the
line that reads "Returns 1.0 if no timer. Otherwise pig_scale(elapsed, duration)
clamped 1.0–3.0." to use the neutral function/term used in the code (e.g.,
"animal_scale(elapsed, duration)" or simply "scale(elapsed, duration)") so it
matches the useRanchAnimalScale naming; keep the semantics about returning 1.0
if no timer and clamping the result to 1.0–3.0.

Comment thread README.md
Comment on lines 51 to 60
```
~/.adhd-ranch/
focuses/
<slug>/focus.md YAML frontmatter + - [ ] bullets
<slug>/timer.json optional countdown timer sidecar
proposals.jsonl pending proposals, one per line
decisions.jsonl audit log of accept/reject (with edited flag)
settings.yaml optional caps + alert config
settings.yaml optional caps + notification/widget/display config
run/port ephemeral HTTP port
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language hint to the fenced code block.

The storage layout fence is missing a language token (MD040).

Suggested patch
-```
+```text
 ~/.adhd-ranch/
   focuses/
     <slug>/focus.md     YAML frontmatter + - [ ] bullets
     <slug>/timer.json    optional countdown timer sidecar
   proposals.jsonl       pending proposals, one per line
   decisions.jsonl       audit log of accept/reject (with edited flag)
   settings.yaml         optional caps + notification/widget/display config
   run/port              ephemeral HTTP port
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 51-51: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 51 - 60, The fenced code block in README.md for the
storage layout is missing a language hint; update the opening triple-backtick
for that block to include a language token (e.g., change ``` to ```text) so the
block becomes fenced as ```text, preserving the existing block contents; locate
the block showing "~/.adhd-ranch/" and update its opening fence accordingly.

Comment thread src/components/App.tsx
Comment on lines +46 to +55
useEffect(() => {
const unsubscribe = subscribeOpenFocusDetail((focusId) => {
if (focuses.some((focus) => focus.id === focusId)) {
setSelectedId(focusId);
}
}).catch(() => () => {});
return () => {
unsubscribe.then((fn) => fn());
};
}, [focuses]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Move tray-event subscription out of App and into a hook.

This effect performs API/event I/O in a component; extract it to a src/hooks/ hook and keep App view-focused.

As per coding guidelines, "Frontend React components in src/components/ should be view-only with no fetch and no direct I/O." and "Frontend hooks in src/hooks/ should handle state + effects and call api/ clients."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/App.tsx` around lines 46 - 55, The tray-event subscription
logic in App's useEffect should be moved into a dedicated hook (e.g.,
useOpenFocusSubscription) so App remains view-only; create a hook in src/hooks
that accepts the focuses array and a callback (or setter) for selection, calls
subscribeOpenFocusDetail(focusId) inside an effect, handles the
promise/unsubscribe correctly (catching errors and returning the unsubscribe
function), and ensures cleanup by invoking the resolved unsubscribe on unmount;
then replace the useEffect in App with a call to this new hook (passing focuses
and setSelectedId) and remove any direct I/O from App.

onClose();
}

const timerStatus = describeTimerStatus(focus.timer ?? null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Timer status display won't update in real-time.

describeTimerStatus is called during render and uses Date.now(), but the component won't re-render automatically as time passes. The timer countdown will appear frozen until another state change triggers a re-render (e.g., user interaction). Users typically expect timer displays to tick down every second.

Consider adding a useEffect hook that calls setInterval to force a re-render every second when a running timer is present.

⏱️ Proposed fix to add live timer updates
+  const [, setTick] = useState(0);
+
+  useEffect(() => {
+    if (!focus.timer || focus.timer.status === "Expired") return;
+    const interval = setInterval(() => setTick((t) => t + 1), 1000);
+    return () => clearInterval(interval);
+  }, [focus.timer]);
+
   const timerStatus = describeTimerStatus(focus.timer ?? null);

Also applies to: 244-254

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/PigDetail.tsx` at line 115, The timer status shown by
describeTimerStatus(focus.timer ?? null) in the PigDetail component will not
update because describeTimerStatus uses Date.now() but the component doesn't
re-render; add a useEffect in PigDetail that, when focus.timer is present and
running, sets up a setInterval to call a state setter (e.g., a local tick state
or forceUpdate) every 1000ms to trigger re-renders, and ensure you clearInterval
in the cleanup to avoid leaks; update the effect dependencies to watch
focus.timer (or its running state) so the interval starts/stops correctly—apply
the same pattern for the other occurrence noted around describeTimerStatus usage
(lines ~244-254).

@@ -42,15 +46,16 @@ export function PigSprite({
const bob = BOB_OFFSETS[frame % BOB_OFFSETS.length];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider scaling the bob offset.

The bob offset (from BOB_OFFSETS) is applied as a constant pixel value regardless of scale. When the sprite is 3× larger, the -2px vertical hop becomes visually smaller relative to the sprite size, potentially making the walk animation look less pronounced.

If a consistent visual bob is desired, multiply the offset by scale:

- const bob = BOB_OFFSETS[frame % BOB_OFFSETS.length];
+ const bob = BOB_OFFSETS[frame % BOB_OFFSETS.length] * scale;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/PigSprite.tsx` at line 46, The bob offset is currently read
from BOB_OFFSETS and applied as a raw pixel value (const bob = BOB_OFFSETS[frame
% BOB_OFFSETS.length]) which ignores the sprite's scale; change the calculation
to multiply the selected offset by the sprite's scale (use the existing scale
variable) so the bob = BOB_OFFSETS[frame % BOB_OFFSETS.length] * scale and
subsequent code that uses bob will produce a visually consistent hop at
different sizes.

Comment on lines +4 to +17
describe("useRanchAnimalScale", () => {
it("returns 1 for focuses without timers", () => {
expect(useRanchAnimalScale(null, null)).toBe(1);
});

it("grows linearly from 1 to 3 over the timer duration", () => {
expect(useRanchAnimalScale(1_000, 120, 1_060_000)).toBe(2);
});

it("clamps to 3 at or after the timer duration", () => {
expect(useRanchAnimalScale(1_000, 120, 1_120_000)).toBe(3);
expect(useRanchAnimalScale(1_000, 120, 2_000_000)).toBe(3);
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add test coverage for the non-positive duration edge case.

The implementation returns 3 when durationSecs <= 0, but this behavior is not tested. Add a test case to verify and document this edge case.

🧪 Suggested test case
   it("clamps to 3 at or after the timer duration", () => {
     expect(useRanchAnimalScale(1_000, 120, 1_120_000)).toBe(3);
     expect(useRanchAnimalScale(1_000, 120, 2_000_000)).toBe(3);
   });
+
+  it("returns 3 for non-positive duration", () => {
+    expect(useRanchAnimalScale(1_000, 0, 1_060_000)).toBe(3);
+    expect(useRanchAnimalScale(1_000, -10, 1_060_000)).toBe(3);
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useRanchAnimalScale.test.ts` around lines 4 - 17, Add a test to
verify the non-positive duration edge case for useRanchAnimalScale: when
durationSecs is 0 or negative the hook currently returns 3, so add an it block
in the existing describe("useRanchAnimalScale") that calls
useRanchAnimalScale(1_000, 0, someNow) and useRanchAnimalScale(1_000, -10,
someNow) (or separate expectations) and asserts they each return 3; place the
new it next to the other behavior tests to document this edge case.

nowMs = Date.now(),
): number {
if (startedAt === null || durationSecs === null) return 1;
if (durationSecs <= 0) return 3;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify the non-positive duration edge case.

Returning 3 (max scale) when durationSecs <= 0 seems arbitrary. This edge case is untested and could cause unexpected visual results. Consider returning 1 (default scale) for invalid durations, or document why max scale is the intended fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useRanchAnimalScale.ts` at line 7, The early-return in
useRanchAnimalScale that currently returns 3 for non-positive durations (the
line checking durationSecs <= 0) is unclear and should be changed to return 1
(default scale) for invalid/edge-case durations; update the conditional in
useRanchAnimalScale to return 1 instead of 3, add a short inline comment
explaining that non-positive durations default to normal scale, and add/update a
unit test for useRanchAnimalScale to cover durationSecs <= 0 behavior.

return 1 + progress * 2;
}

export const useRanchAnimalScale = ranchAnimalScale;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove use prefix from pure function export.

useRanchAnimalScale is not a React hook—it doesn't call any hook APIs (useState, useEffect, etc.). The use prefix is a React community convention reserved exclusively for hooks. Exporting a pure function with this prefix violates the convention and may confuse linters (e.g., react-hooks/rules-of-hooks) or developers expecting hook behavior.

📛 Proposed fix
-export const useRanchAnimalScale = ranchAnimalScale;
+export { ranchAnimalScale as ranchAnimalScale };

Or if an alias is required for backward compatibility, use a non-hook name:

-export const useRanchAnimalScale = ranchAnimalScale;
+export const computeRanchAnimalScale = ranchAnimalScale;

As per coding guidelines, frontend hooks in src/hooks/ should handle state + effects. This is a pure function and should not carry the use prefix.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const useRanchAnimalScale = ranchAnimalScale;
export { ranchAnimalScale as ranchAnimalScale };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useRanchAnimalScale.ts` at line 13, The export uses a `use` prefix
for a pure function; rename the export to remove the hook-style prefix by
exporting ranchAnimalScale directly instead of useRanchAnimalScale (or add a
non-hook alias `ranchAnimalScale` that references the existing ranchAnimalScale
function for backward compatibility), and update any imports that relied on
useRanchAnimalScale to use the new name; locate the symbol useRanchAnimalScale
in the file and change the exported identifier to ranchAnimalScale (or export
both names with the non-hook name as primary).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/PigDetail.test.tsx`:
- Around line 182-192: The Date.now spy in the "shows remaining time for a
running timer" test (nowSpy created via vi.spyOn(Date, "now")) may not be
restored if the assertion fails; update the test that calls renderDetail(...)
and expects "01:30 remaining" to guarantee cleanup by either wrapping the
assertion in a try/finally and calling nowSpy.mockRestore() in the finally
block, or add a suite-level afterEach(() => vi.restoreAllMocks()) so all spies
(including Date.now) are always restored between tests.

In `@src/styles.css`:
- Around line 425-443: The expired animation on .pig-sprite--expired
.pig-sprite-frame uses expired-animal-pulse unconditionally; add a
prefers-reduced-motion media query that removes or overrides the animation
(e.g., set animation: none and optionally keep a static visual cue like
transform: none or a subtle non-animating style) so users with reduced-motion:
reduce do not see the pulse; update or add rules targeting .pig-sprite--expired
.pig-sprite-frame within `@media` (prefers-reduced-motion: reduce) and ensure the
`@keyframes` expired-animal-pulse remains for non-reduced users.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 90f14f4e-c457-4e7a-9bc9-d29699f4d528

📥 Commits

Reviewing files that changed from the base of the PR and between b969d21 and 7025cb7.

📒 Files selected for processing (5)
  • src/components/PigDetail.test.tsx
  • src/components/PigDetail.tsx
  • src/hooks/usePigMovement.test.ts
  • src/hooks/usePigMovement.ts
  • src/styles.css

Comment on lines +182 to +192
it("shows remaining time for a running timer", () => {
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1_030_000);
renderDetail({
focus: {
...baseFocus,
timer: { duration_secs: 120, started_at: 1_000, status: "Running" },
},
});
expect(screen.getByText("01:30 remaining")).toBeInTheDocument();
nowSpy.mockRestore();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -name "PigDetail.test.tsx" -type f 2>/dev/null | head -5

Repository: killallgit/adhd-ranch

Length of output: 47


🏁 Script executed:

git ls-files | grep -i pigdetail

Repository: killallgit/adhd-ranch

Length of output: 166


🏁 Script executed:

fd "PigDetail.test" -t f

Repository: killallgit/adhd-ranch

Length of output: 99


🏁 Script executed:

rg "PigDetail.test" --type tsx

Repository: killallgit/adhd-ranch

Length of output: 93


🏁 Script executed:

fd "PigDetail" -t f

Repository: killallgit/adhd-ranch

Length of output: 128


🏁 Script executed:

git ls-files src/components/ | head -20

Repository: killallgit/adhd-ranch

Length of output: 705


🏁 Script executed:

wc -l src/components/PigDetail.test.tsx

Repository: killallgit/adhd-ranch

Length of output: 103


🏁 Script executed:

cat -n src/components/PigDetail.test.tsx | head -50

Repository: killallgit/adhd-ranch

Length of output: 1816


🏁 Script executed:

cat -n src/components/PigDetail.test.tsx | sed -n '160,240p'

Repository: killallgit/adhd-ranch

Length of output: 4048


🏁 Script executed:

cat -n src/components/PigDetail.test.tsx | grep -A 3 "afterEach\|beforeEach"

Repository: killallgit/adhd-ranch

Length of output: 47


🏁 Script executed:

cat -n src/components/PigDetail.test.tsx

Repository: killallgit/adhd-ranch

Length of output: 12372


🏁 Script executed:

cat -n src/components/PigDetail.test.tsx | sed -n '1,50p' | grep -E "afterEach|beforeEach|afterAll|beforeAll"

Repository: killallgit/adhd-ranch

Length of output: 47


🏁 Script executed:

grep -n "afterEach\|beforeEach\|vi.restoreAllMocks\|clearAllMocks" src/components/PigDetail.test.tsx

Repository: killallgit/adhd-ranch

Length of output: 47


🏁 Script executed:

head -20 src/components/PigDetail.test.tsx | tail -15

Repository: killallgit/adhd-ranch

Length of output: 378


🏁 Script executed:

fd "\.test\.tsx$" src/components/ | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && grep -n "afterEach\|vi.restoreAllMocks" {}'

Repository: killallgit/adhd-ranch

Length of output: 194


🏁 Script executed:

rg "afterEach.*vi\.(restoreAllMocks|clearAllMocks)" src/components/

Repository: killallgit/adhd-ranch

Length of output: 47


🏁 Script executed:

find . -name "vitest.config.*" -o -name "vite.config.*" | head -5

Repository: killallgit/adhd-ranch

Length of output: 82


🏁 Script executed:

rg "vi\.spyOn\(Date" src/components/ -A 5

Repository: killallgit/adhd-ranch

Length of output: 828


🏁 Script executed:

rg "mockRestore\(\)" src/components/ -B 5 -A 1

Repository: killallgit/adhd-ranch

Length of output: 945


🏁 Script executed:

git ls-files | grep -E "vitest|vite\.config|setup" | head -10

Repository: killallgit/adhd-ranch

Length of output: 96


🏁 Script executed:

cat vitest.config.ts 2>/dev/null || cat vitest.config.js 2>/dev/null || echo "No vitest config found"

Repository: killallgit/adhd-ranch

Length of output: 88


🏁 Script executed:

cat -n vite.config.ts

Repository: killallgit/adhd-ranch

Length of output: 993


🏁 Script executed:

cat -n vitest.setup.ts

Repository: killallgit/adhd-ranch

Length of output: 115


🏁 Script executed:

cat src/components/App.test.tsx | grep -B 10 "Date.*now.*spyOn" | head -20

Repository: killallgit/adhd-ranch

Length of output: 47


🏁 Script executed:

rg "afterEach|beforeEach" src/ --type tsx | head -20

Repository: killallgit/adhd-ranch

Length of output: 93


🏁 Script executed:

for f in $(fd "\.test\.tsx$" src/components | head -5); do
  echo "=== $f ===="
  grep -c "mockRestore\|restoreAllMocks" "$f" || echo "0"
done

Repository: killallgit/adhd-ranch

Length of output: 299


🏁 Script executed:

grep -n "mockRestore\|spyOn" src/components/App.test.tsx

Repository: killallgit/adhd-ranch

Length of output: 168


🏁 Script executed:

grep -B 3 -A 10 "spyOn(Date" src/components/App.test.tsx

Repository: killallgit/adhd-ranch

Length of output: 518


🌐 Web query:

Vitest test execution assertion failure stops execution mockRestore

💡 Result:

In Vitest, a failing assertion in a test stops execution of the current test block (it throws immediately). So any code after the failing expect won’t run, which means teardown code like mockRestore in the middle of the test won’t be reached. Vitest explicitly notes that when an expect fails in an it/test block, it “exits that block, as if it crashes”, and suggests using expect.soft if you want the test to continue after failures [1]. What this means for “mockRestore stops execution”: 1) If you call mockRestore after an expect that can fail, it may never run because the failing expect throws and stops the rest of that test function [1]. 2) The safer pattern is to put restoration in afterEach/afterAll (or use global restore via Vitest lifecycle), so it runs even if an assertion fails. (Your restoration code needs to live in a teardown hook, not after a potentially-failing assertion.) If your goal is to restore all mocks/spies, also be aware of Vitest behavior/limitations around restoration: - Vitest has had issues where mockRestore/restoreAllMocks didn’t behave as expected for certain spy setups (e.g., restoration not returning to original descriptor / implementation), so depending on how the mock/spies were created, mockRestore might not give the exact “original behavior” you expect [2][3][4]. Common workaround/verification steps: - Move mockRestore/vi.restoreAllMocks into afterEach so it always runs. - If you need multiple assertions in a single test and still want cleanup to happen, use afterEach for cleanup and optionally use expect.soft for the non-critical assertions so execution continues [1].

Citations:


🌐 Web query:

Vitest vi.restoreAllMocks afterEach cleanup pattern best practice

💡 Result:

Best-practice pattern: call vi.restoreAllMocks in afterEach (per test), unless you enable the equivalent test.restoreMocks option globally. Why afterEach + restoreAllMocks - vi.restoreAllMocks “calls.mockRestore on all spies”, clearing mock history and restoring original implementations and even original property descriptors [1]. - Vitest’s mocking guide warns to “clear or restore mocks before or after each test run” to undo mock state changes between runs [2]. - The official lifecycle guide shows afterEach hooks as the per-test place for cleanup logic [3][4]. Recommended code (local) - Use afterEach( => { vi.restoreAllMocks }) in the test file/suite where you create spyOn mocks. Example: afterEach( => { vi.restoreAllMocks }) Or recommended config (global) - In vitest.config.js, you can set test.restoreMocks: true to make Vitest call vi.restoreAllMocks before each test automatically [5]. - Caution: docs warn this can cause problems with async concurrent tests because one test finishing may restore spies used by another test still in progress [5]. When not to rely on restoreAllMocks - If you’re using vi.mock (module mocks), restoreAllMocks is intended to restore spies created with vi.spyOn / vi.fn; it won’t “unmock” module imports [6]. In that case you’d use the appropriate unmock API (e.g., vi.doUnmock) instead [6]. Bottom line - For typical spyOn usage: prefer afterEach( => vi.restoreAllMocks) for isolation [1][2]. - For suites without async overlap and you want less boilerplate: consider test.restoreMocks: true, but be mindful of the concurrency warning [5].

Citations:


Ensure Date.now spy is always restored, even on assertion failure.

Line 190's expect(screen.getByText("01:30 remaining")) can fail, which stops test execution immediately and prevents nowSpy.mockRestore() on line 191 from running. The spy will leak into subsequent tests, tainting their results. Use try/finally to guarantee cleanup:

 it("shows remaining time for a running timer", () => {
   const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1_030_000);
-  renderDetail({
-    focus: {
-      ...baseFocus,
-      timer: { duration_secs: 120, started_at: 1_000, status: "Running" },
-    },
-  });
-  expect(screen.getByText("01:30 remaining")).toBeInTheDocument();
-  nowSpy.mockRestore();
+  try {
+    renderDetail({
+      focus: {
+        ...baseFocus,
+        timer: { duration_secs: 120, started_at: 1_000, status: "Running" },
+      },
+    });
+    expect(screen.getByText("01:30 remaining")).toBeInTheDocument();
+  } finally {
+    nowSpy.mockRestore();
+  }
 });

Alternatively, add afterEach(() => vi.restoreAllMocks()) once to the suite to handle all spies globally.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("shows remaining time for a running timer", () => {
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1_030_000);
renderDetail({
focus: {
...baseFocus,
timer: { duration_secs: 120, started_at: 1_000, status: "Running" },
},
});
expect(screen.getByText("01:30 remaining")).toBeInTheDocument();
nowSpy.mockRestore();
});
it("shows remaining time for a running timer", () => {
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1_030_000);
try {
renderDetail({
focus: {
...baseFocus,
timer: { duration_secs: 120, started_at: 1_000, status: "Running" },
},
});
expect(screen.getByText("01:30 remaining")).toBeInTheDocument();
} finally {
nowSpy.mockRestore();
}
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/PigDetail.test.tsx` around lines 182 - 192, The Date.now spy
in the "shows remaining time for a running timer" test (nowSpy created via
vi.spyOn(Date, "now")) may not be restored if the assertion fails; update the
test that calls renderDetail(...) and expects "01:30 remaining" to guarantee
cleanup by either wrapping the assertion in a try/finally and calling
nowSpy.mockRestore() in the finally block, or add a suite-level afterEach(() =>
vi.restoreAllMocks()) so all spies (including Date.now) are always restored
between tests.

Comment thread src/styles.css
Comment on lines +425 to +443
.pig-sprite--expired .pig-sprite-frame {
animation: expired-animal-pulse 700ms ease-out 1;
}

@keyframes expired-animal-pulse {
0%,
100% {
transform: translate(0, 0) scale(1);
}
20% {
transform: translate(-2px, 0) scale(1.08);
}
40% {
transform: translate(2px, 0) scale(1.08);
}
60% {
transform: translate(-1px, 0) scale(1.04);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Honor reduced-motion preference for expired animation.

Line 426 applies motion unconditionally. Add a reduced-motion override so the expired state remains visually distinct without forced animation.

Proposed patch
 .pig-sprite--expired .pig-sprite-frame {
   animation: expired-animal-pulse 700ms ease-out 1;
 }

+@media (prefers-reduced-motion: reduce) {
+  .pig-sprite--expired .pig-sprite-frame {
+    animation: none;
+  }
+}
+
 `@keyframes` expired-animal-pulse {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.pig-sprite--expired .pig-sprite-frame {
animation: expired-animal-pulse 700ms ease-out 1;
}
@keyframes expired-animal-pulse {
0%,
100% {
transform: translate(0, 0) scale(1);
}
20% {
transform: translate(-2px, 0) scale(1.08);
}
40% {
transform: translate(2px, 0) scale(1.08);
}
60% {
transform: translate(-1px, 0) scale(1.04);
}
}
.pig-sprite--expired .pig-sprite-frame {
animation: expired-animal-pulse 700ms ease-out 1;
}
`@media` (prefers-reduced-motion: reduce) {
.pig-sprite--expired .pig-sprite-frame {
animation: none;
}
}
`@keyframes` expired-animal-pulse {
0%,
100% {
transform: translate(0, 0) scale(1);
}
20% {
transform: translate(-2px, 0) scale(1.08);
}
40% {
transform: translate(2px, 0) scale(1.08);
}
60% {
transform: translate(-1px, 0) scale(1.04);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/styles.css` around lines 425 - 443, The expired animation on
.pig-sprite--expired .pig-sprite-frame uses expired-animal-pulse
unconditionally; add a prefers-reduced-motion media query that removes or
overrides the animation (e.g., set animation: none and optionally keep a static
visual cue like transform: none or a subtle non-animating style) so users with
reduced-motion: reduce do not see the pulse; update or add rules targeting
.pig-sprite--expired .pig-sprite-frame within `@media` (prefers-reduced-motion:
reduce) and ensure the `@keyframes` expired-animal-pulse remains for non-reduced
users.

@archae0pteryx
archae0pteryx merged commit effabf4 into main May 15, 2026
2 checks passed
@archae0pteryx
archae0pteryx deleted the codex/post-049-doc-cleanup branch May 15, 2026 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

030 — Pig scale growth + expired visual + tray list

1 participant