Skip to content

feat: opt-in setting to pause heartbeats while browser is unfocused - #252

Open
TimeToBuildBob wants to merge 5 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/pause-when-unfocused
Open

TimeToBuildBob wants to merge 5 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/pause-when-unfocused

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in setting, "Only report while the browser is focused"
(default off, so existing behavior is unchanged), that stops heartbeats
while no browser window has OS focus and resumes immediately when focus
returns.

  • Listens for windows.onFocusChanged; WINDOW_ID_NONE pauses heartbeats,
    refocus sends an immediate heartbeat so the gap ends right away instead of
    waiting for the next alarm tick.
  • On every alarm tick, re-polls windows.getLastFocused() as a fallback,
    since focus-change events are unreliable on some Linux window managers
    (noted in the issue).
  • Gates heartbeatAlarmListener, tabActivatedListener, and
    tabUpdatedListener on the new check so no heartbeat is sent while paused
    and the setting is on.

This repo has no existing unit-test framework (no jest/vitest, no test
script, CI only builds/lints) — there's nothing to extend here, so
verification is: npx tsc --noEmit clean, npx vite build --mode production clean, pre-commit hooks clean. Manual Chrome/Firefox
verification (switching apps for a couple minutes and checking for a gap in
the bucket, as suggested in the issue) still needs a human with a desktop
browser — happy to iterate on review feedback.

Fixes #140.

Test plan

  • npx tsc --noEmit — clean
  • npx vite build --mode production — clean
  • pre-commit hooks — clean
  • Manual verification in Chrome and Firefox against :5600 (maintainer/reviewer, no desktop browser available in this environment)

@greptile-apps

greptile-apps Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; the previous browsing-time issue is fixed, and no new actionable issues were found.

Summary

The PR adds an opt-in setting to stop reporting while the browser is unfocused. The latest changes fix how missed focus events are handled:

  • Browser heartbeats pause when no browser window has focus.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Heartbeat alarm] --> B[Poll browser focus]
  B --> C{Pause setting enabled?}
  C -->|No| D[Send normal heartbeat]
  C -->|Yes| E{Focus state}
  E -->|Focus just lost| F[Clear stored data without sending]
  E -->|Still unfocused| G[Skip heartbeat]
  E -->|Focus just returned| H[Send heartbeat with pulsetime zero]
  E -->|Still focused| D
Loading

Reviews (4) · Last reviewed commit: "fix(heartbeat): don't stamp poll-detecte..." · Reviewed by Greptile

Comment thread src/background/heartbeat.ts Resolved
Comment thread src/background/main.ts Resolved
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Fixed both Greptile findings in b178771:

P1 — Short pauses can disappear

Root cause: on refocus, sendInitialHeartbeat used the same tab data as the last pre-pause heartbeat. With AW's 100s pulsetime, any unfocused interval shorter than that would be merged away, making the user appear continuously active.

Fix: on focus loss, send a standard-pulsetime heartbeat to close the AW event at exactly the focus-loss time, then clear the cached heartbeat data. On focus gain, sendInitialHeartbeat is called with pulsetime=0, which tells AW to create a new event at the exact refocus time — no merging regardless of gap length.

P2 — Startup heartbeat ignores pause

Root cause: refreshWindowFocus() was fired-and-forgotten (line 88) concurrently with the clientReady startup chain, so the initial heartbeat could fire before the focus state was updated from the startup default of isWindowFocused=true.

Fix: refreshWindowFocus() is now chained immediately before sendInitialHeartbeat in the startup sequence. Additionally, sendInitialHeartbeat itself now checks shouldPauseForUnfocus() as a safety net — if the browser is unfocused when it fires, the heartbeat is skipped.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread src/background/heartbeat.ts Resolved
@TimeToBuildBob
TimeToBuildBob force-pushed the fix/pause-when-unfocused branch from a1a3902 to 25abb19 Compare October 10, 2026 10:23
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Addressed the remaining P1 ("Polled focus skips gap handling") in 25abb19.

When the alarm poll detects a focus loss that the focus-change event missed, it now closes the AW event at the poll time and clears the stored heartbeat data, the same as the event path. Refocus then starts a fresh event instead of merging across the gap.

Verified: npx tsc --noEmit passes on 25abb19. Re-triggered Greptile on this head.

Comment thread src/background/heartbeat.ts Resolved
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

