Skip to content

Implement Pomodoro timer/focus session feature - #34

Merged
jermzblake merged 25 commits into
mainfrom
pomodoro-timer
Apr 6, 2026
Merged

Implement Pomodoro timer/focus session feature#34
jermzblake merged 25 commits into
mainfrom
pomodoro-timer

Conversation

@jermzblake

Copy link
Copy Markdown
Owner

This pull request introduces a Pomodoro/focus session feature to the client application, allowing users to start and control focus timers directly from the UI. The main changes include adding a Pomodoro timer panel, integrating focus session controls into task details, and introducing supporting components and hooks.

Pomodoro Timer Feature Integration:

  • Added a global PomodoroProvider and PomodoroPanel to the App component, enabling Pomodoro timer state and controls across the application. (src/client/App.tsx)
  • Implemented the PomodoroPanel component, providing a UI for starting, pausing, resetting, and ending Pomodoro or Flowtime sessions, with support for rest duration selection and mode switching. (src/client/components/pomodoro-panel.tsx)

Task Focus Session Controls:

  • Integrated a Pomodoro/focus session action into the task details sidebar, allowing users to start a timer for a specific task and select the desired focus mode. (src/client/components/task-details-content.tsx)
  • Added the TaskFocusSessionAction component, which provides a button and mode selector for starting a focus session from a task. (src/client/components/task-focus-session-action.tsx)
  • Introduced the useTaskPomodoroAction hook for managing state and actions related to starting Pomodoro sessions for tasks. (src/client/hooks/use-task-pomodoro-action)

UI Component Enhancements:

  • Created a reusable ButtonGroup component for grouping buttons and selectors in a visually consistent way, supporting both horizontal and vertical orientations. (src/client/components/ui/button-group.tsx)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a client-side Pomodoro / focus session feature with a global timer panel, task-level “start focus session” controls, and supporting context/hooks/components, along with initial unit tests.

Changes:

  • Introduces a global PomodoroProvider + PomodoroPanel to manage and display focus sessions across the app.
  • Adds task details sidebar controls to start/restart a focus session for a specific task and choose a focus mode.
  • Adds supporting UI (ButtonGroup), hooks, alarm/notification helper, and test coverage for the new modules.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/client/App.tsx Wraps app with PomodoroProvider and mounts the global PomodoroPanel.
src/client/context/pomodoro-context.tsx Implements Pomodoro/Flowtime state machine, ticking, mode config, and actions.
src/client/hooks/use-kitchen-timer-alarm.ts Adds Web Audio + Notification alarm helper for phase transitions.
src/client/components/pomodoro-panel.tsx Provides the collapsible timer UI and controls.
src/client/hooks/use-task-pomodoro-action.ts Encapsulates per-task “start/restart session” behavior (with confirm-on-replace).
src/client/components/task-focus-session-action.tsx Renders task-level focus session CTA + mode selector.
src/client/components/task-details-content.tsx Integrates the task focus session action into the task details sidebar.
src/client/components/ui/button-group.tsx Adds a reusable component to visually group buttons/selects.
src/client/tests/pomodoro-context.test.tsx Tests basic session start mode selection + mode-change restrictions.
src/client/tests/use-task-pomodoro-action.test.tsx Tests per-task session start/restart and confirm-to-replace behavior.
src/client/tests/task-focus-session-action.test.tsx Tests rendering and disabled/click behavior of the task action UI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/client/context/pomodoro-context.tsx Outdated
Comment thread src/client/context/pomodoro-context.tsx Outdated
Comment thread src/client/components/pomodoro-panel.tsx Outdated
Comment thread src/client/components/ui/button-group.tsx
Comment thread src/client/tests/use-task-pomodoro-action.test.tsx Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +185 to +206
// Phase complete — trigger alarm and transition
const currentState = getState()
const alarmMsg =
prev.phase === 'work'
? `Work phase complete for "${currentState.taskName ?? 'task'}". Time to rest!`
: `Rest complete for "${currentState.taskName ?? 'task'}". Ready for the next interval!`

playAlarm(alarmMsg)

if (prev.phase === 'work') {
// Transition to rest
const restSeconds = mode.restSeconds ?? prev.flowtimeRestMinutes * 60
return {
...prev,
...elapsedUpdate,
phase: 'rest',
secondsRemaining: restSeconds,
}
} else {
// Rest complete → go idle, keep elapsed totals, reset for next round
clearTick()
return {

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

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

startTick triggers side effects (playAlarm and clearTick) inside the setState functional updater. React expects state updaters to be pure; in StrictMode/dev or under update rebasing this can fire alarms multiple times or clear intervals unexpectedly. Move these side effects out of the updater (e.g., compute the next state in the interval callback, then call playAlarm/clearTick based on the transition, or trigger them from a useEffect that watches phase/status transitions).

Copilot uses AI. Check for mistakes.
Comment on lines +204 to +212
// Rest complete → go idle, keep elapsed totals, reset for next round
clearTick()
return {
...prev,
...elapsedUpdate,
status: 'idle',
phase: 'work',
secondsRemaining: mode.workSeconds,
}

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

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

When rest completes, the state transitions to idle but keeps taskId/taskName and (for Flowtime) leaves flowtimeWorkSeconds unchanged. This makes the panel show a stale +MM:SS stopwatch value after rest finishes, even though a new work interval hasn’t started. Consider resetting flowtimeWorkSeconds (and/or clearing taskId/taskName if an idle state should mean “no active session”).

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +33
const hasActiveSession = state.taskId !== null
const isDifferentTask = hasActiveSession && state.taskId !== task.id

if (isDifferentTask) {
const activeTaskName = state.taskName ?? 'the current task'
const confirmed = window.confirm(`Replace current Pomodoro session for "${activeTaskName}"?`)
if (!confirmed) return
}

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

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

hasActiveSession is derived only from state.taskId !== null, but the context can be status: 'idle' while keeping taskId set (e.g., after rest completes). That will prompt to “replace current session” even when no timer is running. Use state.status !== 'idle' (or similar) as part of the active-session check so confirmation only happens for running/paused sessions.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This is wanted behaviour for now

Comment thread src/client/App.tsx
// Only allow changing mode when idle or paused
setState((prev) => {
if (prev.status === 'running') return prev
return { ...prev, mode }

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

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

setMode updates only mode when status is paused/idle, but it leaves secondsRemaining (and phase) untouched. This can put the timer into an inconsistent state (e.g., paused with “Standard” selected but remaining time from the previous mode), and subsequent transitions/rest durations will use the new mode’s config. If mode switching while paused is intended, update the timing fields to match the newly selected mode (or disallow mode changes once a session has started, even if paused).

Suggested change
return { ...prev, mode }
const nextMode = POMODORO_MODES[mode]
return {
...prev,
mode,
phase: 'work',
secondsRemaining: nextMode.workSeconds ?? 0,
flowtimeWorkSeconds: nextMode.workSeconds === null ? 0 : prev.flowtimeWorkSeconds,
}

Copilot uses AI. Check for mistakes.
@jermzblake
jermzblake merged commit 571f9d4 into main Apr 6, 2026
7 checks passed
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.

2 participants