Developer reference for the TimerState shape, state transitions, and session intent lifecycle.
Defined in src/shared/types.ts:
| Field | Type | Description |
|---|---|---|
mode | "idle" | "working" | "breaking" | Current timer mode |
workStartTime | number | null | Epoch ms when the current work block began; null when not working |
breakStartTime | number | null | Epoch ms when the current break block began; null when not breaking |
savedBreakMinutes | number | Accumulated unused break time (minutes). Grows when work ends, shrinks when break time is consumed |
breakFraction | number | Ratio of break-to-work (default 1/3). Persists across resets |
totalWorkTime | number | Cumulative work minutes finalized at mode transitions |
totalBreakTime | number | Cumulative break minutes finalized at mode transitions |
sessionStartedAt | number | null | Epoch ms when the session first started (set on the first startWork from idle); used as the upsert key for session history |
All transition functions live in src/frontend/utils/timer-actions.ts. Each returns a TimerActionResult containing the new state and an optional notification.
| Action | Valid from | Mode after | Key field changes |
|---|---|---|---|
startWork | idle, breaking | working | workStartTime = now, breakStartTime = null, sessionStartedAt = now (only if was idle) |
takeBreak | working | breaking | Earned break = workDuration * breakFraction; savedBreakMinutes += earned; totalWorkTime += workDuration; breakStartTime = now; workStartTime = null |
resumeWork | breaking | working | Consumed break = min(breakDuration, savedBreakMinutes); totalBreakTime += consumed; savedBreakMinutes -= breakDuration (floored at 0); workStartTime = now; breakStartTime = null |
handleInterruption | working | breaking | Same accumulation as takeBreak (earned break added to savedBreakMinutes, totalWorkTime updated). Represents an unplanned break |
takeBigBreak | working, breaking | breaking | If was working: totalWorkTime += workDuration. savedBreakMinutes = 0 (all saved break consumed); breakStartTime = now; workStartTime = null |
resetSession | any | idle | All timing fields zeroed: workStartTime = null, breakStartTime = null, savedBreakMinutes = 0, totalWorkTime = 0, totalBreakTime = 0, sessionStartedAt = null. breakFraction is preserved |
totalWorkTime and totalBreakTime only grow at mode transitions (when an action function is called). They never grow continuously.
While the timer is running, the in-progress block (time since workStartTime or breakStartTime) is not reflected in totalWorkTime/totalBreakTime within the state object. UI components and session-history writers compute this on the fly:
currentWorkTime = totalWorkTime + (now - workStartTime) / 60000 // if mode === "working"
currentBreakTime = totalBreakTime + min((now - breakStartTime) / 60000, savedBreakMinutes) // if mode === "breaking"
The break cap (min(..., savedBreakMinutes)) ensures break time only counts up to the amount that was earned.
This pattern appears in three places:
TimerContainerstats calculation (src/frontend/components/TimerContainer.tsx,statsuseMemo)- Backend live upsert (
src/backend/index.ts,POST /api/state) - Frontend reset/rollover (
src/frontend/index.tsx,resetSessionand day-rolloveruseEffect)
A session intent is a short user-entered string describing what they plan to work on. It is stored in localStorage, not in TimerState.
- Key:
timer_session_intentin localStorage - Counter:
timer_breaks_since_intenttracks how many breaks have occurred since the intent was set - Functions:
getSessionIntent,setSessionIntent,getBreaksSinceIntent,setBreaksSinceIntent,incrementBreaksSinceIntentinsrc/frontend/utils/passphrase.ts
| User action | Effect on intent | breaksSinceIntent |
|---|---|---|
startWork | Set to input value (or removed if blank) | Reset to 0 |
takeBreak | Unchanged | Incremented by 1 |
resumeWork | Unchanged | Unchanged |
handleInterruption | Cleared (null) | Reset to 0 |
takeBigBreak | Cleared (null) | Reset to 0 |
| Clear intent button | Cleared (null) | Reset to 0 |
| Submit intent while working | Set to input value | Reset to 0 |
resetSession | Not explicitly cleared | Not explicitly reset |
After handleInterruption and takeBigBreak, the intent input auto-focuses so the user can set a new intent before resuming work.
The intent is not explicitly cleared on resetSession or day rollover. It persists in localStorage until the next startWork overwrites it or the user clears it manually. In practice, every new work session begins with startWork, which always writes the intent.
When session history is written (via POST /api/state or POST /api/history), the current sessionIntent from localStorage is included. This means the history record reflects whatever intent was active at write time, not necessarily the intent that was set at session start.
src/shared/types.ts—TimerState,SessionStats,GroupMemberStatetype definitionssrc/frontend/utils/timer-actions.ts— all state transition functionssrc/frontend/components/TimerContainer.tsx— wires actions to UI, computes live statssrc/frontend/utils/passphrase.ts— intent andbreaksSinceIntentlocalStorage helpers