Kumbatio
Concepts

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):

  1. Sessions auto-expire. Expiry is an emitted event, never a predicate your app must remember to poll - suppression can never outlive the session.
  2. 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 level

While 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. At 0 the 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 at 25, no nudges at 100, 75, or 0.

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'
EventWhen
startA session began (starting while one is active stops the old one first)
breakA break nudge is due - recurring while the session runs
endThe session reached endsAt and auto-expired
stopThe 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.