CI-green and mergeable (Greptile 5/5) — waiting only on a maintainer click.

This PR is ready to merge, but the bot has pull-only access to this repo and can't self-merge — surfacing it here so it isn't lost. The monitoring loop will stop re-flagging it now that this note is posted.

TimeToBuildBob and others added 5 commits October 26, 2026 17:42
Adds "Only report while the browser is focused" (default off) in settings.
When enabled, heartbeats stop while no browser window has OS focus
(windows.onFocusChanged / WINDOW_ID_NONE), and resume immediately on
refocus. Alarm ticks also re-poll windows.getLastFocused as a fallback,
since focus-change events are unreliable on some Linux window managers.

Fixes ActivityWatch#140

Git-Session-Id: eabf
Two bugs in the pause-when-unfocused feature that could cause the
unfocused interval to disappear from the recorded timeline:

1. Short pauses erased (P1): on refocus, the immediate heartbeat had
   the same tab data as the last pre-pause heartbeat. AW's pulsetime
   merging would extend the pre-pause event across the gap, making
   the user appear active during the unfocused interval.

   Fix: on focus loss, send a standard-pulsetime heartbeat to close
   the event at exactly T_loss, then clear the cached heartbeat data.
   On refocus, send with pulsetime=0, which forces AW to create a new
   event at T_gain regardless of how short the gap was.

2. Startup heartbeat ignores pause (P2): the refreshWindowFocus()
   call at startup ran concurrently with the sendInitialHeartbeat
   chain rather than before it, so the focus state could be stale
   when the initial heartbeat fired. Starting while unfocused could
   record an unwanted heartbeat.

   Fix: chain refreshWindowFocus() immediately before
   sendInitialHeartbeat in the startup sequence, and add an explicit
   shouldPauseForUnfocus() guard inside sendInitialHeartbeat.

Git-Session-Id: a1e646d2-a618-5449-a47f-ce4f6402a5b7
When a Linux WM misses a focus-change event, the alarm fallback poll
detects the focus-loss transition but previously skipped the gap-closing
logic (send-close-heartbeat + clearHeartbeatData). The next alarm tick
after refocus would then send a normal-pulsetime heartbeat that could
merge the unfocused interval into the activity record.

Fix: capture wasFocused before refreshWindowFocus(); on a detected
wasFocused → !isWindowFocused transition, apply the same logic as
windowFocusChangedListener — close the AW event at the current time and
clear stored data — so the eventual refocus heartbeat starts fresh.

Git-Session-Id: a9397931-a8f3-58e9-8d53-f8c9b0b513ad
When a focus-change event is missed, the alarm poll knows the browser
is unfocused but not when focus was lost. Sending a closing heartbeat
stamped at poll time extends the previous AW event through the
unfocused interval (heartbeat at 12:00, focus lost at 12:00:05, poll
at 12:01 → 55s of away-time counted as browsing).

Fix: on poll-detected loss, only clear stored heartbeat data so the
event stays at the last known heartbeat. On poll-detected gain, send
with pulsetime=0 (same as the event-path refocus) so a short gap is
not merged by a normal-pulsetime alarm tick.

Git-Session-Id: f2811f35-94c5-5f35-ab15-d55ec4b432cc
Git-Session-Id: 955022f9-0aae-5ac3-8520-7d008562f81b
@TimeToBuildBob
TimeToBuildBob force-pushed the fix/pause-when-unfocused branch from 93d600f to 8e399b3 Compare October 26, 2026 17:45
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Rebased onto current master and pushed 8e399b3 with an explicit lease on the previous head.

The heartbeat conflict now preserves both group-only tab updates from master and the opt-in focus gate. The integration tests also caught an ordering regression: awaiting the pause setting before taking the tab snapshot let subsequent tab mutations leak into earlier events. The pause checks now run inside the serialized heartbeat queue, preserving synchronous snapshots and queue order.

Verified locally:

  • All 40 tests pass (node --test tests/*.test.*), including four added cases for opt-in suppression, default-off behavior, polled loss/refocus and event-driven loss/refocus.
  • TypeScript compilation and Chrome/Firefox production builds pass.
  • Pre-commit checks pass.

Native browser/OS-focus verification remains outstanding; these are deterministic production-module tests with browser/storage/client stubs, not an installed-extension check. CI is running on the new head.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Option to not announcing page data when browser is not active

1 participant