Skip to content

feat(categories): support field-scoped regex rules - #840

Merged
ErikBjare merged 4 commits into
ActivityWatch:vue3from
TimeToBuildBob:bob/select-keys-823
May 23, 2026
Merged

ErikBjare merged 4 commits into
ActivityWatch:vue3from
TimeToBuildBob:bob/select-keys-823

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Summary

  • add optional select_keys support to the web UI regex rule shape
  • expose a field selector in the category editor for canonical string fields
  • make local classification helpers and tests honor field-scoped regex rules

Verification

  • ./node_modules/.bin/jest --runInBand test/unit/classes.test.node.ts test/unit/store/categories.test.node.ts
  • ./node_modules/.bin/eslint src/util/classes.ts src/components/CategoryEditModal.vue

Closes #823

@greptile-apps

greptile-apps Bot commented May 23, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds optional select_keys support to regex category rules, letting users scope a regex to only specific event fields (app, title) rather than the full concatenated string. The change threads the raw IEvent through matchString and getCategoryColorFromEvent, so the two production classification paths (timeline filter and event coloring) now honour field-scoped rules correctly.

  • classes.ts: adds select_keys?: string[] to the Rule interface, a getRuleSelectKeys helper, and updates matchString to accept an optional IEvent; classifyEvents now calls getRuleSelectKeys per rule instead of always iterating all CLASSIFY_KEYS.
  • color.ts: getCategoryColorFromEvent is rewritten to call matchString(…, e) directly rather than delegating to getCategoryColorFromString, fixing the field-scoping gap in the colour-lookup path.
  • CategoryEditModal.vue: surfaces a checkbox-group for field selection and normalises select_keys on both load and save.

Confidence Score: 5/5

Safe to merge; all changed production paths correctly honour field-scoped rules and the modal normalises select_keys on both load and save.

The field-scoping logic is correctly implemented in both classifyEvents and the updated matchString/getCategoryColorFromEvent paths. The modal correctly initialises, displays, and strips select_keys on save. The only gap noted is a pre-existing behavioural difference between matchString's unscoped fallback (tests the concatenated string) and classifyEvents's field-by-field approach, which is not a regression introduced here.

No files require special attention.

Important Files Changed

Filename Overview
src/util/classes.ts Core logic change: adds select_keys to Rule, introduces getRuleSelectKeys, and makes matchString event-aware; classifyEvents updated to use per-rule key selection. Logic is correct and well-tested.
src/util/color.ts getCategoryColorFromEvent now calls matchString directly with the event object, fixing the field-scoping gap; behaviour is semantically equivalent to the old getCategoryColorFromString path for unscoped rules.
src/components/CategoryEditModal.vue Adds checkbox-group for field selection; resetModal and save path both normalise select_keys correctly (empty array on open, deleted from payload when empty on save).
src/views/Timeline.vue Single-line change: passes the raw event to matchString so field-scoped rules are honoured in the timeline category filter.
test/unit/classes.test.node.ts Adds three test cases covering field-scoped classifyEvents, the matchString fallback path, and cleanCategory key stripping; all cases are correct and meaningful.
test/unit/store/categories.test.node.ts New test verifies select_keys round-trips through updateClass without data loss.

Sequence Diagram

sequenceDiagram
    participant UI as CategoryEditModal
    participant CL as classes.ts
    participant TL as Timeline.vue
    participant CO as color.ts

    Note over UI: User saves category with select_keys
    UI->>CL: cleanCategory(cat) strips invalid/empty select_keys
    UI->>CL: saveClasses([...])

    Note over TL: Category filter applied
    TL->>CO: getCategorizationStringFromEvent(bucket, e)
    CO-->>TL: str (e.g. "Firefox\nSession notes")
    TL->>CL: matchString(str, allCats, e)
    CL->>CL: getRuleSelectKeys(rule) returns ['app'] or CLASSIFY_KEYS
    CL->>CL: re.test(e.data[key]) for each selected key
    CL-->>TL: matched Category or null

    Note over CO: Event colour lookup
    CO->>CO: getCategorizationStringFromEvent(bucket, e)
    CO->>CL: matchString(str, allCats, e)
    CL->>CL: field-scoped test via select_keys
    CL-->>CO: matched Category or null
    CO->>CO: getColorFromCategory(c, allCats) or fallbackColor(str)
