Pomodoro
The built-in pomodoro timer: focus blocks and breaks, presets, schedules that auto-start at a wall-clock time, and the today/week/by-project stats. Covers where the Pomodoro tab lives, how a run advances and pauses, how it pairs with Focus Mode, and what it deliberately does not do.
What it is
A built-in pomodoro timer that runs focus blocks (default 25 min) followed by breaks (5 min short, 15 min long every 4th cycle). You pick a preset — a named bundle of those durations plus alarm overrides and an optional default project — click Start, and Omniscio drives a single active run from focus → short-break → focus → … → long-break → … → complete, chiming at each phase change.
The feature is paired with the Alarms virtual project in the Omniscio sidebar group, accessed via the Pomodoro tab of the Alarms project's pane-2 tab control.
When the (in-development) Time Tracker is enabled, each completed focus session also appears on its unified timeline as a
focusblock — a one-way projection that leaves the Pomodoro engine untouched. See time-tracker.md.
Two side-projects on top of "just start a timer":
- Schedules auto-start a preset at a recurring wall-clock time ("every weekday at 9 AM, start the Deep Work preset").
- Stats roll up the focus seconds into Today / This Week / by project, with a "planned vs actual" today block.
The whole feature is local-only — nothing leaves your machine, no AI is involved at any layer, no cost. The only cross-feature integration is with Alarms (shared sound picker, focus-mode pierce, foregrounding) and with Projects (the optional project attribution).
Where to find it
The Pomodoro feature is rendered inside the Alarms virtual project, not as its own sidebar row. Open the Alarms virtual project in the Omniscio sidebar group, and at the top of pane 2 there is a tab control with Alarms / Pomodoro tabs — click Pomodoro to switch the rest of the project panel to the pomodoro UI.
Inside the Pomodoro tab:
- Pane 2 (sidebar) — the preset list with a per-row Play button for quick-start. Header has three icon toggles: a calendar-clock icon (Schedule editor), a history icon (History view), and a gear (Settings panel). Clicking a preset row selects it and loads its editor into pane 3; clicking Play on a row starts a run immediately using that preset's default project (if set).
- Pane 3 (detail) — when a preset is selected, the preset editor: name, focus minutes, short-break minutes, long-break minutes, cycles-until-long-break, total cycles (or "unlimited"), default project (dropdown of all your Omniscio projects + "Unassigned"), and an Advanced section with the per-preset alarm overrides (sound, modal vs. banner assertiveness, bring-to-foreground, pierce-focus-mode). When the Schedule toggle is on, pane 3 swaps to the Schedule editor (list of recurring rows, each with day-of-week chips + HH:MM time + preset picker + project picker + label + enabled toggle); when the History toggle is on, pane 3 swaps to the History view (today / week / by project rollup + a planned-vs-actual block). When a run is active, pane 3 also shows the Session view above the editor: big countdown, phase label, Pause / Resume, Skip Phase, and Stop buttons. Skip Phase advances immediately to the next phase in the ladder (focus → break, break → focus) without waiting for the countdown to reach zero; it counts the phase as completed (focus seconds for the phase are added even though you skipped early).
The "Pomodoro is a tab of Alarms" layout is deliberate — it keeps both notification-surface features (timers + reminders) under one cognitive heading and lets them share the same sound picker, foregrounding rules, and focus-mode interplay without duplicating settings UI.
How it behaves
How to start a run
- Open the Alarms virtual project in the Omniscio sidebar.
- Click the Pomodoro tab at the top of pane 2.
- Click the Play button on any preset row in pane 2 (or select the preset and click Start in pane 3). The run begins immediately — pane 3 swaps to show the Session view with a live countdown.
If the preset has a Default project set, the run is attributed to that project automatically (stats roll up under it). You can clear the default project on the preset to be prompted/unassigned, or override per-run by starting from a different preset.
Phase transitions
The active run advances itself. A 1-second background tick checks whether the current phase has ended; when it has, Omniscio transitions atomically and emits a push to the renderer, which:
- Plays the chime for the resolved sound (per-preset override or the Alarms-default sound).
- Shows a transition toast with the new phase label and remaining time.
- Brings the Omniscio window to the foreground only if both layers say so — see "Foregrounding" below; the default is no for phase changes.
The phase ladder is:
- focus → short-break when
cyclesCompleted % cyclesUntilLongBreak !== 0. - focus → long-break when
cyclesCompleted % cyclesUntilLongBreak === 0. - break → focus always (the cycle counter advances on the focus → break edge, so the break never re-counts).
- focus → complete when
cyclesCompleted >= totalCycles(the preset'stotalCyclesis nullable —nullmeans unlimited, so the run only ends when you click End).
Pause / resume / skip / stop
The Session view has four controls below the countdown:
- Pause freezes the countdown without losing time. Pause records the
moment you paused in memory; Resume extends
phase_ends_atby the pause duration and persists the new value, so the remaining time after resume matches what you saw when you paused. - Skip Phase advances immediately to the next phase (no confirmation — it's a discrete state transition). The skipped phase is counted as completed; its focus-seconds (if it was a focus phase) are still added to the stats rollup.
- Stop ends the entire run after a confirm dialog (to avoid accidental loss of cycles).
Caveat: pause state is in-memory. If you close Omniscio while a run is paused,
the run is reaped as crashed on the next boot — pausing isn't crash-safe.
This trade-off avoids a paused_at DB column for what is normally a few
minutes of pause; the alternative is to End the run before closing Omniscio.
Foregrounding (an important deliberate split)
Three settings interact at phase-change time:
| Setting | Default | What it does |
|---|---|---|
pomodoroPhaseForeground |
false | Whether ANY phase change can pull Omniscio's window to the foreground. |
alarmsBringToForeground |
true | The Alarms feature's foregrounding default (NOT consulted here). |
per-preset bringToForegroundOverride |
null | When non-null, overrides pomodoroPhaseForeground for THIS preset. |
The deliberate split: a 5-minute focus block ending should chime + show a
tray badge but not yank you out of whatever you were doing — so the
pomodoro default is false. The alarm default is true (an alarm exists
precisely to interrupt). Per-preset bringToForegroundOverride is the
escape hatch when you want a particular preset to pierce.
If you change alarmsBringToForeground you do not change pomodoro
behavior. They're independent, and the Settings UI labels both clearly.
Focus Mode integration (auto-silence during focus blocks)
A pomodoro run can automatically enable Focus Mode — the toolbar bell pill that batches inbox notifications — while you're in a focus block, then release it during breaks. The intent is to pair "I'm starting a focus timer" with "stop pinging me about every push notification" without forcing you to remember to click the bell manually.
The setting
Settings → Notifications → Alarms → Auto-Enable Focus Mode During Focus Blocks
(pomodoroAutoEnableFocusModeDefault, default on) is the global default.
Every per-preset Auto-enable Focus Mode override (Advanced section of
the preset editor) is a tri-state:
| Override value | What it means |
|---|---|
null (default) |
Inherit the global pomodoroAutoEnableFocusModeDefault. |
true |
This preset always auto-enables Focus Mode, even when the global default is off. |
false |
This preset never touches Focus Mode, even when the global default is on. |
When Focus Mode flips during a run
When the run is in a state that warrants silence — actively in a focus phase — Focus Mode is on. Any other state (break, paused, ended) drops it. Concretely:
- Start a run → if a focus phase is the opening phase (it always is), Focus Mode is enabled.
- focus → short-break or focus → long-break → Focus Mode is disabled (you get your alerts back).
- break → focus (start of the next cycle) → Focus Mode re-enables.
- Pause during a focus phase → Focus Mode drops (pause is treated as a break — you're stepping away, so let notifications through).
- Resume after a pause → Focus Mode re-enables only if the run resumes into a focus phase.
- End the run (manually or by reaching
totalCycles) → Focus Mode drops back to whatever it was before the run started. - Omniscio crashes / closes → because Focus Mode state is settings-persisted
and the run row is marked
crashedon the next boot, there's no orphaned Focus Mode lock. If you had Focus Mode on for an unrelated reason before the run, that state is restored.
Manual-toggle protection
If you click the bell icon manually at any point during a managed run — either turning Focus Mode on when the run had dropped it, or off when the run had it enabled — Pomodoro stops managing Focus Mode for the rest of that run. The reasoning: your manual click is a deliberate statement ("actually, I want it the other way right now"), and the timer shouldn't override you on the next phase transition. The block lasts only for the current run; the next run starts fresh.
This is wired through the Pomodoro service's subscription to
focusModeService's change listener (onFocusModeExternalChange in
src/main/services/pomodoro-service.ts). Every change carries a source, and the
listener ignores its own emissions (event.source === 'pomodoro'), so a
programmatic enable/disable from the Pomodoro service itself never releases its own
claim; any other source — the bell-icon click
(src/main/ipc/focus-mode-handlers.ts),
the system, or startup — does.
Per-preset escape hatches
The per-preset Auto-enable Focus Mode override is the way to opt a single preset out (or in) without flipping the global default. Common shapes:
- "Deep Work" preset — override =
true. Always silences, even if you've globally disabled the integration. - "Quick admin pomodoro" preset — override =
false. Stays notification-permissive even when you've turned the integration on globally. - All other presets — override =
null. Follows the global default.
Schedules — auto-start at a wall-clock time
A schedule is a recurring row that calls Start for you. Open the Schedule editor from pane 2's calendar-clock icon, and add rows of:
- Day of week — chips for Sun / Mon / … / Sat; pick one day per row.
- Start time —
HH:MMin your local timezone. - Preset — dropdown of all your presets.
- Project (optional) — overrides the preset's default project for this schedule. Empty = use the preset's default. Set explicitly to "Unassigned" to opt out per-schedule.
- Label (optional) — surfaces in the run row and stats so you can tell apart "Mon 9am — Deep Work" from "Wed 9am — Writing".
- Enabled — toggle to mute the schedule without deleting it.
How the resolver works
A 1Hz background tick (the same one that drives phase transitions) evaluates every enabled schedule for the current minute. For each match:
- 30 seconds before the scheduled minute, Omniscio shows a pre-start toast
("Pomodoro starting in 30s — Deep Work") so you can dismiss it or just be
ready. Toasts are per-
(schedule, minute)so a re-render doesn't double up. - At the scheduled minute exactly, Omniscio atomically claims the minute and calls Start. If another run is already active (you manually started one, or the previous schedule's run hasn't ended yet), the new schedule is skipped — manual starts always win, no displacement.
- Idempotency: a DB column
last_fired_minuteon the schedule row stores the last minute it fired. The atomic SQL UPDATE that claims a minute also bumps this column, so two concurrent ticks (or a restart mid-minute) can never both fire the same schedule. Lose-on-restart never happens — if Omniscio crashed at 8:59:30, the resolver after restart at 9:00:05 still fires the 9 AM schedule, becauselast_fired_minute < 540(9 AM in minute-of-day). Resume in the same minute = idempotent skip.
The master kill switch
Settings → Notifications → Alarms → Pomodoro: Enable schedules (pomodoroScheduleEnabled,
default on) controls whether the resolver runs at all. When off, no
pre-start toasts, no auto-starts, no schedule-related DB reads — but
manually-started runs continue ticking normally. This is the
"silence the schedule" toggle when you want to keep your preset library and
schedule rows intact but stop the auto-fires (a vacation week, a deadline
crunch, etc).
Individual schedule rows also have their own enabled toggle so you can
silence just one without flipping the master.
Stats — Today, This Week, by Project
Open the History view from pane 2's history icon. Three rollups:
- Today — sum of
focus_seconds_totalacross runs started today, in your local timezone (resets at local midnight). The engine incrementsfocus_seconds_totalon every focus-phase completion, so a run you stopped mid-break still contributes the focus seconds from the phases that finished before you stopped. Phases skipped via Skip Phase also count as completed. - This Week — same sum since the most recent Monday 00:00 local.
- By Project — table with rows for each
projectIdthat has any focus seconds this week, sorted DESC by weekly total. Rows include today and weekly columns. Runs withprojectId: nullaggregate under an "Unassigned" bucket (never silently dropped — see invariant I6 in the contract).
Planned vs actual
Above the rollups is a "Today's planned vs actual" block:
- Planned sums today's enabled schedules × their preset's
focusMinutes. E.g. 4 schedules today at 25 min each = 100 planned focus minutes. - Actual is the same number as the "Today" rollup (completed focus seconds, converted to minutes).
This is a rough target — the planned number is what you'd accomplish if you ran every scheduled session to completion. Schedules that haven't fired yet still count toward "planned"; if you delete or disable a schedule, it drops out of planned immediately. Soft-deleted presets and disabled schedules are both excluded.
Presets — what you can customize
Each preset row in pane 2 corresponds to a row in pomodoro_presets. The
editor lets you set:
| Field | Default | Notes |
|---|---|---|
| Name | "Default" | Free text. Shown in the sidebar list, schedule rows, history. |
| Focus minutes | 25 | The focus phase duration. |
| Short-break minutes | 5 | The short-break phase duration. |
| Long-break minutes | 15 | The long-break phase duration (every Nth cycle). |
| Cycles until long break | 4 | After this many focus-block completions, the next break is long. |
| Total cycles | null | null = unlimited (only ends on End). Number = run completes at N. |
| Default project | null | Pre-fills the run's projectId when Start is clicked from this preset. |
| Auto-start breaks | true | Editor toggle, currently inert — see "Known gap" below. |
| Auto-start focus | false | Editor toggle, currently inert — see "Known gap" below. |
| Sound override (Advanced) | null | Per-preset chime; null = inherit alarmsDefaultSound. |
| Assertiveness (Advanced) | null | modal / banner / silent-banner; null = inherit. |
| Bring-to-foreground (Advanced) | null | null = inherit pomodoroPhaseForeground (default false). |
| Pierce focus-mode (Advanced) | null | null = inherit alarmsPierceFocusMode (default false). |
| Auto-enable Focus Mode (Advanced) | null | null = inherit pomodoroAutoEnableFocusModeDefault (default on). |
The null = inherit pattern means a single change at Settings → Notifications → Alarms
propagates to every preset that didn't override, while presets that did
override are immune (intentionally so — you set them up that way).
Presets soft-delete (is_deleted = 1) when removed, so any run rows that
reference them in stats keep working. A soft-deleted preset is silently
dropped from the sidebar, the schedule's preset dropdown, and the planned
rollup.
Known gap — Auto-start toggles are inert
The Auto-start breaks and Auto-start focus toggles render in the
preset editor (and persist to pomodoro_presets.auto_start_breaks /
auto_start_focus), but the engine's transition() in
src/main/services/pomodoro-service.ts
doesn't read them — every phase change unconditionally starts the next
phase. In other words, both toggles behave as if they were always on for
breaks and always on for focus. Flipping either toggle changes the stored
value but produces no observable behavior. This is a UI/engine wiring gap,
not a feature decision — track it as such if you see it.
Settings
All Pomodoro settings live under Settings → Notifications → Alarms (the section is shared with Alarms; the Pomodoro subsection groups its own toggles). Three settings are exclusive to Pomodoro:
pomodoroScheduleEnabled(default on) — the master kill switch described above. Hides nothing in the UI; only stops the resolver.pomodoroPhaseForeground(default off) — whether phase-change transitions can pull Omniscio to the foreground. Default off because a focus block ending shouldn't yank you out of your deep-work window.pomodoroAutoEnableFocusModeDefault(default on) — whether a focus block automatically enables Focus Mode for the duration of that phase (releases on breaks / pause / end). See "Focus Mode integration" above for the per-run lifecycle and manual-toggle protection.
Three settings are inherited from Alarms when a preset has the matching
override set to null:
alarmsDefaultSound→ preset'ssoundIdalarmsAssertiveness→ preset'sassertivenessOverridealarmsPierceFocusMode→ preset'spierceFocusModeOverride
(alarmsBringToForeground is NOT inherited — see "Foregrounding" above.)
What it deliberately doesn't do
- No cross-device sync. Stats and runs are local SQLite. If you run Omniscio on two machines, they each have their own pomodoro history.
- No automatic break enforcement. If you skip a break by manually ending the run mid-break and starting a fresh focus, the engine doesn't try to stop you. The chime + tray badge are the nudge; the rest is on you.
- No "tomato" gamification. No streaks, no leaderboards, no badges. The stats are a usage record, not a habit-tracking layer.
- No mobile pomodoro controls. The Pomodoro tab is desktop-only. The mobile sidebar shows the Alarms virtual project, and Alarms surfaces are still reachable on phone — but starting / pausing / configuring presets and schedules is desktop only.
- No project picker per-run. The Start button always uses the preset's default project (or no project, if unset). To change per-run project, edit the preset's default or set a schedule with an override.
- No tray-icon countdown overlay. The tray badge updates on phase changes, but the seconds count down only in the Session view inside Omniscio.
Where to look when it goes wrong
- A schedule didn't auto-start at the scheduled minute — first check
Settings → Notifications → Alarms → Pomodoro: Enable schedules is on (the master
kill switch). Then check the schedule row's
enabledtoggle. Then check whether a different run was already active at the scheduled minute (manual starts win — schedules don't displace active runs). Finally check the schedule's day-of-week — Mon=1, …, Sun=0 in storage. - The pre-start toast fired but the auto-start didn't — most likely a
run was already active. The toast fires unconditionally in the 30s window;
the actual auto-start checks
getActiveRun()first. - A phase change played the wrong sound — the resolution is per-preset
soundId(non-null) →alarmsDefaultSound(Settings → Notifications → Alarms). If the preset'ssoundIdis non-null, the global default is bypassed. - Omniscio pulled itself to the foreground when I didn't want it to — check
the preset's
bringToForegroundOverride. If non-null, it's overridingpomodoroPhaseForeground. Set it back tonullto inherit the (off) default. - Focus Mode didn't enable when my focus block started — first check
Settings → Notifications → Alarms → Auto-Enable Focus Mode During Focus Blocks is on.
Then check the preset's per-preset Auto-enable Focus Mode override —
if it's
false, this preset is opted out. If you toggled the bell icon manually during the run, the service stops managing Focus Mode for the rest of that run (deliberate — your click wins); restart the run to re-enable management. - Focus Mode stayed on after my run ended — only happens if you toggled the bell manually during the run (the service handed control back to you and didn't drop it on end). Click the bell to disable.
- My stats look wrong — the Today rollup uses your local timezone
midnight; the Week rollup uses the most recent local Monday 00:00. If
you've recently changed timezones, "today" can include yesterday's runs
(or vice versa). The rollup queries are in
src/main/db/queries-pomodoro.ts —
search for
getPomodoroStats. - An active run survived an Omniscio restart and now nothing can start —
this shouldn't happen because
reapStaleActiveRuns()runs at service boot and marks any leftoveractive/pausedrows asend_reason = 'crashed'. If it does, manually flipping the row'sended_atin the DB clears the lock, but file a bug — the reaper has a hole.
For agents
CLI access
Pomodoro is fully reachable from the CLI control server (127.0.0.1:19519),
so an external agent or script can drive a run, edit your presets and schedules,
and read your stats headlessly:
- Runs —
POST /pomodoro/runs/start,POST /pomodoro/runs/:id/pause,POST /pomodoro/runs/:id/resume,POST /pomodoro/runs/:id/skip-phase,POST /pomodoro/runs/:id/stop,GET /pomodoro/runs/active,GET /pomodoro/runs/history. - Presets —
GET/POST /pomodoro/presets,PATCH/DELETE /pomodoro/presets/:id,POST /pomodoro/presets/:id/set-default. - Schedules —
GET/POST /pomodoro/schedules,PATCH/DELETE /pomodoro/schedules/:id,POST /pomodoro/schedules/:id/toggle. - Stats —
GET /pomodoro/stats(Today / This Week / by-Project).
Two of those are DELETEs, so an agent you point at this family can remove a saved preset or schedule. The routes are registered unconditionally (pomodoro has no unreleased-feature flag) and take the usual CLI bearer token.
The desktop/mobile gap is in the UI, not the API: the Pomodoro tab itself
does not render on a paired phone, as noted above — but the routes above answer
headlessly either way, and GET /pomodoro/stats is the supported way to read
those numbers rather than opening the local SQLite file.
Architecture pointers (for code edits)
If you're editing the feature, read the contract first: .claude/memory/contracts/pomodoro-contract.md — it names the test that locks each invariant, the state machine, and the "changing the engine" checklist.
The major files are:
- DB schema migration v230 in
src/main/db/database.ts — adds the
project_idcolumn topomodoro_runs, thedefault_project_idcolumn topomodoro_presets, and thepomodoro_schedulestable. - DB queries in
src/main/db/queries-pomodoro.ts —
every CRUD operation,
claimScheduleMinute()for the atomic firing gate,getPomodoroStats()for the rollups. - Service in src/main/services/pomodoro-service.ts — the 1Hz tick, phase transitions, schedule resolver, and the foregrounding resolution.
- IPC handlers in src/main/ipc/pomodoro-handlers.ts.
- Renderer store in src/renderer/src/stores/pomodoro-store.ts — caches presets, schedules, stats, active run; subscribes to pushes.
- Renderer UI under
src/renderer/src/features/alarms/ —
PomodoroSidebar.tsx,PomodoroPresetEditor.tsx,PomodoroScheduleEditor.tsx,PomodoroSessionView.tsx,PomodoroHistoryView.tsx,PomodoroPhaseTransitionToast.tsx,PomodoroSchedulePrestartToast.tsx.
Related
Alarms is the paired feature in the same virtual project, sharing the sound picker, the foregrounding helper, and the focus-mode pierce. Focus Mode is the batching gate that pomodoro chimes route through, and a preset's Pierce Focus Mode is the bypass around it. And Use Recipes covers recipes, which run on agent time rather than wall-clock time — if you want "every Monday at 9 AM, spawn an agent" instead of "every Monday at 9 AM, start a focus timer", use a cron job, not a pomodoro schedule.
Last verified 2026-10-06