Skip to content

feat(queries): enrich window events with browser url/$domain via merge_subwatcher_fields - #1153

Open
TimeToBuildBob wants to merge 8 commits into
ActivityWatch:masterfrom
TimeToBuildBob:feat/canonicalize-browser-url-enrichment
Open

TimeToBuildBob wants to merge 8 commits into
ActivityWatch:masterfrom
TimeToBuildBob:feat/canonicalize-browser-url-enrichment

Conversation

@TimeToBuildBob

@TimeToBuildBob TimeToBuildBob commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Refs ActivityWatch/activitywatch#352 — «Can't categorize activities by URLs». This PR adds query enrichment; the rule-editor picker is the separate stacked #1171, and browser coverage remains caller-dependent.

What

Copy url and $domain from browser events into desktop window events before categorize() runs, so URL-based category rules can match browser activity.

The backend function is provided by ActivityWatch/aw-server-rust#824, merged as 1d101d4c41b7c434f0e06e89bfceb8abe30ba364, and by aw-core for Python aw-server. That SHA belongs to aw-server-rust, not aw-webui. It splits base events at overlapping subwatcher boundaries while preserving total base duration; event count can increase. It does not add time outside the base window stream.

  • Enrichment runs before the stopwatch union, keeping explicitly tracked stopwatch events outside this merge.
  • Incognito browser events are excluded from this merge.
  • base_wins preserves fields already recorded by the window watcher (notably native macOS URLs).
  • categories: null skips enrichment along with categorization; an empty category list keeps the existing enabled-category semantics.
  • No browser buckets, Android queries, and unsupported servers do not emit the merge.

Server compatibility

merge_subwatcher_fields is newer than the supported server floor (v0.12.3b18). Referencing it unconditionally would fail the whole activity query with VariableNotDefined("merge_subwatcher_fields").

The webui probes at startup with:

x = merge_subwatcher_fields([], [], ["url"], "sub_wins"); RETURN = x;

Only a successful probe enables enrichment. Views mount after the probe settles, so their first queries see the support flag. Older servers retain the previous behavior without version parsing. The RETURN is essential: a program without it fails with EmptyQuery even when the function exists. The probe is shared between the store and server-backed tests.

Behavior changes and privacy limitations (unreleased notes)

These notes also ship in the README's Unreleased: browser-aware categorization section.

  • Existing category totals can change. Blank match fields inspect every string field, now including URLs/domains. An unanchored vim matches vimeo.com; ActivityWatch|aw- can match a URL containing aw-. Restrict rules to app/title to retain the previous match surface. Stored events and category rules are not rewritten.
  • Title hiding is not a cross-bucket privacy boundary. --exclude-title, --exclude-titles, and window-only privacy filters do not remove separate browser history. This merge can attach its URL to window time whose title was hidden/redacted. base_wins does not prevent that when the window has no URL. Sensitive browser data needs filtering at collection or in the browser bucket too. Excluding incognito events from this merge does not remove them from the Browser section or storage.
  • Edge extension v0.6.0: Edge may be identified as Chrome, missing Edge enrichment and potentially stamping Edge URLs onto simultaneous Chrome activity. fix(browser): detect Edge before Chrome in detectBrowser() aw-watcher-web#262 fixes identification for a future extension release; overriding the extension browser name to edge separates newly collected events, not historical data.
  • Coverage is caller-dependent: only desktop queries supplying browser buckets and enabling categorization receive enrichment. This does not wire browser buckets into every view. The rule-editor url/$domain picker stays in feat(categories): offer url and $domain as rule match fields #1171 rather than being cherry-picked here.

Performance (measured)

Prior measurements against locally built aw-server from aw-server-rust at 1d101d4c, testing mode, synthetic data in throwaway buckets: window events every ~29 seconds across an 8-hour day, browser events at 3x that rate, overlapping timestamps; seven repetitions, median:

