Skip to content

[timer-picker-ui]: Preset picker in NewFocusWindow + PigDetail - #55

Merged
archae0pteryx merged 2 commits into
mainfrom
timer-picker-ui
May 12, 2026
Merged

[timer-picker-ui]: Preset picker in NewFocusWindow + PigDetail#55
archae0pteryx merged 2 commits into
mainfrom
timer-picker-ui

Conversation

@archae0pteryx

@archae0pteryx archae0pteryx commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces #54 (auto-closed when its base branch was deleted after #53 merged).

Adds the two surfaces where users actually set a timer:

  • Dedicated Tauri new-focus window now exposes a preset dropdown (2/4/8/16/32m + custom) and submits the resolved preset to create_focus.
  • PigDetail popover gains the same picker plus a Start/Restart button that fires a new start_timer command against an existing focus. Button label flips to "Restart" when focus.timer.status === "Running" to flag the overwrite.

Shared bits:

  • TimerPresetPicker component extracted from the orphaned NewFocusForm. Orphan left in place per CLAUDE.md scope rule.
  • resolvePreset / isCustomValid pure helpers; rejects custom < 1 minute.

Backend:

  • Commands::start_timer(focus_id, preset) builds a Running FocusTimer (started_at = clock_secs, duration from preset) and calls store.update_timer.
  • ui_bridge::start_timer registered; FocusWriter.startTimer follows the existing WriteOutcome pattern. Fixture + App.test.tsx mock updated.

Test plan

  • task check green (lint + typecheck + cargo tests + vitest)
  • Backend: 2 new start_timer tests (preset-duration mapping, not-found)
  • Frontend: 9 TimerPresetPicker tests + 2 FocusWriter.startTimer tests + 4 PigDetail timer-picker tests
  • Manual: open new-focus window, pick 2m, confirm timer fires via the [029-timer-expiry-and-notification-interface]: Unify cap + timer-expiry notifications #53 expiry loop
  • Manual: click pig, pick custom 1m, confirm Start persists timer.json and expiry fires

Summary by CodeRabbit

  • New Features

    • Start/Restart timers for focus sessions with named presets (2,4,8,16,32) or custom minutes; Start button reflects running state.
    • Timer selection added to new-focus flow and focus details via a preset picker UI.
  • Bug Fixes / Validation

    • Custom-minute input validated (must be integer ≥1) with inline error messaging.
  • Tests

    • Extensive unit and UI tests covering preset resolution, picker behavior, start timer flows, and error cases.

Review Change Stack

Two timer-setting surfaces. The dedicated Tauri new-focus window now
exposes a preset dropdown (2/4/8/16/32m + custom). The pig-detail
popover does the same, plus a Start/Restart button that fires a new
start_timer command against the existing focus.

Shared component:
- TimerPresetPicker (select + custom-minutes input) extracted from the
  orphaned NewFocusForm; orphan left in place per CLAUDE.md scope rule.
- resolvePreset() / isCustomValid() are pure helpers used by both
  surfaces.

NewFocusWindow / useNewFocusWindow:
- Picker state lifted into the hook. Submit forwards the resolved
  preset to focusWriter.createFocus. Custom-minutes validation matches
  the inline form (reject < 1).

Backend (crates/commands):
- Commands::start_timer(focus_id, preset) builds a Running FocusTimer
  with started_at = clock_secs and calls store.update_timer.
- Tests cover preset-duration mapping and not-found from store.

Tauri + frontend transport:
- ui_bridge::start_timer registered; FocusWriter gains startTimer
  (mirrors the WriteOutcome pattern). Fixture writer + App test mock
  updated.

PigDetail:
- Renders TimerPresetPicker + Start button. Button reads "Restart"
  when focus.timer.status === "Running" to flag the overwrite.
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR extends timer functionality across the full stack: a backend Commands::start_timer method creates and persists a running timer, exposed via Tauri to the frontend, which provides a new startTimer API method. A reusable timer preset picker component supports both named durations and custom minutes. The app integrates the timer start flow into existing focus details and new focus creation.

Changes

Timer Start Feature

