Skip to content

feat(categories): offer url and $domain as rule match fields - #1171

Draft
BMMach wants to merge 9 commits into
ActivityWatch:masterfrom
BMMach:feat/categories-url-match-fields
Draft

BMMach wants to merge 9 commits into
ActivityWatch:masterfrom
BMMach:feat/categories-url-match-fields

Conversation

@BMMach

@BMMach BMMach commented Oct 21, 2026 •

Copy link
Copy Markdown

Stacked on #1153, so it needs that merged first. Only the last commit (816765e) is new; I'll rebase the first four away once #1153 lands. Draft until then.

Problem

#1153 merges browser url/$domain into the window events that categorize() sees. But the rule editor's match fields still offer only app and title (CANONICAL_SELECT_KEYS). The only way to use the URL is a rule with blank match fields, and that matches every string field at once. There's no way to write a rule from the editor that matches only the URL or only the domain.

Change

  • src/util/classes.ts: add BROWSER_SELECT_KEYS = ['url', '$domain'].
  • src/components/CategoryEditModal.vue: offer those keys next to app/title only when the startup probe from feat(queries): enrich window events with browser url/$domain via merge_subwatcher_fields #1153 found merge_subwatcher_fields.
    • In feat: more categorization and support for select_keys #286, ErikBjare asked for a select_keys UI before the default categories use it. This extends that UI to the browser fields.
    • On macOS, aw-watcher-window records url itself for some browsers, so a url rule could match there even on older servers. I didn't special-case that.
  • A key already stored on a rule is still shown either way, as before.
  • The $domain value is the hostname without a leading www., as computed by split_url_events.

There are no query, server or i18n changes, so query performance is unchanged. Field names render as-is, like app/title today.

Known limitations

  • Single-device only. canonicalEvents() merges browser fields only when the caller passes browser buckets.
    • Today that means the single-device Activity view (including the category history that Report's chart also reads) and AI Summary.
    • Report's event list, Trends, Alerts, Graph, the Timeline category row, the category builder and the all-devices view don't pass them. There, a rule scoped only to url/$domain matches nothing, except url on macOS, where aw-watcher-window records it.
    • Passing browser buckets in those views changes query cost and how blank-field rules behave, so I'd rather do it as a separate PR.
  • Stored keys on older servers. A rule that already stores url/$domain (for example from an imported preset) still shows them on older servers, where they won't match.
  • No browser bucket. The keys are offered whenever the server supports the merge, even without an aw-watcher-web bucket.
  • Timeline coloring. The Timeline colors and filters raw bucket events in the browser. Raw window events have no url (except on macOS), and raw web events have no $domain, so $domain rules don't color Timeline rows, and url rules only match browser rows.

Tests

  • CategoryEditModal tests check three things: url/$domain are offered only when supportsMergeSubwatcherFields() is true, a stored url stays visible when it isn't, and with the flag on a stored url is listed once.
  • The new cases in browserRegexServer.test.node.ts use a minute of Edge, then a minute of Code.exe with the same tab still reported active, and a Code.exe title that a blank-field rule would match. With the merge, a url- or $domain-scoped rule claims only the Edge minute. Without it, everything stays Uncategorized.
  • The suite's probe now fails loudly unless the server's error names the missing function, so a broken probe can't turn these cases into no-ops.
  • These cases pass against aw-server-rust master (1d101d4) and against Python aw-server with aw-core master (28c3acf). They fail if the scoping or feat(queries): enrich window events with browser url/$domain via merge_subwatcher_fields #1153's merge line is removed.
  • make lint, npm run check:locales, npm run typecheck:test and npx jest (with a local server) pass after merging current master.

Out of scope

Refs ActivityWatch/activitywatch#352

🤖 Generated with Claude Code

…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
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
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
@greptile-apps

greptile-apps Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The change affects category matching in a bounded set of browser-aware activity views.

The PR appears safe to merge, with a non-blocking startup feedback issue.

Findings

  1. P2 Startup shows a blank page ▶

Summary

Adds url and $domain to the category rule editor when the server supports copying browser fields onto window events.

  • Category rules can match browser URLs and domains.

BMMach explicitly acknowledges single-device coverage, stored fields on older servers, offering fields without browser buckets, and Timeline coloring as known limitations. BMMach also leaves merge order, incognito behavior, conflict policy, blank-field rules, and the external documentation update out of scope.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Load settings] --> B[Fetch server info]
  B --> C[Probe browser-field merge support]
  C --> D[Render views]
  D --> E[Offer supported rule fields]
  D --> F[Build activity query]
  F --> G{Supported and browser buckets supplied?}
  G -->|Yes| H[Copy URL and domain onto window events]
  G -->|No| I[Keep window events unchanged]
  H --> J[Apply category rules]
  I --> J
Loading

Reviews (1) · Last reviewed commit: "feat(categories): offer url and $domain ..." · Reviewed by Greptile

Comment thread src/App.vue
Comment on lines 68 to +69
await serverStore.getInfo();
this.loaded = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Startup shows a blank page

Waiting for getInfo() now hides the entire page, including navigation and the error boundary. If the server stalls after settings load, the info request and capability probe run sequentially and can each take the default 30-second timeout. Users see a blank page for up to another minute.

Keep views gated until the probe finishes, but show a loading message while they wait.

…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 and others added 5 commits October 23, 2026 12:38
Git-Session-Id: 31183379-9a70-5e62-bc65-142d2a63be63
Git-Session-Id: 77734be4-4d7d-52ca-8645-c3eea4ffe632
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
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
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>
@BMMach
BMMach force-pushed the feat/categories-url-match-fields branch from 816765e to 0b32b32 Compare October 24, 2026 00:59
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.

2 participants