Day's data Query without enrichment With merge_subwatcher_fields Added
1,000 window / 3,000 browser 0.8 ms 2.3 ms +1.5 ms
5,000 window / 15,000 browser 3.2 ms 12.2 ms +9.0 ms

Both columns measure the query alone (query_bucket + flood(query_bucket)); real Activity queries add intersection, categorization and aggregation. These are absolute costs, not a share of full Activity-query latency. Growth in this fixture was close to linear. These are the original merge-function measurements, not a fresh benchmark of the subsequent incognito/base-precedence guard changes.

The startup probe measured 0.2 ms median (n=20) on localhost against the same server. It is paid once per page load, including when no browser bucket exists; remote cost scales with RTT. Views remain gated during server-info/probe startup.

Verification

  • Query unit tests cover support gating, browser presence, Android exclusion, incognito filtering, base precedence, merge-before-stopwatch ordering, and the probe's RETURN.
  • Added a categories: null regression: failed before the guard, then passed. 61 query tests pass, TypeScript tsc --noEmit -p tsconfig.json passes, and ESLint on the changed TypeScript files passes.
  • The server-backed regex suite probes the running server, exercising enrichment where supported. Its existing CI jobs passed on the previous head; new-head CI is reported separately, not assumed from local tests.

…e_subwatcher_fields

Wire browser url and $domain fields into the canonical window event stream
before categorize() runs, so URL-based category rules fire on desktop.

merge_subwatcher_fields (added to aw-server-rust in ActivityWatch#824) overlays the named
fields from browser_events onto the matching window events by temporal overlap.
The call is guarded by the browser-buckets condition, so queries against
servers without the function are unaffected when no browser watcher is
installed.

Closes ActivityWatch/activitywatch#352.

Git-Session-Id: 0db2fd2e-f72d-4387-898c-6c33d3138142
@codecov

codecov Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.42857% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.63%. Comparing base (78ad8ce) to head (092a376).
⚠️ Report is 38 commits behind head on master.

Files with missing lines Patch % Lines
src/stores/server.ts 14.28% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1153      +/-   ##
==========================================
+ Coverage   72.43%   78.63%   +6.20%     
==========================================
  Files          60       73      +13     
  Lines        4183     4400     +217     
  Branches     1016     1165     +149     
==========================================
+ Hits         3030     3460     +430     
+ Misses       1148      886     -262     
- Partials        5       54      +49     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; both numbered previous findings are addressed and no new actionable issues were found.

Summary

This PR adds browser URLs and domains to desktop window events before category rules run.

  • Desktop category rules can now match browser URLs during activity queries.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Probe server support] --> B[Mount views]
  B --> C[Fetch desktop window and browser events]
  C --> D{Enrichment enabled?}
  D -->|Yes| E[Exclude incognito browser events]
  E --> F[Copy URL while preserving native URL]
  F --> G[Derive domain from chosen URL]
  D -->|No| H[Add stopwatch events]
  G --> H
  H --> I[Apply category rules]
Loading

Reviews (5) · Last reviewed commit: "fix(queries): derive enriched domain fro..." · Reviewed by Greptile

Comment thread test/unit/queries.test.node.ts Outdated
The supported server floor (v0.12.3b18, both aw-server and aw-server-rust)
predates merge_subwatcher_fields, so emitting it unconditionally broke the
activity query for every user with a browser watcher on an older server
(VariableNotDefined, whole Activity view fails) and failed the pinned-version
CI jobs.

Probe the function once at startup with a no-op query and only emit the
enrichment when the server resolves it. Views now mount after the probe, so
the first activity query already sees the correct flag.

Also strengthen the stopwatch-order test (it never supplied bid_stopwatch, so
moving the merge before the union would still have passed) and let the
server-backed regex suite exercise enrichment only when the running server
supports it.

Git-Session-Id: b38a584e-5f94-5837-baea-39b0d5af5c57
@ErikBjare

Copy link
Copy Markdown
Member

@TimeToBuildBob Some benchmarks on this would be nice in the PR description

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread src/App.vue
@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

Copy link
Copy Markdown
Contributor Author