Loading

Reviews (4): Last reviewed commit: "test(categories): add direct matchString..." | Re-trigger Greptile

Comment thread src/components/CategoryEditModal.vue Outdated
Comment on lines +188 to +191
const rule = _.cloneDeep(cat.rule);
if (rule.type === 'regex') {
rule.select_keys = rule.select_keys ? [...rule.select_keys] : [];
}

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 When resetModal is called for a category whose rule type is not regex (e.g., none), select_keys is never set on the cloned rule object. If the user then changes the type dropdown to regex, editing.rule.select_keys is undefined rather than [], and the b-form-checkbox-group v-model starts in an undefined state. While Vue 3's proxy may recover on the first selection, the initial render could produce a warning and the "Leave blank to match all fields" path would silently produce no-op checkbox state instead of a clean empty array.

Suggested change
const rule = _.cloneDeep(cat.rule);
if (rule.type === 'regex') {
rule.select_keys = rule.select_keys ? [...rule.select_keys] : [];
}
const rule = _.cloneDeep(cat.rule);
rule.select_keys = rule.type === 'regex' && rule.select_keys ? [...rule.select_keys] : [];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 30b6dee — resetModal now initializes select_keys to [] regardless of the existing rule type (not just when type is already regex), so switching a none-rule to regex always starts the checkbox group with a valid empty array.

Also added a direct matchString unit test for the event-based field-scoping path in b1bedfd (the second Greptile finding from the re-review).

…r paths

matchString now accepts an optional IEvent; when provided and the category
has select_keys set, it tests only those specific fields instead of the
pre-concatenated string.  Without the event the existing behavior is
preserved (backward-compatible).

Callers updated:
- Timeline.vue: passes e to matchString so category filter honors select_keys
- color.ts: getCategoryColorFromEvent calls matchString directly with the
  event instead of routing through getCategoryColorFromString

Also ensures select_keys is always initialized to [] in resetModal
regardless of the existing rule type, so switching a none-rule to regex
gives the checkbox group a valid empty array instead of undefined.

Addresses Greptile review findings on PR ActivityWatch#840.
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Fixed both findings in 30b6dee:

P1 (matchString ignores select_keys): Updated matchString to accept an optional IEvent parameter. When provided and the category has select_keys set, it tests each specified field individually instead of the pre-concatenated string. Callers that have the raw event (Timeline.vue, getCategoryColorFromEvent in color.ts) now pass it through. Categories without select_keys retain the existing string-matching behavior.

P2 (select_keys undefined on rule-type switch): resetModal now always initializes select_keys to [], regardless of the existing rule type. This ensures the checkbox group always has a valid v-model when the user switches a none-type rule to regex.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Greptile's re-review (on 30b6dee) correctly notes that getCategoryColorFromString in summary.ts doesn't propagate select_keys. This is an architectural constraint of the summary visualization: summary.ts operates on pre-aggregated Entry objects (name + duration) that don't carry the original IEvent, so there's no event to pass to matchString.

The three views that do have raw events — Timeline filtering, getCategoryColorFromEvent, and classifyEvents — all correctly apply field-scoped rules now. The summary bar chart's color path is a fallback that matches on the aggregated app-name string; fixing it would require classifying events before aggregation, which is a larger refactor beyond this PR's scope.

Happy to track that as a follow-up if it matters.

…lect_keys scoping

Greptile noted the new event path in matchString lacked a direct unit test.
Added matchString-with-event test covering:
- event + select_keys matches only the specified field (app hit, title-only miss)
- fallback to string matching when no event is provided
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

All Greptile findings from the review cycles have been addressed — the latest re-review came back at 5/5 confidence, safe to merge.

Quick recap of what changed since the initial draft:

  • matchString now accepts an optional IEvent; callers that have the raw event (Timeline.vue, getCategoryColorFromEvent) pass it through, so field-scoped rules are applied correctly
  • resetModal initialises select_keys: [] unconditionally to avoid undefined v-model when switching a none-type rule to regex
  • The summary.ts / getCategoryColorFromString path intentionally left unscoped — that path receives pre-aggregated Entry objects with no original event; scoping it would require classifying before aggregation (separate refactor)

Happy to rebase or make any tweaks @ErikBjare — otherwise this should be ready to merge.

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