Layer / File(s) Summary
Backend timer start command
crates/commands/src/focus.rs
Commands::start_timer creates a FocusTimer marked Running with preset duration and current timestamp, persists via store, with tests verifying success and NotFound error cases.
Tauri command bridge
src-tauri/src/ui_bridge/mod.rs, src-tauri/src/app/mod.rs
Tauri start_timer command handler registered and implemented to forward focus id and preset to the backend, with required domain import and registration in invoke_handler.
FocusWriter API layer
src/api/focusWriter.ts, src/api/focusWriter.test.ts, src/api/fixtureFocusWriter.ts, src/components/App.test.tsx
FocusWriter.startTimer interface and Tauri implementation invoke the start_timer command with { focusId, preset } and reuse existing error/result handling; tests and fixture writer include coverage.
Timer preset picker component
src/lib/timerPreset.ts, src/lib/timerPreset.test.ts, src/components/TimerPresetPicker.tsx, src/components/TimerPresetPicker.test.tsx
Adds PresetSelection type, resolvePreset and isCustomValid helpers, TimerPresetPicker controlled component rendering named presets plus a custom input, and tests for selection and validation.
App-level timer orchestration
src/components/App.tsx
handleStartTimer handler calls focusWriter.startTimer and routes write failures via onWriteFailure, then passes the handler into PigDetail.
PigDetail timer UI
src/components/PigDetail.tsx, src/components/PigDetail.test.tsx
PigDetail integrates TimerPresetPicker, Start/Restart button, selection/custom-minute state and validation, resets picker state when focus changes, and includes tests for named/custom starts, validation, restart labeling, and state reset.
New focus creation with timer
src/hooks/useNewFocusWindow.ts, src/components/NewFocusWindow.tsx, src/new-focus.tsx
Hook and components manage timer selection and custom minutes, validate custom input on submit, compute timer_preset via resolvePreset, and pass it to focusWriter.createFocus during new-focus creation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • killallgit/adhd-ranch#51: Related — adds FocusStore update and timer-ticker logic referenced by the new Commands::start_timer persistence calls.
  • killallgit/adhd-ranch#53: Related — introduces FocusStore::update_timer implementation that Commands::start_timer invokes.
  • killallgit/adhd-ranch#32: Related — earlier work introducing timer domain types and plumbing used by this PR.

Poem

🐰 I nudged a timer into flight,

Presets set and minutes bright,
Front and back in sync they play,
Start or restart — hooray, hooray! 🕰️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% 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 '[timer-picker-ui]: Preset picker in NewFocusWindow + PigDetail' accurately and specifically describes the main changes: adding timer preset picker UI components to two surfaces (NewFocusWindow and PigDetail).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 timer-picker-ui

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

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/useNewFocusWindow.ts (1)

47-55: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Use hideWindow() in the Escape path to avoid reset drift.

Escape currently reimplements reset logic inline. Reusing hideWindow() keeps one reset source of truth.