Added a Performance section to the description. Summary:

  • Startup probe: one no-op HTTP round-trip before views mount — ~1–5 ms on localhost, scales with RTT on remote setups. Paid once per page load.
  • Per-query merge: entirely server-side, O(N + M) sorted scan. For a typical day (~1k window events, ~3k browser events) the added server time is in the low-ms range — well below the existing ~200–500 ms query latency. Client cost is a single boolean flag read.
  • No overhead at all for users without a browser watcher or on a server predating aw-server-rust#824.

aw-server answers any query program without a RETURN statement with
EmptyQuery (HTTP 400) whether or not the called function exists, so the
probe reported 'unsupported' on every server and silently disabled the
URL enrichment this PR adds. Share the probe query between the store and
the server-backed test so the two cannot drift, and assert it returns a
value.

Git-Session-Id: 2cf884cf-c63a-5199-abce-89689529d224
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Measured benchmarks are now in the PR description (replacing the estimates), plus one correctness fix found while building the harness.

Benchmarks — against a locally built aw-server at aw-server-rust@1d101d4, testing mode, synthetic data in throwaway buckets, 7 reps, median:

Day's data Without enrichment With merge_subwatcher_fields Added
1 000 window / 3 000 browser 0.8 ms 2.3 ms +1.5 ms
5 000 window / 15 000 browser 3.2 ms 12.2 ms +9.0 ms

The startup probe measured 0.2 ms median (n=20) on localhost. Growth is close to linear in event count, as expected from the sorted two-stream scan.

Correctness fix (3004fd6) — the capability probe was querying x = merge_subwatcher_fields(...) with no RETURN statement. aw-server answers any program without RETURN with EmptyQuery (HTTP 400) regardless of whether the called function exists, so the probe reported "unsupported" on every server and the enrichment would never have activated in production. Verified directly: the old probe returns {"message":"EmptyQuery"} HTTP 400 against a server that has the function; adding RETURN = x; returns [[]] HTTP 200, and still errors on a server without it.

The same broken query was used in the server-backed test's probe, which is why CI was green while the feature was dead — that test now enables the capability for real against aw-server-rust master. Probe query is shared between the store and the test so they can't drift, and a unit test asserts it returns a value.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@BMMach

BMMach commented Oct 21, 2026 •

Copy link
Copy Markdown

I checked this against current master and both servers' sources. It builds on merge_subwatcher_fields (aw-core 662aba1, which closed activitywatch#1305, and aw-server-rust#824), and it still rebases cleanly. I'd suggest three changes before merge, plus one behaviour change worth flagging.

1. Merge before the stopwatch union, not after. As written, merge_subwatcher_fields runs after events = union_no_overlap(stopwatch_events, events). Both implementations stamp every overlapping base event. So any stopwatch event that overlaps focused-browser time gets split and given url/$domain, and a rule with blank match fields can recategorize time the user tracked explicitly with the stopwatch. Moving the line above the union avoids that, and window events end up the same. It means inverting the merge call runs after the stopwatch union and before categorize test in queries.test.node.ts.

2. Exclude private-window tabs. aw-watcher-web sends incognito on every event. When the extension is allowed in private windows, those URLs already show in the Browser section. With the merge they also reach category rules. Suggestion:

events = merge_subwatcher_fields(events, exclude_keyvals(browser_events, "incognito", [true]), ["url", "$domain"], "base_wins");

exclude_keyvals exists on both servers (aw-server-rust functions.rs, aw-core q2_exclude_keyvals). This also changes the exact string in the emits merge_subwatcher_fields when browser buckets are present test.

3. base_wins instead of sub_wins? On macOS, aw-watcher-window already writes url into window events for supported browsers (macos.swift), and sub_wins overwrites it with the extension's value. title isn't in the key list, so the conflict mode only decides which url wins. base_wins is the default on both servers and keeps what the window watcher recorded. sub_wins is more uniform across platforms. Either is defensible, but it seemed worth a deliberate choice.

