Vibecode Session
track this build4 phases, 12 steps, beginner friendly0%Session's solo core is compact: build a local desktop utility that captures the essential focus timer workflow with keyboard-first controls and local storage. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on polish, OS integration.
You are building a lean indie version of Session. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Session · indie build A local focus timer whose hard problems are measuring elapsed time correctly when the machine sleeps, never counting an abandoned session as completed, and being honest about what app blocking a normal application can and cannot do. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, and a tray presence | a timer you have to find a window for is a timer you stop using | | Storage | SQLite, sessions stored as intervals rather than a countdown | a remaining-seconds number cannot survive a crash | | Time | Wall-clock instants for scheduling, both clocks recorded, suspends detected | the monotonic clock's behaviour across sleep is not portable | | Input | Every action keyboard-reachable, with a global shortcut to start and stop | used dozens of times a day means never reaching for the mouse | | Blocking | Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more | a block that pretends to be enforcement is a lie you tell yourself | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface and runs the tests. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain** · free - Why: Tauri 2 needs it, and the tray, the global shortcut and the natively scheduled notification all live there. - Get it: rustup.rs, then the platform build dependencies Tauri lists. - Verify: cargo --version prints a version and a dev build shows a tray icon - [ ] **A machine you can actually put to sleep** · free - Why: The suspend behaviour is the central correctness problem here and it cannot be tested in a unit test, only by closing a lid and opening it again. - Get it: Any laptop, or a desktop you can suspend and wake on demand. - Verify: A session started before a suspend produces a suspends row after waking - [ ] **Your platform's documentation on restricting applications** · free - Why: What blocking can honestly mean differs completely by operating system, and writing this feature without reading that first produces either a placebo or a request for a permission you do not need. - Get it: Read the operating system vendor's own developer documentation on the relevant mechanism and permission. - Verify: You can state in one sentence what your platform allows and what it does not - [ ] **Your suspend and clock-change rule** · free - Why: Whether a session slept through counts as focus is a judgement that changes every number in the history, and it must be made before the history exists. - Get it: Write one paragraph covering suspend, forward clock change and backward clock change. - Verify: The paragraph is in the README and each case is named in a test ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Cross-device sync. - Per-application activity tracking. - Calendar integration. - Enforcement-grade blocking via privileged helpers. - cross-device sync - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates If one of those is essential to you, that is the reason to keep paying for Session, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Session The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a focus timer that keeps honest time across a closed lid, a changed clock and a crash to replace Session. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, and a tray presence · a timer you have to find a window for is a timer you stop using - Storage: SQLite, sessions stored as intervals rather than a countdown · a remaining-seconds number cannot survive a crash - Time: Wall-clock instants for scheduling, both clocks recorded, suspends detected · the monotonic clock's behaviour across sleep is not portable - Input: Every action keyboard-reachable, with a global shortcut to start and stop · used dozens of times a day means never reaching for the mouse - Blocking: Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more · a block that pretends to be enforcement is a lie you tell yourself ### Data model (create this before Phase 1) - sessions(id, intent, planned_seconds, started_at_utc, ended_at_utc, outcome, tag) · outcome is completed, stopped, abandoned or interrupted - intervals(session_id, ordinal, kind, started_at_utc, ended_at_utc, monotonic_start_ms, monotonic_end_ms) · kind is focus, pause or suspend - suspends(session_id, detected_at_utc, wall_gap_s, monotonic_gap_s, resolution) · when the two clocks disagree, this row records by how much - breaks(session_id, ordinal, planned_seconds, started_at_utc, ended_at_utc, skipped) - blocklist(id, pattern, kind, enabled) · kind names the mechanism, because what is possible differs per platform - days(date, focus_seconds, completed_sessions, abandoned_sessions) · derived, rebuildable from intervals - There is no remaining-seconds column anywhere. Remaining is computed from the stored instants on every tick, which is why a crash costs nothing. ### Phase 1 · Time that survives a closed lid Build: Store started_at as a wall-clock instant in UTC plus the planned duration, and compute remaining on every tick. Record a monotonic reading alongside each wall-clock one. On every tick compare the two gaps: a wall gap much larger than the monotonic gap means the machine slept, and that difference is written to the suspends table with a stated resolution rule. Trap: Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. Done when: A session running across a real suspend produces a suspends row with both gaps recorded and resolves according to the documented rule, verified by actually sleeping the machine. Changing the system clock forward during a session does not complete it early, and the change is recorded rather than absorbed, tested. Killing the application mid-session and reopening it restores the session with the correct remaining time computed from the stored instants. No code path anywhere stores or decrements a remaining-seconds value, verified by review. Do not build yet: No pauses, notifications, history or blocking. ### Phase 2 · Start, pause, stop, from the keyboard Build: Model start, pause, resume, stop and complete as appended interval rows rather than as edits to a session row. Bind a global shortcut for start and stop and give every action a key in the window. The tray shows the current state without opening anything. Trap: Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. Done when: Focus time for a session with three pauses equals the sum of its focus intervals and excludes the pauses, tested. Every action is reachable by keyboard with no pointer, checked by completing a full session using only the keyboard. The global shortcut starts and stops a session when the window is closed. A crash during a pause resumes in the paused state with the correct accumulated focus time. Do not build yet: No notifications, history, stats or blocking yet. ### Phase 3 · A chime that arrives on time Build: Compute the end instant when the session starts and schedule the notification against it from the native side. Recompute remaining on every window focus and on resume from sleep. Let the user choose sound, notification or both, and make it work when the window is hidden. Trap: Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. Done when: A session whose window is hidden for its whole duration still announces at the correct moment, tested by hiding the window and measuring. Returning to the window recomputes and displays the correct remaining time immediately, with no catch-up animation. Resuming from sleep after the end instant has passed announces immediately and marks the session according to the suspend rule. Notification preferences persist and a session with both sound and notification disabled still records correctly. Do not build yet: No history, stats or blocking yet. ### Phase 4 · History that does not flatter you Build: Record an outcome for every session: completed, stopped by the user, abandoned when the app was closed mid-session, or interrupted by a suspend past the rule's threshold. Show daily and weekly focus time computed from focus intervals, with abandoned sessions visible rather than excluded. Allow editing an outcome, recording that it was edited. Trap: Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately. Done when: A session whose application was closed before its end instant is recorded as abandoned and excluded from focus totals, tested. Daily totals recomputed from intervals match the stored daily rows exactly, asserted by rebuilding. An edited outcome is marked as edited with a timestamp, so the history stays honest about being adjusted. The history view shows abandoned and interrupted sessions rather than hiding them. Do not build yet: No blocking or export yet. ### Phase 5 · Blocking, described honestly, and everything exportable Build: Implement the blocking mechanism your platform genuinely supports, request the permission it needs through the documented flow, and label it for what it is. Where the only available mechanism is advisory, call it a nudge in the interface rather than a block. Never ask for a privileged permission without saying exactly what it is used for. Export sessions and intervals as CSV and JSON, and provide a wipe. Trap: Shipping a feature called blocking that edits a system file or closes a window, when any determined user reopens it in seconds and a permission prompt was needed to do even that, trades a real privilege for a placebo and teaches the user their tool lies to them. Find out what your platform's documented mechanism actually is, implement that, and name the feature after what it does. Done when: The blocking feature names its mechanism and its limits in the interface, not only in the README, and a reader can tell from the label whether it is enforcement or a nudge. Any permission is requested through the platform's documented flow, with the reason shown before the prompt, and the app works with the permission declined. Sessions and intervals export as CSV and JSON that open elsewhere, including outcomes and suspends. A wipe removes the database and derived files, verified by inspecting the data directory. Do not build yet: Nothing after this. ### Out of scope (and why) - Cross-device sync, which turns a single-file local app into a conflict-resolution problem for very little gain on a timer. - Deep per-application activity tracking, which is surveillance of yourself that you will not enjoy reading and which needs permissions disproportionate to a timer. - Calendar integration, which is a provider-specific timezone problem larger than this whole application. - Enforcement-grade blocking through privileged helpers or system extensions, which is a signed, notarised, separately-updated component and a project of its own. ### README must contain - That the timer stores instants and computes remaining, so a crash or a sleep never loses a session - What happens to a session that spans a system suspend, and that the rule was chosen deliberately - That the monotonic clock's behaviour across suspend is not guaranteed by the platform, with the Rust documentation cited - That an abandoned session is recorded as abandoned and excluded from focus totals - Exactly what the blocking feature does on each platform, which permission it needs, and what it cannot stop ===== AGENTS.md ===== # Agent instructions · Session indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, and a tray presence, SQLite, sessions stored as intervals rather than a countdown, Wall-clock instants for scheduling, both clocks recorded, suspends detected, Every action keyboard-reachable, with a global shortcut to start and stop, Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. - Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. - Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. - Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately. ===== BUILD_PLAN.md ===== # Build plan · Session A local focus timer whose hard problems are measuring elapsed time correctly when the machine sleeps, never counting an abandoned session as completed, and being honest about what app blocking a normal application can and cannot do. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Time that survives a closed lid A session's end is derived from recorded instants, and a suspend is detected and recorded rather than silently absorbed. ### Steps 1. Store instants, compute remaining The countdown is a rendering, not a state. Files: `src-tauri/src/session.rs`, `src/timer/remaining.ts` 2. Record both clocks on every tick Their disagreement is the only way to see a suspend. Files: `src-tauri/src/clocks.rs`, `tests/suspend-detection.test.ts` 3. Write the suspend rule down Does a slept twenty minutes count as focus? Decide once, in the README. Files: `docs/time-policy.md` ### Done when - [ ] A real suspend produces a recorded row and the documented outcome - [ ] A forward clock change does not complete a session early - [ ] A crash and reopen restores the correct remaining time - [ ] No remaining-seconds value is stored anywhere ### Watch out - Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. ## Phase 2 · Start, pause, stop, from the keyboard The session state machine is append-only and every transition has a key. ### Steps 1. Append intervals, never edit a total The total is a query; the intervals are the truth. Files: `src/timer/intervals.ts` 2. Give every action a key Including the ones you think nobody uses. Files: `src/shortcuts.ts`, `tests/keyboard-complete.test.ts` 3. Show state in the tray So the timer does not need a window to be useful. Files: `src-tauri/src/tray.rs` ### Done when - [ ] Focus time excludes pauses, computed from intervals - [ ] A full session completes using only the keyboard - [ ] The global shortcut works with the window closed - [ ] A crash during a pause resumes paused, correctly ### Watch out - Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. ## Phase 3 · A chime that arrives on time The end of a session is announced when it happens, whatever the window was doing. ### Steps 1. Schedule against the end instant natively The web view tick is decoration. Files: `src-tauri/src/notify.rs` 2. Recompute on focus and on resume Two moments where a stale display becomes a wrong decision. Files: `src/timer/sync.ts` 3. Handle an end instant already in the past Sleeping through the end is the normal case, not the edge case. Files: `src/timer/sync.ts`, `tests/late-resume.test.ts` ### Done when - [ ] A hidden window still announces on time - [ ] Refocusing shows correct remaining immediately - [ ] Waking after the end announces and resolves correctly - [ ] Preferences persist and a silent session still records ### Watch out - Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. ## Phase 4 · History that does not flatter you Statistics count what happened, and an abandoned session is not a completed one. ### Steps 1. Record an outcome for every session Four outcomes, and only one of them is completed. Files: `src/sessions/outcome.ts`, `tests/abandoned.test.ts` 2. Rebuild daily totals from intervals And assert the rebuild matches, so the derived table cannot drift. Files: `src/stats/rebuild.ts` 3. Show the sessions you did not finish The number that is actually useful is the honest one. Files: `src/views/history.tsx` ### Done when - [ ] A closed-app session is recorded abandoned - [ ] Rebuilt daily totals match stored rows - [ ] Edited outcomes are marked as edited - [ ] Abandoned and interrupted sessions are visible ### Watch out - Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately. ## Not in this build - Cross-device sync. - Per-application activity tracking. - Calendar integration. - Enforcement-grade blocking via privileged helpers. ## After v1, if you want it - Tagging sessions by project, which is one column and one filter on data you already store - A weekly review view built entirely from intervals, with no new storage - Optional import of an existing timer's history, mapped onto outcomes and intervals ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The SQLite file with sessions, intervals and suspends. DATA_DIR=~/.local/share/focus # Required. The planned duration for a new session. Stored per session, so changing it does not rewrite history. DEFAULT_SESSION_MINUTES=25 # Required. Breaks are recorded like sessions and can be skipped. DEFAULT_BREAK_MINUTES=5 # Required. A clock divergence larger than this is treated as a suspend and resolved by the documented rule. SUSPEND_THRESHOLD_SECONDS=60 # Required. Starts and stops without the window. Conflicts are the commonest setup failure. GLOBAL_SHORTCUT=CmdOrCtrl+Shift+F # Required. Whatever your platform genuinely supports. The interface label is generated from this value. BLOCKING_MODE=nudge
You are building a lean indie version of Session. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Session · indie build A local focus timer whose hard problems are measuring elapsed time correctly when the machine sleeps, never counting an abandoned session as completed, and being honest about what app blocking a normal application can and cannot do. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, and a tray presence | a timer you have to find a window for is a timer you stop using | | Storage | SQLite, sessions stored as intervals rather than a countdown | a remaining-seconds number cannot survive a crash | | Time | Wall-clock instants for scheduling, both clocks recorded, suspends detected | the monotonic clock's behaviour across sleep is not portable | | Input | Every action keyboard-reachable, with a global shortcut to start and stop | used dozens of times a day means never reaching for the mouse | | Blocking | Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more | a block that pretends to be enforcement is a lie you tell yourself | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface and runs the tests. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain** · free - Why: Tauri 2 needs it, and the tray, the global shortcut and the natively scheduled notification all live there. - Get it: rustup.rs, then the platform build dependencies Tauri lists. - Verify: cargo --version prints a version and a dev build shows a tray icon - [ ] **A machine you can actually put to sleep** · free - Why: The suspend behaviour is the central correctness problem here and it cannot be tested in a unit test, only by closing a lid and opening it again. - Get it: Any laptop, or a desktop you can suspend and wake on demand. - Verify: A session started before a suspend produces a suspends row after waking - [ ] **Your platform's documentation on restricting applications** · free - Why: What blocking can honestly mean differs completely by operating system, and writing this feature without reading that first produces either a placebo or a request for a permission you do not need. - Get it: Read the operating system vendor's own developer documentation on the relevant mechanism and permission. - Verify: You can state in one sentence what your platform allows and what it does not - [ ] **Your suspend and clock-change rule** · free - Why: Whether a session slept through counts as focus is a judgement that changes every number in the history, and it must be made before the history exists. - Get it: Write one paragraph covering suspend, forward clock change and backward clock change. - Verify: The paragraph is in the README and each case is named in a test ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Cross-device sync. - Per-application activity tracking. - Calendar integration. - Enforcement-grade blocking via privileged helpers. - cross-device sync - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates If one of those is essential to you, that is the reason to keep paying for Session, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Session The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a focus timer that keeps honest time across a closed lid, a changed clock and a crash to replace Session. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, and a tray presence · a timer you have to find a window for is a timer you stop using - Storage: SQLite, sessions stored as intervals rather than a countdown · a remaining-seconds number cannot survive a crash - Time: Wall-clock instants for scheduling, both clocks recorded, suspends detected · the monotonic clock's behaviour across sleep is not portable - Input: Every action keyboard-reachable, with a global shortcut to start and stop · used dozens of times a day means never reaching for the mouse - Blocking: Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more · a block that pretends to be enforcement is a lie you tell yourself ### Data model (create this before Phase 1) - sessions(id, intent, planned_seconds, started_at_utc, ended_at_utc, outcome, tag) · outcome is completed, stopped, abandoned or interrupted - intervals(session_id, ordinal, kind, started_at_utc, ended_at_utc, monotonic_start_ms, monotonic_end_ms) · kind is focus, pause or suspend - suspends(session_id, detected_at_utc, wall_gap_s, monotonic_gap_s, resolution) · when the two clocks disagree, this row records by how much - breaks(session_id, ordinal, planned_seconds, started_at_utc, ended_at_utc, skipped) - blocklist(id, pattern, kind, enabled) · kind names the mechanism, because what is possible differs per platform - days(date, focus_seconds, completed_sessions, abandoned_sessions) · derived, rebuildable from intervals - There is no remaining-seconds column anywhere. Remaining is computed from the stored instants on every tick, which is why a crash costs nothing. ### Phase 1 · Time that survives a closed lid Build: Store started_at as a wall-clock instant in UTC plus the planned duration, and compute remaining on every tick. Record a monotonic reading alongside each wall-clock one. On every tick compare the two gaps: a wall gap much larger than the monotonic gap means the machine slept, and that difference is written to the suspends table with a stated resolution rule. Trap: Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. Done when: A session running across a real suspend produces a suspends row with both gaps recorded and resolves according to the documented rule, verified by actually sleeping the machine. Changing the system clock forward during a session does not complete it early, and the change is recorded rather than absorbed, tested. Killing the application mid-session and reopening it restores the session with the correct remaining time computed from the stored instants. No code path anywhere stores or decrements a remaining-seconds value, verified by review. Do not build yet: No pauses, notifications, history or blocking. ### Phase 2 · Start, pause, stop, from the keyboard Build: Model start, pause, resume, stop and complete as appended interval rows rather than as edits to a session row. Bind a global shortcut for start and stop and give every action a key in the window. The tray shows the current state without opening anything. Trap: Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. Done when: Focus time for a session with three pauses equals the sum of its focus intervals and excludes the pauses, tested. Every action is reachable by keyboard with no pointer, checked by completing a full session using only the keyboard. The global shortcut starts and stops a session when the window is closed. A crash during a pause resumes in the paused state with the correct accumulated focus time. Do not build yet: No notifications, history, stats or blocking yet. ### Phase 3 · A chime that arrives on time Build: Compute the end instant when the session starts and schedule the notification against it from the native side. Recompute remaining on every window focus and on resume from sleep. Let the user choose sound, notification or both, and make it work when the window is hidden. Trap: Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. Done when: A session whose window is hidden for its whole duration still announces at the correct moment, tested by hiding the window and measuring. Returning to the window recomputes and displays the correct remaining time immediately, with no catch-up animation. Resuming from sleep after the end instant has passed announces immediately and marks the session according to the suspend rule. Notification preferences persist and a session with both sound and notification disabled still records correctly. Do not build yet: No history, stats or blocking yet. ### Phase 4 · History that does not flatter you Build: Record an outcome for every session: completed, stopped by the user, abandoned when the app was closed mid-session, or interrupted by a suspend past the rule's threshold. Show daily and weekly focus time computed from focus intervals, with abandoned sessions visible rather than excluded. Allow editing an outcome, recording that it was edited. Trap: Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately. Done when: A session whose application was closed before its end instant is recorded as abandoned and excluded from focus totals, tested. Daily totals recomputed from intervals match the stored daily rows exactly, asserted by rebuilding. An edited outcome is marked as edited with a timestamp, so the history stays honest about being adjusted. The history view shows abandoned and interrupted sessions rather than hiding them. Do not build yet: No blocking or export yet. ### Phase 5 · Blocking, described honestly, and everything exportable Build: Implement the blocking mechanism your platform genuinely supports, request the permission it needs through the documented flow, and label it for what it is. Where the only available mechanism is advisory, call it a nudge in the interface rather than a block. Never ask for a privileged permission without saying exactly what it is used for. Export sessions and intervals as CSV and JSON, and provide a wipe. Trap: Shipping a feature called blocking that edits a system file or closes a window, when any determined user reopens it in seconds and a permission prompt was needed to do even that, trades a real privilege for a placebo and teaches the user their tool lies to them. Find out what your platform's documented mechanism actually is, implement that, and name the feature after what it does. Done when: The blocking feature names its mechanism and its limits in the interface, not only in the README, and a reader can tell from the label whether it is enforcement or a nudge. Any permission is requested through the platform's documented flow, with the reason shown before the prompt, and the app works with the permission declined. Sessions and intervals export as CSV and JSON that open elsewhere, including outcomes and suspends. A wipe removes the database and derived files, verified by inspecting the data directory. Do not build yet: Nothing after this. ### Out of scope (and why) - Cross-device sync, which turns a single-file local app into a conflict-resolution problem for very little gain on a timer. - Deep per-application activity tracking, which is surveillance of yourself that you will not enjoy reading and which needs permissions disproportionate to a timer. - Calendar integration, which is a provider-specific timezone problem larger than this whole application. - Enforcement-grade blocking through privileged helpers or system extensions, which is a signed, notarised, separately-updated component and a project of its own. ### README must contain - That the timer stores instants and computes remaining, so a crash or a sleep never loses a session - What happens to a session that spans a system suspend, and that the rule was chosen deliberately - That the monotonic clock's behaviour across suspend is not guaranteed by the platform, with the Rust documentation cited - That an abandoned session is recorded as abandoned and excluded from focus totals - Exactly what the blocking feature does on each platform, which permission it needs, and what it cannot stop ===== AGENTS.md ===== # Agent instructions · Session indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, and a tray presence, SQLite, sessions stored as intervals rather than a countdown, Wall-clock instants for scheduling, both clocks recorded, suspends detected, Every action keyboard-reachable, with a global shortcut to start and stop, Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. - Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. - Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. - Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately. ===== BUILD_PLAN.md ===== # Build plan · Session A local focus timer whose hard problems are measuring elapsed time correctly when the machine sleeps, never counting an abandoned session as completed, and being honest about what app blocking a normal application can and cannot do. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Time that survives a closed lid A session's end is derived from recorded instants, and a suspend is detected and recorded rather than silently absorbed. ### Steps 1. Store instants, compute remaining The countdown is a rendering, not a state. Files: `src-tauri/src/session.rs`, `src/timer/remaining.ts` 2. Record both clocks on every tick Their disagreement is the only way to see a suspend. Files: `src-tauri/src/clocks.rs`, `tests/suspend-detection.test.ts` 3. Write the suspend rule down Does a slept twenty minutes count as focus? Decide once, in the README. Files: `docs/time-policy.md` ### Done when - [ ] A real suspend produces a recorded row and the documented outcome - [ ] A forward clock change does not complete a session early - [ ] A crash and reopen restores the correct remaining time - [ ] No remaining-seconds value is stored anywhere ### Watch out - Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. ## Phase 2 · Start, pause, stop, from the keyboard The session state machine is append-only and every transition has a key. ### Steps 1. Append intervals, never edit a total The total is a query; the intervals are the truth. Files: `src/timer/intervals.ts` 2. Give every action a key Including the ones you think nobody uses. Files: `src/shortcuts.ts`, `tests/keyboard-complete.test.ts` 3. Show state in the tray So the timer does not need a window to be useful. Files: `src-tauri/src/tray.rs` ### Done when - [ ] Focus time excludes pauses, computed from intervals - [ ] A full session completes using only the keyboard - [ ] The global shortcut works with the window closed - [ ] A crash during a pause resumes paused, correctly ### Watch out - Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. ## Phase 3 · A chime that arrives on time The end of a session is announced when it happens, whatever the window was doing. ### Steps 1. Schedule against the end instant natively The web view tick is decoration. Files: `src-tauri/src/notify.rs` 2. Recompute on focus and on resume Two moments where a stale display becomes a wrong decision. Files: `src/timer/sync.ts` 3. Handle an end instant already in the past Sleeping through the end is the normal case, not the edge case. Files: `src/timer/sync.ts`, `tests/late-resume.test.ts` ### Done when - [ ] A hidden window still announces on time - [ ] Refocusing shows correct remaining immediately - [ ] Waking after the end announces and resolves correctly - [ ] Preferences persist and a silent session still records ### Watch out - Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. ## Phase 4 · History that does not flatter you Statistics count what happened, and an abandoned session is not a completed one. ### Steps 1. Record an outcome for every session Four outcomes, and only one of them is completed. Files: `src/sessions/outcome.ts`, `tests/abandoned.test.ts` 2. Rebuild daily totals from intervals And assert the rebuild matches, so the derived table cannot drift. Files: `src/stats/rebuild.ts` 3. Show the sessions you did not finish The number that is actually useful is the honest one. Files: `src/views/history.tsx` ### Done when - [ ] A closed-app session is recorded abandoned - [ ] Rebuilt daily totals match stored rows - [ ] Edited outcomes are marked as edited - [ ] Abandoned and interrupted sessions are visible ### Watch out - Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately. ## Not in this build - Cross-device sync. - Per-application activity tracking. - Calendar integration. - Enforcement-grade blocking via privileged helpers. ## After v1, if you want it - Tagging sessions by project, which is one column and one filter on data you already store - A weekly review view built entirely from intervals, with no new storage - Optional import of an existing timer's history, mapped onto outcomes and intervals ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The SQLite file with sessions, intervals and suspends. DATA_DIR=~/.local/share/focus # Required. The planned duration for a new session. Stored per session, so changing it does not rewrite history. DEFAULT_SESSION_MINUTES=25 # Required. Breaks are recorded like sessions and can be skipped. DEFAULT_BREAK_MINUTES=5 # Required. A clock divergence larger than this is treated as a suspend and resolved by the documented rule. SUSPEND_THRESHOLD_SECONDS=60 # Required. Starts and stops without the window. Conflicts are the commonest setup failure. GLOBAL_SHORTCUT=CmdOrCtrl+Shift+F # Required. Whatever your platform genuinely supports. The interface label is generated from this value. BLOCKING_MODE=nudge
You are building a production product version of Session. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Session · product brief ## Problem Session's solo core is compact: build a local desktop utility that captures the essential focus timer workflow with keyboard-first controls and local storage. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on polish, OS integration. ## Product outcome A timer you can trust after a month, because it survives sleeps and crashes, never counts time you did not spend, and does not pretend to block anything it cannot. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS, Windows, or Linux development machine - Node.js 22 and Rust toolchain - Local filesystem access and OS permissions ## Explicit non-goals for v1 - Cross-device sync. - Per-application activity tracking. - Calendar integration. - Enforcement-grade blocking via privileged helpers. - cross-device sync - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates ## Success criteria - A real suspend is confirmed to produce the documented outcome - A forward clock change is confirmed not to complete a session early - A hidden window is confirmed to announce on time - A closed-app session is confirmed recorded as abandoned - The blocking label is confirmed to match the implemented mechanism ===== BRIEF.md ===== # Build brief · Session The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a focus timer that keeps honest time across a closed lid, a changed clock and a crash to replace Session. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, and a tray presence · a timer you have to find a window for is a timer you stop using - Storage: SQLite, sessions stored as intervals rather than a countdown · a remaining-seconds number cannot survive a crash - Time: Wall-clock instants for scheduling, both clocks recorded, suspends detected · the monotonic clock's behaviour across sleep is not portable - Input: Every action keyboard-reachable, with a global shortcut to start and stop · used dozens of times a day means never reaching for the mouse - Blocking: Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more · a block that pretends to be enforcement is a lie you tell yourself ### Data model (create this before Phase 1) - sessions(id, intent, planned_seconds, started_at_utc, ended_at_utc, outcome, tag) · outcome is completed, stopped, abandoned or interrupted - intervals(session_id, ordinal, kind, started_at_utc, ended_at_utc, monotonic_start_ms, monotonic_end_ms) · kind is focus, pause or suspend - suspends(session_id, detected_at_utc, wall_gap_s, monotonic_gap_s, resolution) · when the two clocks disagree, this row records by how much - breaks(session_id, ordinal, planned_seconds, started_at_utc, ended_at_utc, skipped) - blocklist(id, pattern, kind, enabled) · kind names the mechanism, because what is possible differs per platform - days(date, focus_seconds, completed_sessions, abandoned_sessions) · derived, rebuildable from intervals - There is no remaining-seconds column anywhere. Remaining is computed from the stored instants on every tick, which is why a crash costs nothing. ### Phase 1 · Time that survives a closed lid Build: Store started_at as a wall-clock instant in UTC plus the planned duration, and compute remaining on every tick. Record a monotonic reading alongside each wall-clock one. On every tick compare the two gaps: a wall gap much larger than the monotonic gap means the machine slept, and that difference is written to the suspends table with a stated resolution rule. Trap: Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. Done when: A session running across a real suspend produces a suspends row with both gaps recorded and resolves according to the documented rule, verified by actually sleeping the machine. Changing the system clock forward during a session does not complete it early, and the change is recorded rather than absorbed, tested. Killing the application mid-session and reopening it restores the session with the correct remaining time computed from the stored instants. No code path anywhere stores or decrements a remaining-seconds value, verified by review. Do not build yet: No pauses, notifications, history or blocking. ### Phase 2 · Start, pause, stop, from the keyboard Build: Model start, pause, resume, stop and complete as appended interval rows rather than as edits to a session row. Bind a global shortcut for start and stop and give every action a key in the window. The tray shows the current state without opening anything. Trap: Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. Done when: Focus time for a session with three pauses equals the sum of its focus intervals and excludes the pauses, tested. Every action is reachable by keyboard with no pointer, checked by completing a full session using only the keyboard. The global shortcut starts and stops a session when the window is closed. A crash during a pause resumes in the paused state with the correct accumulated focus time. Do not build yet: No notifications, history, stats or blocking yet. ### Phase 3 · A chime that arrives on time Build: Compute the end instant when the session starts and schedule the notification against it from the native side. Recompute remaining on every window focus and on resume from sleep. Let the user choose sound, notification or both, and make it work when the window is hidden. Trap: Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. Done when: A session whose window is hidden for its whole duration still announces at the correct moment, tested by hiding the window and measuring. Returning to the window recomputes and displays the correct remaining time immediately, with no catch-up animation. Resuming from sleep after the end instant has passed announces immediately and marks the session according to the suspend rule. Notification preferences persist and a session with both sound and notification disabled still records correctly. Do not build yet: No history, stats or blocking yet. ### Phase 4 · History that does not flatter you Build: Record an outcome for every session: completed, stopped by the user, abandoned when the app was closed mid-session, or interrupted by a suspend past the rule's threshold. Show daily and weekly focus time computed from focus intervals, with abandoned sessions visible rather than excluded. Allow editing an outcome, recording that it was edited. Trap: Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately. Done when: A session whose application was closed before its end instant is recorded as abandoned and excluded from focus totals, tested. Daily totals recomputed from intervals match the stored daily rows exactly, asserted by rebuilding. An edited outcome is marked as edited with a timestamp, so the history stays honest about being adjusted. The history view shows abandoned and interrupted sessions rather than hiding them. Do not build yet: No blocking or export yet. ### Phase 5 · Blocking, described honestly, and everything exportable Build: Implement the blocking mechanism your platform genuinely supports, request the permission it needs through the documented flow, and label it for what it is. Where the only available mechanism is advisory, call it a nudge in the interface rather than a block. Never ask for a privileged permission without saying exactly what it is used for. Export sessions and intervals as CSV and JSON, and provide a wipe. Trap: Shipping a feature called blocking that edits a system file or closes a window, when any determined user reopens it in seconds and a permission prompt was needed to do even that, trades a real privilege for a placebo and teaches the user their tool lies to them. Find out what your platform's documented mechanism actually is, implement that, and name the feature after what it does. Done when: The blocking feature names its mechanism and its limits in the interface, not only in the README, and a reader can tell from the label whether it is enforcement or a nudge. Any permission is requested through the platform's documented flow, with the reason shown before the prompt, and the app works with the permission declined. Sessions and intervals export as CSV and JSON that open elsewhere, including outcomes and suspends. A wipe removes the database and derived files, verified by inspecting the data directory. Do not build yet: Nothing after this. ### Out of scope (and why) - Cross-device sync, which turns a single-file local app into a conflict-resolution problem for very little gain on a timer. - Deep per-application activity tracking, which is surveillance of yourself that you will not enjoy reading and which needs permissions disproportionate to a timer. - Calendar integration, which is a provider-specific timezone problem larger than this whole application. - Enforcement-grade blocking through privileged helpers or system extensions, which is a signed, notarised, separately-updated component and a project of its own. ### README must contain - That the timer stores instants and computes remaining, so a crash or a sleep never loses a session - What happens to a session that spans a system suspend, and that the rule was chosen deliberately - That the monotonic clock's behaviour across suspend is not guaranteed by the platform, with the Rust documentation cited - That an abandoned session is recorded as abandoned and excluded from focus totals - Exactly what the blocking feature does on each platform, which permission it needs, and what it cannot stop ===== ARCHITECTURE.md ===== # Architecture · Session ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, and a tray presence | a timer you have to find a window for is a timer you stop using | | Storage | SQLite, sessions stored as intervals rather than a countdown | a remaining-seconds number cannot survive a crash | | Time | Wall-clock instants for scheduling, both clocks recorded, suspends detected | the monotonic clock's behaviour across sleep is not portable | | Input | Every action keyboard-reachable, with a global shortcut to start and stop | used dozens of times a day means never reaching for the mouse | | Blocking | Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more | a block that pretends to be enforcement is a lie you tell yourself | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Time | Instants, dual clock readings and suspend detection. | Not swappable for a stored countdown, which is the bug this design prevents. | | Intervals | The append-only session state machine. | Any store, provided totals are derived and never edited directly. | | Notify | Natively scheduled announcements. | Any mechanism, provided it does not depend on a foreground window. | | Stats | Outcomes and rebuildable daily totals. | Any presentation, provided abandoned time stays visible. | | Blocking | The platform mechanism and its honest label. | Any mechanism, provided the label matches what it actually does. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATA_DIR` · required · The SQLite file with sessions, intervals and suspends. - `DEFAULT_SESSION_MINUTES` · required · The planned duration for a new session. Stored per session, so changing it does not rewrite history. - `DEFAULT_BREAK_MINUTES` · required · Breaks are recorded like sessions and can be skipped. - `SUSPEND_THRESHOLD_SECONDS` · required · A clock divergence larger than this is treated as a suspend and resolved by the documented rule. - `GLOBAL_SHORTCUT` · required · Starts and stops without the window. Conflicts are the commonest setup failure. - `BLOCKING_MODE` · required · Whatever your platform genuinely supports. The interface label is generated from this value. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Session product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, and a tray presence, SQLite, sessions stored as intervals rather than a countdown, Wall-clock instants for scheduling, both clocks recorded, suspends detected, Every action keyboard-reachable, with a global shortcut to start and stop, Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. - Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. - Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. - Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately. - Shipping a feature called blocking that edits a system file or closes a window, when any determined user reopens it in seconds and a permission prompt was needed to do even that, trades a real privilege for a placebo and teaches the user their tool lies to them. Find out what your platform's documented mechanism actually is, implement that, and name the feature after what it does. ===== MILESTONES.md ===== # Delivery milestones · Session Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Time that survives a closed lid A session's end is derived from recorded instants, and a suspend is detected and recorded rather than silently absorbed. ### Steps 1. Store instants, compute remaining The countdown is a rendering, not a state. Files: `src-tauri/src/session.rs`, `src/timer/remaining.ts` 2. Record both clocks on every tick Their disagreement is the only way to see a suspend. Files: `src-tauri/src/clocks.rs`, `tests/suspend-detection.test.ts` 3. Write the suspend rule down Does a slept twenty minutes count as focus? Decide once, in the README. Files: `docs/time-policy.md` ### Done when - [ ] A real suspend produces a recorded row and the documented outcome - [ ] A forward clock change does not complete a session early - [ ] A crash and reopen restores the correct remaining time - [ ] No remaining-seconds value is stored anywhere ### Watch out - Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. ## M2 · Start, pause, stop, from the keyboard The session state machine is append-only and every transition has a key. ### Steps 1. Append intervals, never edit a total The total is a query; the intervals are the truth. Files: `src/timer/intervals.ts` 2. Give every action a key Including the ones you think nobody uses. Files: `src/shortcuts.ts`, `tests/keyboard-complete.test.ts` 3. Show state in the tray So the timer does not need a window to be useful. Files: `src-tauri/src/tray.rs` ### Done when - [ ] Focus time excludes pauses, computed from intervals - [ ] A full session completes using only the keyboard - [ ] The global shortcut works with the window closed - [ ] A crash during a pause resumes paused, correctly ### Watch out - Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. ## M3 · A chime that arrives on time The end of a session is announced when it happens, whatever the window was doing. ### Steps 1. Schedule against the end instant natively The web view tick is decoration. Files: `src-tauri/src/notify.rs` 2. Recompute on focus and on resume Two moments where a stale display becomes a wrong decision. Files: `src/timer/sync.ts` 3. Handle an end instant already in the past Sleeping through the end is the normal case, not the edge case. Files: `src/timer/sync.ts`, `tests/late-resume.test.ts` ### Done when - [ ] A hidden window still announces on time - [ ] Refocusing shows correct remaining immediately - [ ] Waking after the end announces and resolves correctly - [ ] Preferences persist and a silent session still records ### Watch out - Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. ## M4 · History that does not flatter you Statistics count what happened, and an abandoned session is not a completed one. ### Steps 1. Record an outcome for every session Four outcomes, and only one of them is completed. Files: `src/sessions/outcome.ts`, `tests/abandoned.test.ts` 2. Rebuild daily totals from intervals And assert the rebuild matches, so the derived table cannot drift. Files: `src/stats/rebuild.ts` 3. Show the sessions you did not finish The number that is actually useful is the honest one. Files: `src/views/history.tsx` ### Done when - [ ] A closed-app session is recorded abandoned - [ ] Rebuilt daily totals match stored rows - [ ] Edited outcomes are marked as edited - [ ] Abandoned and interrupted sessions are visible ### Watch out - Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately. ## M5 · Blocking, described honestly, and everything exportable (production only) Whatever blocking the platform actually allows is what is offered, named accurately, and the data leaves in an open format. ### Steps 1. Implement only what the platform documents Read the platform's own guidance before writing a line of this. Files: `src-tauri/src/blocking.rs`, `docs/blocking-per-platform.md` 2. Name the feature after what it does Nudge or block, in the label, not in a footnote. Files: `src/views/blocking.tsx` 3. Export intervals, not just totals Totals are opinions; intervals are the data. Files: `src/export.ts` ### Done when - [ ] The blocking label states the mechanism and its limits - [ ] Permissions are requested with a reason and declining still works - [ ] Exports include outcomes, intervals and suspends - [ ] The wipe leaves nothing in the data directory ### Watch out - Shipping a feature called blocking that edits a system file or closes a window, when any determined user reopens it in seconds and a permission prompt was needed to do even that, trades a real privilege for a placebo and teaches the user their tool lies to them. Find out what your platform's documented mechanism actually is, implement that, and name the feature after what it does. ===== OPERATIONS.md ===== # Operations · Session ## Backup The data directory, which is one small file, plus an occasional CSV export. A year of focus history is a few megabytes and is the only thing here that cannot be rebuilt. ## Restore Restore and rebuild the daily totals from intervals, then compare them with the restored rows. A mismatch means the derived table drifted, which is worth knowing before you trust a chart. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool this is the suite: the suspend test on a real machine, the clock-change tests, the hidden-window notification test, and the daily rebuild assertion. Any of them failing means the history has become fiction. ## Incident checklist A session that came out wrong is diagnosed from its intervals and suspends rows, which record both clocks, so the answer is in the data rather than in a guess about what the machine was doing. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A real suspend is confirmed to produce the documented outcome - [ ] A forward clock change is confirmed not to complete a session early - [ ] A hidden window is confirmed to announce on time - [ ] A closed-app session is confirmed recorded as abandoned - [ ] The blocking label is confirmed to match the implemented mechanism ## Launch constraint Do not market omitted Session capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The SQLite file with sessions, intervals and suspends. DATA_DIR=~/.local/share/focus # Required. The planned duration for a new session. Stored per session, so changing it does not rewrite history. DEFAULT_SESSION_MINUTES=25 # Required. Breaks are recorded like sessions and can be skipped. DEFAULT_BREAK_MINUTES=5 # Required. A clock divergence larger than this is treated as a suspend and resolved by the documented rule. SUSPEND_THRESHOLD_SECONDS=60 # Required. Starts and stops without the window. Conflicts are the commonest setup failure. GLOBAL_SHORTCUT=CmdOrCtrl+Shift+F # Required. Whatever your platform genuinely supports. The interface label is generated from this value. BLOCKING_MODE=nudge
# Session · indie build A local focus timer whose hard problems are measuring elapsed time correctly when the machine sleeps, never counting an abandoned session as completed, and being honest about what app blocking a normal application can and cannot do. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, and a tray presence | a timer you have to find a window for is a timer you stop using | | Storage | SQLite, sessions stored as intervals rather than a countdown | a remaining-seconds number cannot survive a crash | | Time | Wall-clock instants for scheduling, both clocks recorded, suspends detected | the monotonic clock's behaviour across sleep is not portable | | Input | Every action keyboard-reachable, with a global shortcut to start and stop | used dozens of times a day means never reaching for the mouse | | Blocking | Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more | a block that pretends to be enforcement is a lie you tell yourself | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface and runs the tests. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain** · free - Why: Tauri 2 needs it, and the tray, the global shortcut and the natively scheduled notification all live there. - Get it: rustup.rs, then the platform build dependencies Tauri lists. - Verify: cargo --version prints a version and a dev build shows a tray icon - [ ] **A machine you can actually put to sleep** · free - Why: The suspend behaviour is the central correctness problem here and it cannot be tested in a unit test, only by closing a lid and opening it again. - Get it: Any laptop, or a desktop you can suspend and wake on demand. - Verify: A session started before a suspend produces a suspends row after waking - [ ] **Your platform's documentation on restricting applications** · free - Why: What blocking can honestly mean differs completely by operating system, and writing this feature without reading that first produces either a placebo or a request for a permission you do not need. - Get it: Read the operating system vendor's own developer documentation on the relevant mechanism and permission. - Verify: You can state in one sentence what your platform allows and what it does not - [ ] **Your suspend and clock-change rule** · free - Why: Whether a session slept through counts as focus is a judgement that changes every number in the history, and it must be made before the history exists. - Get it: Write one paragraph covering suspend, forward clock change and backward clock change. - Verify: The paragraph is in the README and each case is named in a test ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Cross-device sync. - Per-application activity tracking. - Calendar integration. - Enforcement-grade blocking via privileged helpers. - cross-device sync - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates If one of those is essential to you, that is the reason to keep paying for Session, and the README should say so rather than pretend.
# Build brief · Session The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a focus timer that keeps honest time across a closed lid, a changed clock and a crash to replace Session. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, and a tray presence · a timer you have to find a window for is a timer you stop using - Storage: SQLite, sessions stored as intervals rather than a countdown · a remaining-seconds number cannot survive a crash - Time: Wall-clock instants for scheduling, both clocks recorded, suspends detected · the monotonic clock's behaviour across sleep is not portable - Input: Every action keyboard-reachable, with a global shortcut to start and stop · used dozens of times a day means never reaching for the mouse - Blocking: Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more · a block that pretends to be enforcement is a lie you tell yourself ### Data model (create this before Phase 1) - sessions(id, intent, planned_seconds, started_at_utc, ended_at_utc, outcome, tag) · outcome is completed, stopped, abandoned or interrupted - intervals(session_id, ordinal, kind, started_at_utc, ended_at_utc, monotonic_start_ms, monotonic_end_ms) · kind is focus, pause or suspend - suspends(session_id, detected_at_utc, wall_gap_s, monotonic_gap_s, resolution) · when the two clocks disagree, this row records by how much - breaks(session_id, ordinal, planned_seconds, started_at_utc, ended_at_utc, skipped) - blocklist(id, pattern, kind, enabled) · kind names the mechanism, because what is possible differs per platform - days(date, focus_seconds, completed_sessions, abandoned_sessions) · derived, rebuildable from intervals - There is no remaining-seconds column anywhere. Remaining is computed from the stored instants on every tick, which is why a crash costs nothing. ### Phase 1 · Time that survives a closed lid Build: Store started_at as a wall-clock instant in UTC plus the planned duration, and compute remaining on every tick. Record a monotonic reading alongside each wall-clock one. On every tick compare the two gaps: a wall gap much larger than the monotonic gap means the machine slept, and that difference is written to the suspends table with a stated resolution rule. Trap: Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. Done when: A session running across a real suspend produces a suspends row with both gaps recorded and resolves according to the documented rule, verified by actually sleeping the machine. Changing the system clock forward during a session does not complete it early, and the change is recorded rather than absorbed, tested. Killing the application mid-session and reopening it restores the session with the correct remaining time computed from the stored instants. No code path anywhere stores or decrements a remaining-seconds value, verified by review. Do not build yet: No pauses, notifications, history or blocking. ### Phase 2 · Start, pause, stop, from the keyboard Build: Model start, pause, resume, stop and complete as appended interval rows rather than as edits to a session row. Bind a global shortcut for start and stop and give every action a key in the window. The tray shows the current state without opening anything. Trap: Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. Done when: Focus time for a session with three pauses equals the sum of its focus intervals and excludes the pauses, tested. Every action is reachable by keyboard with no pointer, checked by completing a full session using only the keyboard. The global shortcut starts and stops a session when the window is closed. A crash during a pause resumes in the paused state with the correct accumulated focus time. Do not build yet: No notifications, history, stats or blocking yet. ### Phase 3 · A chime that arrives on time Build: Compute the end instant when the session starts and schedule the notification against it from the native side. Recompute remaining on every window focus and on resume from sleep. Let the user choose sound, notification or both, and make it work when the window is hidden. Trap: Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. Done when: A session whose window is hidden for its whole duration still announces at the correct moment, tested by hiding the window and measuring. Returning to the window recomputes and displays the correct remaining time immediately, with no catch-up animation. Resuming from sleep after the end instant has passed announces immediately and marks the session according to the suspend rule. Notification preferences persist and a session with both sound and notification disabled still records correctly. Do not build yet: No history, stats or blocking yet. ### Phase 4 · History that does not flatter you Build: Record an outcome for every session: completed, stopped by the user, abandoned when the app was closed mid-session, or interrupted by a suspend past the rule's threshold. Show daily and weekly focus time computed from focus intervals, with abandoned sessions visible rather than excluded. Allow editing an outcome, recording that it was edited. Trap: Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately. Done when: A session whose application was closed before its end instant is recorded as abandoned and excluded from focus totals, tested. Daily totals recomputed from intervals match the stored daily rows exactly, asserted by rebuilding. An edited outcome is marked as edited with a timestamp, so the history stays honest about being adjusted. The history view shows abandoned and interrupted sessions rather than hiding them. Do not build yet: No blocking or export yet. ### Phase 5 · Blocking, described honestly, and everything exportable Build: Implement the blocking mechanism your platform genuinely supports, request the permission it needs through the documented flow, and label it for what it is. Where the only available mechanism is advisory, call it a nudge in the interface rather than a block. Never ask for a privileged permission without saying exactly what it is used for. Export sessions and intervals as CSV and JSON, and provide a wipe. Trap: Shipping a feature called blocking that edits a system file or closes a window, when any determined user reopens it in seconds and a permission prompt was needed to do even that, trades a real privilege for a placebo and teaches the user their tool lies to them. Find out what your platform's documented mechanism actually is, implement that, and name the feature after what it does. Done when: The blocking feature names its mechanism and its limits in the interface, not only in the README, and a reader can tell from the label whether it is enforcement or a nudge. Any permission is requested through the platform's documented flow, with the reason shown before the prompt, and the app works with the permission declined. Sessions and intervals export as CSV and JSON that open elsewhere, including outcomes and suspends. A wipe removes the database and derived files, verified by inspecting the data directory. Do not build yet: Nothing after this. ### Out of scope (and why) - Cross-device sync, which turns a single-file local app into a conflict-resolution problem for very little gain on a timer. - Deep per-application activity tracking, which is surveillance of yourself that you will not enjoy reading and which needs permissions disproportionate to a timer. - Calendar integration, which is a provider-specific timezone problem larger than this whole application. - Enforcement-grade blocking through privileged helpers or system extensions, which is a signed, notarised, separately-updated component and a project of its own. ### README must contain - That the timer stores instants and computes remaining, so a crash or a sleep never loses a session - What happens to a session that spans a system suspend, and that the rule was chosen deliberately - That the monotonic clock's behaviour across suspend is not guaranteed by the platform, with the Rust documentation cited - That an abandoned session is recorded as abandoned and excluded from focus totals - Exactly what the blocking feature does on each platform, which permission it needs, and what it cannot stop
# Agent instructions · Session indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, and a tray presence, SQLite, sessions stored as intervals rather than a countdown, Wall-clock instants for scheduling, both clocks recorded, suspends detected, Every action keyboard-reachable, with a global shortcut to start and stop, Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. - Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. - Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. - Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately.
# Build plan · Session A local focus timer whose hard problems are measuring elapsed time correctly when the machine sleeps, never counting an abandoned session as completed, and being honest about what app blocking a normal application can and cannot do. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Time that survives a closed lid A session's end is derived from recorded instants, and a suspend is detected and recorded rather than silently absorbed. ### Steps 1. Store instants, compute remaining The countdown is a rendering, not a state. Files: `src-tauri/src/session.rs`, `src/timer/remaining.ts` 2. Record both clocks on every tick Their disagreement is the only way to see a suspend. Files: `src-tauri/src/clocks.rs`, `tests/suspend-detection.test.ts` 3. Write the suspend rule down Does a slept twenty minutes count as focus? Decide once, in the README. Files: `docs/time-policy.md` ### Done when - [ ] A real suspend produces a recorded row and the documented outcome - [ ] A forward clock change does not complete a session early - [ ] A crash and reopen restores the correct remaining time - [ ] No remaining-seconds value is stored anywhere ### Watch out - Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. ## Phase 2 · Start, pause, stop, from the keyboard The session state machine is append-only and every transition has a key. ### Steps 1. Append intervals, never edit a total The total is a query; the intervals are the truth. Files: `src/timer/intervals.ts` 2. Give every action a key Including the ones you think nobody uses. Files: `src/shortcuts.ts`, `tests/keyboard-complete.test.ts` 3. Show state in the tray So the timer does not need a window to be useful. Files: `src-tauri/src/tray.rs` ### Done when - [ ] Focus time excludes pauses, computed from intervals - [ ] A full session completes using only the keyboard - [ ] The global shortcut works with the window closed - [ ] A crash during a pause resumes paused, correctly ### Watch out - Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. ## Phase 3 · A chime that arrives on time The end of a session is announced when it happens, whatever the window was doing. ### Steps 1. Schedule against the end instant natively The web view tick is decoration. Files: `src-tauri/src/notify.rs` 2. Recompute on focus and on resume Two moments where a stale display becomes a wrong decision. Files: `src/timer/sync.ts` 3. Handle an end instant already in the past Sleeping through the end is the normal case, not the edge case. Files: `src/timer/sync.ts`, `tests/late-resume.test.ts` ### Done when - [ ] A hidden window still announces on time - [ ] Refocusing shows correct remaining immediately - [ ] Waking after the end announces and resolves correctly - [ ] Preferences persist and a silent session still records ### Watch out - Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. ## Phase 4 · History that does not flatter you Statistics count what happened, and an abandoned session is not a completed one. ### Steps 1. Record an outcome for every session Four outcomes, and only one of them is completed. Files: `src/sessions/outcome.ts`, `tests/abandoned.test.ts` 2. Rebuild daily totals from intervals And assert the rebuild matches, so the derived table cannot drift. Files: `src/stats/rebuild.ts` 3. Show the sessions you did not finish The number that is actually useful is the honest one. Files: `src/views/history.tsx` ### Done when - [ ] A closed-app session is recorded abandoned - [ ] Rebuilt daily totals match stored rows - [ ] Edited outcomes are marked as edited - [ ] Abandoned and interrupted sessions are visible ### Watch out - Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately. ## Not in this build - Cross-device sync. - Per-application activity tracking. - Calendar integration. - Enforcement-grade blocking via privileged helpers. ## After v1, if you want it - Tagging sessions by project, which is one column and one filter on data you already store - A weekly review view built entirely from intervals, with no new storage - Optional import of an existing timer's history, mapped onto outcomes and intervals
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The SQLite file with sessions, intervals and suspends. DATA_DIR=~/.local/share/focus # Required. The planned duration for a new session. Stored per session, so changing it does not rewrite history. DEFAULT_SESSION_MINUTES=25 # Required. Breaks are recorded like sessions and can be skipped. DEFAULT_BREAK_MINUTES=5 # Required. A clock divergence larger than this is treated as a suspend and resolved by the documented rule. SUSPEND_THRESHOLD_SECONDS=60 # Required. Starts and stops without the window. Conflicts are the commonest setup failure. GLOBAL_SHORTCUT=CmdOrCtrl+Shift+F # Required. Whatever your platform genuinely supports. The interface label is generated from this value. BLOCKING_MODE=nudge
# Session · product brief ## Problem Session's solo core is compact: build a local desktop utility that captures the essential focus timer workflow with keyboard-first controls and local storage. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on polish, OS integration. ## Product outcome A timer you can trust after a month, because it survives sleeps and crashes, never counts time you did not spend, and does not pretend to block anything it cannot. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS, Windows, or Linux development machine - Node.js 22 and Rust toolchain - Local filesystem access and OS permissions ## Explicit non-goals for v1 - Cross-device sync. - Per-application activity tracking. - Calendar integration. - Enforcement-grade blocking via privileged helpers. - cross-device sync - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates ## Success criteria - A real suspend is confirmed to produce the documented outcome - A forward clock change is confirmed not to complete a session early - A hidden window is confirmed to announce on time - A closed-app session is confirmed recorded as abandoned - The blocking label is confirmed to match the implemented mechanism
# Build brief · Session The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a focus timer that keeps honest time across a closed lid, a changed clock and a crash to replace Session. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, and a tray presence · a timer you have to find a window for is a timer you stop using - Storage: SQLite, sessions stored as intervals rather than a countdown · a remaining-seconds number cannot survive a crash - Time: Wall-clock instants for scheduling, both clocks recorded, suspends detected · the monotonic clock's behaviour across sleep is not portable - Input: Every action keyboard-reachable, with a global shortcut to start and stop · used dozens of times a day means never reaching for the mouse - Blocking: Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more · a block that pretends to be enforcement is a lie you tell yourself ### Data model (create this before Phase 1) - sessions(id, intent, planned_seconds, started_at_utc, ended_at_utc, outcome, tag) · outcome is completed, stopped, abandoned or interrupted - intervals(session_id, ordinal, kind, started_at_utc, ended_at_utc, monotonic_start_ms, monotonic_end_ms) · kind is focus, pause or suspend - suspends(session_id, detected_at_utc, wall_gap_s, monotonic_gap_s, resolution) · when the two clocks disagree, this row records by how much - breaks(session_id, ordinal, planned_seconds, started_at_utc, ended_at_utc, skipped) - blocklist(id, pattern, kind, enabled) · kind names the mechanism, because what is possible differs per platform - days(date, focus_seconds, completed_sessions, abandoned_sessions) · derived, rebuildable from intervals - There is no remaining-seconds column anywhere. Remaining is computed from the stored instants on every tick, which is why a crash costs nothing. ### Phase 1 · Time that survives a closed lid Build: Store started_at as a wall-clock instant in UTC plus the planned duration, and compute remaining on every tick. Record a monotonic reading alongside each wall-clock one. On every tick compare the two gaps: a wall gap much larger than the monotonic gap means the machine slept, and that difference is written to the suspends table with a stated resolution rule. Trap: Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. Done when: A session running across a real suspend produces a suspends row with both gaps recorded and resolves according to the documented rule, verified by actually sleeping the machine. Changing the system clock forward during a session does not complete it early, and the change is recorded rather than absorbed, tested. Killing the application mid-session and reopening it restores the session with the correct remaining time computed from the stored instants. No code path anywhere stores or decrements a remaining-seconds value, verified by review. Do not build yet: No pauses, notifications, history or blocking. ### Phase 2 · Start, pause, stop, from the keyboard Build: Model start, pause, resume, stop and complete as appended interval rows rather than as edits to a session row. Bind a global shortcut for start and stop and give every action a key in the window. The tray shows the current state without opening anything. Trap: Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. Done when: Focus time for a session with three pauses equals the sum of its focus intervals and excludes the pauses, tested. Every action is reachable by keyboard with no pointer, checked by completing a full session using only the keyboard. The global shortcut starts and stops a session when the window is closed. A crash during a pause resumes in the paused state with the correct accumulated focus time. Do not build yet: No notifications, history, stats or blocking yet. ### Phase 3 · A chime that arrives on time Build: Compute the end instant when the session starts and schedule the notification against it from the native side. Recompute remaining on every window focus and on resume from sleep. Let the user choose sound, notification or both, and make it work when the window is hidden. Trap: Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. Done when: A session whose window is hidden for its whole duration still announces at the correct moment, tested by hiding the window and measuring. Returning to the window recomputes and displays the correct remaining time immediately, with no catch-up animation. Resuming from sleep after the end instant has passed announces immediately and marks the session according to the suspend rule. Notification preferences persist and a session with both sound and notification disabled still records correctly. Do not build yet: No history, stats or blocking yet. ### Phase 4 · History that does not flatter you Build: Record an outcome for every session: completed, stopped by the user, abandoned when the app was closed mid-session, or interrupted by a suspend past the rule's threshold. Show daily and weekly focus time computed from focus intervals, with abandoned sessions visible rather than excluded. Allow editing an outcome, recording that it was edited. Trap: Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately. Done when: A session whose application was closed before its end instant is recorded as abandoned and excluded from focus totals, tested. Daily totals recomputed from intervals match the stored daily rows exactly, asserted by rebuilding. An edited outcome is marked as edited with a timestamp, so the history stays honest about being adjusted. The history view shows abandoned and interrupted sessions rather than hiding them. Do not build yet: No blocking or export yet. ### Phase 5 · Blocking, described honestly, and everything exportable Build: Implement the blocking mechanism your platform genuinely supports, request the permission it needs through the documented flow, and label it for what it is. Where the only available mechanism is advisory, call it a nudge in the interface rather than a block. Never ask for a privileged permission without saying exactly what it is used for. Export sessions and intervals as CSV and JSON, and provide a wipe. Trap: Shipping a feature called blocking that edits a system file or closes a window, when any determined user reopens it in seconds and a permission prompt was needed to do even that, trades a real privilege for a placebo and teaches the user their tool lies to them. Find out what your platform's documented mechanism actually is, implement that, and name the feature after what it does. Done when: The blocking feature names its mechanism and its limits in the interface, not only in the README, and a reader can tell from the label whether it is enforcement or a nudge. Any permission is requested through the platform's documented flow, with the reason shown before the prompt, and the app works with the permission declined. Sessions and intervals export as CSV and JSON that open elsewhere, including outcomes and suspends. A wipe removes the database and derived files, verified by inspecting the data directory. Do not build yet: Nothing after this. ### Out of scope (and why) - Cross-device sync, which turns a single-file local app into a conflict-resolution problem for very little gain on a timer. - Deep per-application activity tracking, which is surveillance of yourself that you will not enjoy reading and which needs permissions disproportionate to a timer. - Calendar integration, which is a provider-specific timezone problem larger than this whole application. - Enforcement-grade blocking through privileged helpers or system extensions, which is a signed, notarised, separately-updated component and a project of its own. ### README must contain - That the timer stores instants and computes remaining, so a crash or a sleep never loses a session - What happens to a session that spans a system suspend, and that the rule was chosen deliberately - That the monotonic clock's behaviour across suspend is not guaranteed by the platform, with the Rust documentation cited - That an abandoned session is recorded as abandoned and excluded from focus totals - Exactly what the blocking feature does on each platform, which permission it needs, and what it cannot stop
# Architecture · Session ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, and a tray presence | a timer you have to find a window for is a timer you stop using | | Storage | SQLite, sessions stored as intervals rather than a countdown | a remaining-seconds number cannot survive a crash | | Time | Wall-clock instants for scheduling, both clocks recorded, suspends detected | the monotonic clock's behaviour across sleep is not portable | | Input | Every action keyboard-reachable, with a global shortcut to start and stop | used dozens of times a day means never reaching for the mouse | | Blocking | Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more | a block that pretends to be enforcement is a lie you tell yourself | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Time | Instants, dual clock readings and suspend detection. | Not swappable for a stored countdown, which is the bug this design prevents. | | Intervals | The append-only session state machine. | Any store, provided totals are derived and never edited directly. | | Notify | Natively scheduled announcements. | Any mechanism, provided it does not depend on a foreground window. | | Stats | Outcomes and rebuildable daily totals. | Any presentation, provided abandoned time stays visible. | | Blocking | The platform mechanism and its honest label. | Any mechanism, provided the label matches what it actually does. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATA_DIR` · required · The SQLite file with sessions, intervals and suspends. - `DEFAULT_SESSION_MINUTES` · required · The planned duration for a new session. Stored per session, so changing it does not rewrite history. - `DEFAULT_BREAK_MINUTES` · required · Breaks are recorded like sessions and can be skipped. - `SUSPEND_THRESHOLD_SECONDS` · required · A clock divergence larger than this is treated as a suspend and resolved by the documented rule. - `GLOBAL_SHORTCUT` · required · Starts and stops without the window. Conflicts are the commonest setup failure. - `BLOCKING_MODE` · required · Whatever your platform genuinely supports. The interface label is generated from this value. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Session product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, and a tray presence, SQLite, sessions stored as intervals rather than a countdown, Wall-clock instants for scheduling, both clocks recorded, suspends detected, Every action keyboard-reachable, with a global shortcut to start and stop, Opt-in, permission-gated, and honestly described as a nudge unless the platform offers more. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. - Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. - Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. - Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately. - Shipping a feature called blocking that edits a system file or closes a window, when any determined user reopens it in seconds and a permission prompt was needed to do even that, trades a real privilege for a placebo and teaches the user their tool lies to them. Find out what your platform's documented mechanism actually is, implement that, and name the feature after what it does.
# Delivery milestones · Session Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Time that survives a closed lid A session's end is derived from recorded instants, and a suspend is detected and recorded rather than silently absorbed. ### Steps 1. Store instants, compute remaining The countdown is a rendering, not a state. Files: `src-tauri/src/session.rs`, `src/timer/remaining.ts` 2. Record both clocks on every tick Their disagreement is the only way to see a suspend. Files: `src-tauri/src/clocks.rs`, `tests/suspend-detection.test.ts` 3. Write the suspend rule down Does a slept twenty minutes count as focus? Decide once, in the README. Files: `docs/time-policy.md` ### Done when - [ ] A real suspend produces a recorded row and the documented outcome - [ ] A forward clock change does not complete a session early - [ ] A crash and reopen restores the correct remaining time - [ ] No remaining-seconds value is stored anywhere ### Watch out - Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session. ## M2 · Start, pause, stop, from the keyboard The session state machine is append-only and every transition has a key. ### Steps 1. Append intervals, never edit a total The total is a query; the intervals are the truth. Files: `src/timer/intervals.ts` 2. Give every action a key Including the ones you think nobody uses. Files: `src/shortcuts.ts`, `tests/keyboard-complete.test.ts` 3. Show state in the tray So the timer does not need a window to be useful. Files: `src-tauri/src/tray.rs` ### Done when - [ ] Focus time excludes pauses, computed from intervals - [ ] A full session completes using only the keyboard - [ ] The global shortcut works with the window closed - [ ] A crash during a pause resumes paused, correctly ### Watch out - Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only. ## M3 · A chime that arrives on time The end of a session is announced when it happens, whatever the window was doing. ### Steps 1. Schedule against the end instant natively The web view tick is decoration. Files: `src-tauri/src/notify.rs` 2. Recompute on focus and on resume Two moments where a stale display becomes a wrong decision. Files: `src/timer/sync.ts` 3. Handle an end instant already in the past Sleeping through the end is the normal case, not the edge case. Files: `src/timer/sync.ts`, `tests/late-resume.test.ts` ### Done when - [ ] A hidden window still announces on time - [ ] Refocusing shows correct remaining immediately - [ ] Waking after the end announces and resolves correctly - [ ] Preferences persist and a silent session still records ### Watch out - Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only. ## M4 · History that does not flatter you Statistics count what happened, and an abandoned session is not a completed one. ### Steps 1. Record an outcome for every session Four outcomes, and only one of them is completed. Files: `src/sessions/outcome.ts`, `tests/abandoned.test.ts` 2. Rebuild daily totals from intervals And assert the rebuild matches, so the derived table cannot drift. Files: `src/stats/rebuild.ts` 3. Show the sessions you did not finish The number that is actually useful is the honest one. Files: `src/views/history.tsx` ### Done when - [ ] A closed-app session is recorded abandoned - [ ] Rebuilt daily totals match stored rows - [ ] Edited outcomes are marked as edited - [ ] Abandoned and interrupted sessions are visible ### Watch out - Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately. ## M5 · Blocking, described honestly, and everything exportable (production only) Whatever blocking the platform actually allows is what is offered, named accurately, and the data leaves in an open format. ### Steps 1. Implement only what the platform documents Read the platform's own guidance before writing a line of this. Files: `src-tauri/src/blocking.rs`, `docs/blocking-per-platform.md` 2. Name the feature after what it does Nudge or block, in the label, not in a footnote. Files: `src/views/blocking.tsx` 3. Export intervals, not just totals Totals are opinions; intervals are the data. Files: `src/export.ts` ### Done when - [ ] The blocking label states the mechanism and its limits - [ ] Permissions are requested with a reason and declining still works - [ ] Exports include outcomes, intervals and suspends - [ ] The wipe leaves nothing in the data directory ### Watch out - Shipping a feature called blocking that edits a system file or closes a window, when any determined user reopens it in seconds and a permission prompt was needed to do even that, trades a real privilege for a placebo and teaches the user their tool lies to them. Find out what your platform's documented mechanism actually is, implement that, and name the feature after what it does.
# Operations · Session ## Backup The data directory, which is one small file, plus an occasional CSV export. A year of focus history is a few megabytes and is the only thing here that cannot be rebuilt. ## Restore Restore and rebuild the daily totals from intervals, then compare them with the restored rows. A mismatch means the derived table drifted, which is worth knowing before you trust a chart. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool this is the suite: the suspend test on a real machine, the clock-change tests, the hidden-window notification test, and the daily rebuild assertion. Any of them failing means the history has become fiction. ## Incident checklist A session that came out wrong is diagnosed from its intervals and suspends rows, which record both clocks, so the answer is in the data rather than in a guess about what the machine was doing. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A real suspend is confirmed to produce the documented outcome - [ ] A forward clock change is confirmed not to complete a session early - [ ] A hidden window is confirmed to announce on time - [ ] A closed-app session is confirmed recorded as abandoned - [ ] The blocking label is confirmed to match the implemented mechanism ## Launch constraint Do not market omitted Session capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The SQLite file with sessions, intervals and suspends. DATA_DIR=~/.local/share/focus # Required. The planned duration for a new session. Stored per session, so changing it does not rewrite history. DEFAULT_SESSION_MINUTES=25 # Required. Breaks are recorded like sessions and can be skipped. DEFAULT_BREAK_MINUTES=5 # Required. A clock divergence larger than this is treated as a suspend and resolved by the documented rule. SUSPEND_THRESHOLD_SECONDS=60 # Required. Starts and stops without the window. Conflicts are the commonest setup failure. GLOBAL_SHORTCUT=CmdOrCtrl+Shift+F # Required. Whatever your platform genuinely supports. The interface label is generated from this value. BLOCKING_MODE=nudge
$ choose a build depth, inspect the files, then open the complete pack in your agent
Session: People keep paying because a utility used dozens of times a day must be instant, predictable, and compatible with every OS update.
xcross-device sync
xdeep operating-system edge-case coverage
xpolished shortcuts and accessibility
xvendor-maintained updates
Session pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | Unlimited focus and rest sessions; analytics limited to the past 2 days; calendar view included. |
| pro | $4.99/user | $3.33/user | Full analytics, planning, blocking, and Pro features. |
free tierUnlimited focus and rest sessions; analytics show only the past 2 days.
billingmonthly + annual; 7-day trial with no subscription started
hidden costsThe $3.33 figure on file is the annual-billing equivalent; month-to-month Pro is $4.99. Prices can differ by region and subscriptions auto-renew; the 30% student discount lasts 1 year and does not renew.
verified 2026-08-14 · source ↗
Vibecode Session
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Session replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Session cost?
Session costs about $3.33/month (Pro, checked 2026-07-31), which is $39.96 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Session?
Honestly: cross-device sync; deep operating-system edge-case coverage; polished shortcuts and accessibility; vendor-maintained updates. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Session?
Yes: ActivityWatch (Local-first automatic time tracking with an extensible event model.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.