Suggested patch
   useEffect(() => {
     const onKey = (e: KeyboardEvent) => {
       if (e.key === "Escape") {
-        setTitle("");
-        setDescription("");
-        setTimerSelection("none");
-        setCustomMinutes(10);
-        setError(null);
-        void getCurrentWindow().hide();
+        void hideWindow();
       }
     };
🤖 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/useNewFocusWindow.ts` around lines 47 - 55, Replace the inline
reset logic in the onKey handler (the Escape branch inside function onKey) with
a call to the existing hideWindow() helper so the reset behavior is centralized;
locate onKey and remove the manual setTitle, setDescription, setTimerSelection,
setCustomMinutes, setError and getCurrentWindow().hide() calls and invoke
hideWindow() instead to preserve single source of truth for resetting/hiding.
🤖 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.tsx`:
- Around line 52-55: The timer state (timerSelection, customMinutes, timerError)
in PigDetail is only initialized once and will persist when the component
receives a new focus prop; add a useEffect inside PigDetail that watches the
focus prop and calls setTimerSelection("Eight"), setCustomMinutes(10) and
setTimerError(null) to reset the picker whenever focus changes so state doesn't
leak between focuses.

In `@src/hooks/useNewFocusWindow.ts`:
- Around line 4-8: The hook useNewFocusWindow imports UI component logic
(PresetSelection type and helper functions isCustomValid, resolvePreset) from
src/components; extract these three symbols into a new shared non-UI module (for
example src/timer/presets or src/timer/index) and update imports so both
useNewFocusWindow and TimerPresetPicker import from that shared module instead
of src/components; ensure the exported names remain PresetSelection,
isCustomValid, and resolvePreset and update any relative import paths in
useNewFocusWindow and the component to reference the new module.

---

Outside diff comments:
In `@src/hooks/useNewFocusWindow.ts`:
- Around line 47-55: Replace the inline reset logic in the onKey handler (the
Escape branch inside function onKey) with a call to the existing hideWindow()
helper so the reset behavior is centralized; locate onKey and remove the manual
setTitle, setDescription, setTimerSelection, setCustomMinutes, setError and
getCurrentWindow().hide() calls and invoke hideWindow() instead to preserve
single source of truth for resetting/hiding.
🪄 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: ec656ff6-ee95-4986-a61e-b52efb7f5ddb

📥 Commits

Reviewing files that changed from the base of the PR and between 3ed6159 and ac6c93e.

📒 Files selected for processing (15)
  • crates/commands/src/focus.rs
  • src-tauri/src/app/mod.rs
  • src-tauri/src/ui_bridge/mod.rs
  • src/api/fixtureFocusWriter.ts
  • src/api/focusWriter.test.ts
  • src/api/focusWriter.ts
  • src/components/App.test.tsx
  • src/components/App.tsx
  • src/components/NewFocusWindow.tsx
  • src/components/PigDetail.test.tsx
  • src/components/PigDetail.tsx
  • src/components/TimerPresetPicker.test.tsx
  • src/components/TimerPresetPicker.tsx
  • src/hooks/useNewFocusWindow.ts
  • src/new-focus.tsx

Comment thread src/components/PigDetail.tsx
Comment thread src/hooks/useNewFocusWindow.ts Outdated
- Move PresetSelection / resolvePreset / isCustomValid out of
  src/components/ into src/lib/timerPreset.ts. The hook
  useNewFocusWindow was importing domain helpers from a component
  module; per CLAUDE.md frontend rules, hooks/ should depend on lib/
  or api/, not on components/. TimerPresetPicker stays the UI piece
  and now just imports PresetSelection from the lib module.
- Reset PigDetail's timer-picker state when focus.id changes.
  Previously the same component instance could leak selection and
  custom-minutes across focuses when its focus prop swapped.

Test coverage:
- Move resolvePreset + isCustomValid unit tests to lib/timerPreset.test.ts.
- New PigDetail test renders a Harness that swaps the focus.id mid-flight
  and asserts the picker resets to the default selection.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/useNewFocusWindow.ts (1)

69-82: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle thrown errors from createFocus to avoid unhandled submit failures.

Line 70 assumes focusWriter.createFocus always resolves to WriteOutcome. If it throws, submit fails without a controlled error state. Add a catch path and set a fallback error.

Suggested patch
   try {
     const outcome = await focusWriter.createFocus({
       title: title.trim(),
       description: description.trim(),
       timer_preset: resolvePreset(timerSelection, customMinutes),
     });
     if (outcome.ok) {
       await hideWindow();
     } else {
       setError(outcome.message);
     }
+  } catch (error) {
+    setError(error instanceof Error ? error.message : "failed to create focus");
   } finally {
     setSubmitting(false);
   }
🤖 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/useNewFocusWindow.ts` around lines 69 - 82, The submit block
assumes focusWriter.createFocus always resolves; wrap the await
focusWriter.createFocus(...) call in a try/catch (inside the existing
try/finally) or add a nested try/catch so thrown exceptions are caught, call
setError with a sensible fallback message (e.g., "Failed to create focus" plus
the caught error message), and ensure you still call setSubmitting(false) in the
finally; also guard against outcome being undefined before checking outcome.ok
and calling hideWindow or setError. Reference: useNewFocusWindow.ts,
focusWriter.createFocus, setError, setSubmitting, hideWindow, resolvePreset.
🤖 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.

Outside diff comments:
In `@src/hooks/useNewFocusWindow.ts`:
- Around line 69-82: The submit block assumes focusWriter.createFocus always
resolves; wrap the await focusWriter.createFocus(...) call in a try/catch
(inside the existing try/finally) or add a nested try/catch so thrown exceptions
are caught, call setError with a sensible fallback message (e.g., "Failed to
create focus" plus the caught error message), and ensure you still call
setSubmitting(false) in the finally; also guard against outcome being undefined
before checking outcome.ok and calling hideWindow or setError. Reference:
useNewFocusWindow.ts, focusWriter.createFocus, setError, setSubmitting,
hideWindow, resolvePreset.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 277fcc4b-a210-4d2f-8ede-9c69e1b54c26

📥 Commits

Reviewing files that changed from the base of the PR and between ac6c93e and f47bcc6.

📒 Files selected for processing (8)
  • src/components/NewFocusWindow.tsx
  • src/components/PigDetail.test.tsx
  • src/components/PigDetail.tsx
  • src/components/TimerPresetPicker.test.tsx
  • src/components/TimerPresetPicker.tsx
  • src/hooks/useNewFocusWindow.ts
  • src/lib/timerPreset.test.ts
  • src/lib/timerPreset.ts

@archae0pteryx

Copy link
Copy Markdown
Contributor Author

Declining the latest outside-diff CodeRabbit finding (createFocus could throw): the FocusWriter contract (introduced in #52 / issue 041) guarantees that no writer method ever rejects — runInvoke wraps every Tauri invoke in try/catch and toFailure always returns a WriteOutcome. Adding a top-level try/catch in useNewFocusWindow would be defensive code for a scenario the type system makes impossible, which CLAUDE.md explicitly says to avoid ("Don't add error handling, fallbacks, or validation for scenarios that can't happen").

@archae0pteryx
archae0pteryx merged commit cdf82d1 into main May 12, 2026
2 checks passed
@archae0pteryx
archae0pteryx deleted the timer-picker-ui branch May 12, 2026 04:32
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.

1 participant