4. Existing rules change behaviour. Rules with no select_keys match every string field. Once browser segments carry url/$domain, existing rules start matching URLs.

  • The default Work > Programming rule contains case-sensitive vim, so every vimeo.com URL moves into Programming.
  • The default ActivityWatch|aw- rule (ignore case) matches any URL containing "aw-".

That may be the intended meaning of "Leave blank to match every string field", but it changes people's category totals. It's worth a release note or a setting.

Related: the merge adds browser URLs to window time whose title the user chose to hide.

  • --exclude-title (and matching --exclude-titles patterns) replace the title with "excluded". On macOS the watcher also drops url for those windows, and the merge puts it back.
  • An opt-in aw-server-rust privacy filter that redacts aw-watcher-window titles (the Settings example does this for banking) leaves the web bucket untouched. The redacted window time then gets the page URL.

base_wins doesn't help here, because these window events have no url key.

Minor: on master, activeDurationQuery also goes through canonicalEvents with browser buckets and categories: null. Gating the merge on params.categories as well would skip it there.

Edge, for the release notes: the released extension (v0.6.0) detects Edge as chrome, so Edge data lands in aw-watcher-web-chrome_<host>. The chrome stream is clipped to Chrome-family app names, so msedge time gets no url/$domain. If Chrome also runs, Edge heartbeats share that bucket and can be stamped onto Chrome time. This is fixed by ActivityWatch/aw-watcher-web#262, which isn't released yet. Until then, setting the browser name by hand moves new data to aw-watcher-web-edge_<host>. Not a blocker for this PR.

I've put up a small stacked follow-up that adds url/$domain to the rule editor's match fields, gated on this PR's capability probe: PR#1171. Happy for you to cherry-pick it into this PR instead if you'd rather land them together.

🤖 Drafted with Claude Code. Points 1 and 2 were also run against local aw-server-rust and aw-server builds from master.

…e_wins

Per review feedback:
- Move merge_subwatcher_fields before the stopwatch union — stopwatch events
  explicitly recorded by the user should not be split and stamped with
  browser url/$domain.
- Exclude incognito browser events so private-window URLs don't reach
  category rules via this path.
- Switch conflict mode from sub_wins to base_wins — on macOS,
  aw-watcher-window already writes url natively, and base_wins preserves it.

Update test to assert merge now runs BEFORE the stopwatch union.

Git-Session-Id: 38379c75-dec8-5614-bf5f-4c972d785e04
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — all good points.

Pushed df82e24 with three changes from your review:

1. Merge now runs before the stopwatch union. You're right that the old order could split user-tracked stopwatch time and stamp it with browser data. Moved and inverted the test assertion accordingly.

2. Incognito events excluded. Using exclude_keyvals(browser_events, "incognito", [true]) as you suggested.

3. base_wins instead of sub_wins. Agreed — on macOS the window watcher's native url is the better source; base_wins preserves it. Switched.

On your point 4 (existing rules gaining new match surface): acknowledged as a real behaviour change. I'll add a note to the PR description and release notes. The vim → vimeo.com case you flagged is a good concrete example to cite. I'd rather document it explicitly than gate the whole feature on select_keys — the blank-match semantics are correct, the surprising part is just that category authors haven't seen URLs in window events before.

Re the activeDurationQuery + categories: null edge case: I'll add the params.categories guard in a follow-up commit so the merge is a no-op there.

The Edge/aw-watcher-web bucket naming and the --exclude-title privacy note are good release-note candidates — I'll incorporate them.

On the stacked follow-up for the rule editor (the link in your comment looks like a placeholder — happy to link it properly if you post the URL): cherry-picking it in makes sense if it's small enough.

BMMach added a commit to BMMach/aw-webui that referenced this pull request Oct 21, 2026
Builds on ActivityWatch#1153, which merges browser url/$domain into the categorized
window events when the server supports merge_subwatcher_fields. This
offers those keys in the rule editor's match fields, so a rule can
target the URL or domain alone instead of every string field.

