Focus Sessions
Time-boxed suppression windows with auto-expiry, break nudges, and energy-derived defaults.
A focus session is a temporary "one thing at a time" commitment layered on top of the energy model - it suppresses interruptions for a bounded duration, surfaces break nudges, and always ends on time. It is not an energy level; it's a window.
Two invariants, by construction
Both come from field evidence (a shipped ADHD email client got both wrong, and both were user-hostile):
- Sessions auto-expire. Expiry is an emitted event, never a predicate your app must remember to poll - suppression can never outlive the session.
- Suppression lifts before the end event fires. An end-of-session notification can never be swallowed by the session's own suppression.
Basic usage
import {
createEnergyEngine,
createNotificationGate,
createFocusSessionController,
} from '@kumbatio/energy-system'
const engine = createEnergyEngine({ initialLevel: 75 })
const gate = createNotificationGate(engine, {
onDeliver({ notifications }) { showToasts(notifications) },
})
const focus = createFocusSessionController({ engine, gate })
focus.subscribe((event, session) => {
if (event === 'break') showBreakNudge()
if (event === 'end') showSessionSummary(session)
})
focus.start() // duration and break cadence default from the current energy levelWhile a session runs, the controller holds the gate's suppression flag; on stop, end, or dispose it releases it. The controller owns the flag for the session's lifetime, so it can never be left stuck on.
Energy-derived defaults
With an engine supplied, start() derives its defaults from the current level:
- Duration - the level's expected productivity window: 120 / 90 / 45 / 25 minutes at
100/75/50/25. At0the window is 0, so an explicit session falls back to 25 minutes rather than expiring instantly. - Break cadence - the task-complexity guidance: every 45 minutes at
50, every 25 at25, no nudges at100,75, or0.
Without an engine: 25 minutes, no breaks. Override either explicitly:
focus.start({ durationMinutes: 50, breakEveryMinutes: 20 })A break nudge that would land at or after the session end is skipped - the end event already tells the user to step away.
Lifecycle events
type FocusSessionEvent = 'start' | 'break' | 'end' | 'stop'| Event | When |
|---|---|
start | A session began (starting while one is active stops the old one first) |
break | A break nudge is due - recurring while the session runs |
end | The session reached endsAt and auto-expired |
stop | The session was ended manually before endsAt |
Every listener receives the session snapshot: { startedAt, endsAt, breakIntervalMs }.
Inspecting a session
focus.getSession() // FocusSession | null
focus.remainingMs() // 0 when idle
import { sessionRemainingMs, isSessionExpired } from '@kumbatio/energy-system'
sessionRemainingMs(session) // pure helpers, take an optional `now`
isSessionExpired(session)Controller options
Prop
Type
dispose() stops any active session first (releasing suppression and emitting stop), then tears down listeners.
Related
- Notification gate - what suppression actually does to notifications
- Energy state - the metrics behind the defaults