- Offer url/$domain only when the startup probe from ActivityWatch#1153 found
  merge_subwatcher_fields. Without it, $domain never reaches window
  events, and url only does where aw-watcher-window records it itself
  (macOS).
- Keep showing a stored key even when it is not offered, without
  listing it twice.
- Add server-backed cases: url- and $domain-scoped rules claim only
  focused browser time, not another app's window (even when its title
  would match), and match nothing on servers without the merge.
- Fail the server suite's probe unless the server reports the function
  as missing, so a broken probe can't turn those cases into no-ops.

Refs ActivityWatch/activitywatch#352

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TimeToBuildBob

TimeToBuildBob commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 AI code review

Adds browser URL enrichment to desktop canonicalEvents() queries before categorization and the stopwatch union, gated by a server capability flag. Adds a startup probe for merge_subwatcher_fields, delays rendering until server initialization completes, and adds query-generation and server-backed checks. Documents categorization changes, privacy limitations, and browser coverage.

Safe to merge — no P0/P1 findings

Confidence 5/5

✅ No findings. The diff looks correct to me on this pass.

Files changed (7) — the diff as I read it
  • README.md — Adds unreleased notes describing browser-aware categorization, changed matching surfaces, privacy limitations, Edge identification, and caller-dependent coverage.
  • src/App.vue — Moves serverStore.getInfo() into beforeCreate() and awaits it before setting loaded to true.
  • src/queries.ts — Conditionally merges non-incognito browser URLs into desktop window events with base_wins, then calls split_url_events() before stopwatch merging and categorization.
  • src/stores/server.ts — Adds probeMergeSubwatcherFields() and invokes it after fetching server information to update the capability flag.
  • src/util/serverCapabilities.ts — Introduces the shared probe query and getter and setter functions for the merge_subwatcher_fields capability flag.
  • test/unit/browserRegexServer.test.node.ts — Adds server capability probing and URL/domain cases covering native window URLs, browser URL fallback, and malformed native URLs.
  • test/unit/queries.test.node.ts — Adds assertions for enrichment conditions, ordering before stopwatch merging and categorization, and the probe query's return statement.
Previous review passes
commit score findings engine when
df82e244cfc7 3/5 1 subscription 2026-10-23 12:24 UTC
6c52b84f28fb 4/5 1 subscription 2026-10-23 20:24 UTC

Reviewed 092a376ec433 · panel: codex:gpt-6.1-sol · subscription engine · 30s · about this reviewer

Maintainer commands

@TimeToBuildBob review (own line) — fresh review · @TimeToBuildBob fix — a worker acts on the findings. Once per comment; 👀 = received.

Outage coverage: 1-pass, not merge evidence. Subscription review supplied findings while full consensus capacity was unavailable. The consensus merge gate remains unsatisfied.

Git-Session-Id: 31183379-9a70-5e62-bc65-142d2a63be63
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Completed the remaining follow-through in 90472e0dbbb8:

  • categories: null now skips browser enrichment. The new regression failed before the guard and passes after it.
  • Added unreleased notes to the README and PR description covering changed blank-field category matches, title-redaction/privacy limitations, Edge v0.6.0 bucket naming, and caller-dependent coverage. Also corrected the description: the merge preserves total duration, not event count, and the startup probe still runs without browser buckets.
  • @BMMach Your feat(categories): offer url and $domain as rule match fields #1171 link is valid; my earlier “placeholder” remark was wrong. Keeping that picker change stacked rather than duplicating your work here.

The latest AI-review P1 is a wrong-repository lookup, not a missing implementation: GitHub resolves ActivityWatch/aw-server-rust@1d101d4c41b7c434f0e06e89bfceb8abe30ba364 to the merged backend function. The review's “static preflight: GitHub API commit lookup” checked aw-webui instead of the explicitly named backend repo. The description now links the full backend SHA directly.

Verification: 61 query tests passed; the broader local suite passed 72 suites / 859 tests (13 skipped), excluding the server-backed browser-regex suite, which I did not rerun locally. TypeScript and targeted ESLint passed; commit hooks passed. Pushed and verified the GitHub head, then requested Greptile on that head. New-head CI/review results are pending; no merge-ready claim.

Comment thread src/queries.ts Outdated
Git-Session-Id: 77734be4-4d7d-52ca-8645-c3eea4ffe632
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread test/unit/browserRegexServer.test.node.ts
The beforeAll probe now distinguishes the expected VariableNotDefined
response (old server without merge_subwatcher_fields) from unexpected
failures (5xx, network errors). A transient HTTP 500 previously silently
disabled enrichment, making all three URL/domain tests skip instead of
fail. Now a probeError variable is set and the enrichment tests throw it
rather than returning silently.

Git-Session-Id: 7c6247ec-d763-5fa9-be60-11ec4e47d9c0
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Fixed in 92d93099: the probe beforeAll now distinguishes the expected VariableNotDefined response (unsupported old server) from unexpected failures. A transient HTTP 500 (or any other non-OK response that doesn't contain VariableNotDefined) is recorded as probeError; the three enrichment tests then throw it rather than silently returning. The browser-regex tests that don't depend on merge_subwatcher_fields are unaffected.

aw-server (Python) rejects the unknown merge_subwatcher_fields call with
QueryInterpretException "Tried to call function ... which doesn't exist",
not aw-server-rust's VariableNotDefined. The probe classified it as an
unexpected error, failing the three enrichment tests on the Python leg.

Git-Session-Id: 58312207-09b2-4529-81a4-643fa48042ee
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

CI fix in 092a376: the Python aw-server leg (v0.12.3b18) failed because the server test's probe only treated aw-server-rust's VariableNotDefined as "unsupported". aw-server reports the missing function as QueryInterpretException: Tried to call function 'merge_subwatcher_fields' which doesn't exist, so the probe flagged it as unexpected and the three enrichment tests threw. The test now accepts both server errors; any other non-OK response still fails loudly. Checked against a local v0.12.3b18 aw-server: 11/11 pass with the fix, and the previous head fails the same 3 tests. Test-only change; the runtime probe in stores/server.ts already treats any failure as unsupported.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Benchmarks are in the description (added 2026-10-19, measured against aw-server-rust@1d101d4c, 7 reps, median):

Day's data Without enrichment With merge_subwatcher_fields Delta
1k window / 3k browser 0.8 ms 2.3 ms +1.5 ms
5k window / 15k browser 3.2 ms 12.2 ms +9.0 ms

Startup probe cost: ~0.2 ms median on localhost (paid once per page load).

Since the last merge-ready note, BMMach's review was incorporated (df82e24): enrichment now runs before the stopwatch union (not after), incognito events are excluded, base_wins preserves native macOS URLs. A probe beforeAll robustness fix landed today (092a376e) so the server-backed tests correctly distinguish expected "unsupported" responses from unexpected failures on both aw-server-rust and Python aw-server.

CI is green across all 8 checks (vite/webpack build, lint, CodeQL, aw-server-rust master, aw-server-rust v0.12.3b18, Python aw-server v0.12.3b18). All review threads resolved. Ready for merge.

BMMach added a commit to BMMach/aw-webui that referenced this pull request Oct 24, 2026
Builds on ActivityWatch#1153, which merges browser url/$domain into the categorized
window events when the server supports merge_subwatcher_fields. This
offers those keys in the rule editor's match fields, so a rule can
target the URL or domain alone instead of every string field.

- Offer url/$domain only when the startup probe from ActivityWatch#1153 found
  merge_subwatcher_fields. Without it, $domain never reaches window
  events, and url only does where aw-watcher-window records it itself
  (macOS).
- Keep showing a stored key even when it is not offered, without
  listing it twice.
- Add server-backed cases: url- and $domain-scoped rules claim only
  focused browser time, not another app's window (even when its title
  would match), and match nothing on servers without the merge.

Refs ActivityWatch/activitywatch#352

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

3 participants