Skip to content

feat: add WhatsApp chat name tracking for macOS#122

Open
rossigiulio wants to merge 1 commit intoActivityWatch:masterfrom
rossigiulio:feat/whatsapp-chat-tracking
Open

feat: add WhatsApp chat name tracking for macOS#122
rossigiulio wants to merge 1 commit intoActivityWatch:masterfrom
rossigiulio:feat/whatsapp-chat-tracking

Conversation

@rossigiulio
Copy link
Copy Markdown

Summary

Adds support for tracking individual WhatsApp chat names on macOS, providing more granular time tracking within WhatsApp
conversations.

Motivation

Currently, the watcher only captures "WhatsApp" as the app name, making it impossible to know which specific chat/conversation
was active. This enhancement uses macOS Accessibility API to capture the selected chat name.

Changes

  • Added WhatsApp-specific handling in printAppStatus.jxa (before the switch statement)
  • Searches for AXHeading elements with identifier "NavigationBar_HeaderViewButton"
  • Uses AXDescription attribute to extract chat name (Electron app compatibility)
  • Falls back to default window title if chat name cannot be found
  • Only affects WhatsApp Desktop on macOS; other apps unchanged

Data Format

  • app: "WhatsApp"
  • title: Chat name (e.g., "Alice", "Work Group")

This matches ActivityWatch's existing currentwindow schema and keeps categorization/reporting consistent with other apps.

Testing

  • Tested on macOS with WhatsApp Desktop
  • Verified chat names appear correctly in activity data
  • Confirmed fallback behavior works when chat name unavailable
  • Other apps continue to work as expected

Example Output

{"app":"WhatsApp","title":"Alice","url":null,"incognito":null}

  Enhances the macOS watcher to capture the active chat name from
  WhatsApp Desktop app using accessibility API. The title field
  now shows just the chat name (e.g., 'Alice'), allowing users to
  track time spent in specific WhatsApp conversations.

  - Adds WhatsApp-specific handling in printAppStatus.jxa
  - Searches for AXHeading with NavigationBar_HeaderViewButton identifier
  - Uses AXDescription attribute (Electron app compatibility)
  - Maintains fallback to default behavior if chat name not found
  - Only affects WhatsApp Desktop on macOS, other apps unchanged
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 27, 2026

Greptile Summary

This PR adds WhatsApp-specific handling to printAppStatus.jxa that uses the macOS Accessibility API to extract the active chat name and surface it as the window title, providing finer-grained time tracking inside WhatsApp conversations. The approach is well-structured: it runs before the switch statement (avoiding hidden-character matching issues), includes fallback logic at multiple levels, and correctly guards the default case so non-WhatsApp apps are unaffected.

Key observations:

  • The main concern is a significant performance issue: mainWindow.entireContents() is called on every watcher poll while WhatsApp is frontmost. For a large Electron app this synchronously materialises the entire AX element tree — potentially thousands of nodes — and is known to take hundreds of milliseconds to seconds per invocation. A targeted query (e.g. uiElements.whose({ role: "AXHeading" })) would be substantially cheaper.
  • A minor redundant guard (identifier && identifier === ...) and a duplicated windows().find() call in the catch block are worth cleaning up for clarity.
  • The overall fallback chain (chat name → window title → undefined) is sound and the change is scoped correctly to WhatsApp only.

Confidence Score: 4/5

Safe to merge with the caveat that entireContents() on every poll will measurably degrade watcher performance whenever WhatsApp is the active app.

The logic is correct and the fallback chain is solid, but the entireContents() call introduces a genuine performance regression on every poll cycle while WhatsApp is focused. This is a P1 concern affecting watcher reliability in the stated primary use case, so a score of 4 is appropriate until the AX element traversal is made more targeted.

aw_watcher_window/printAppStatus.jxa — specifically the entireContents() call and the duplicated catch-block window lookup.

Important Files Changed

Filename Overview
aw_watcher_window/printAppStatus.jxa Adds WhatsApp-specific pre-switch handling to extract the active chat name via the macOS Accessibility API. Logic and fallback coverage are correct, but entireContents() fetches the complete AX element tree on every poll, which is a significant performance concern for an Electron app the size of WhatsApp.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Script starts - get appName] --> B{appName contains WhatsApp?}
    B -- Yes --> C[Find AXMain window]
    C --> D{mainWindow found?}
    D -- Yes --> E[Call entireContents - fetch ALL AX elements]
    E --> F{Loop: find AXHeading with NavigationBar_HeaderViewButton}
    F -- Found --> G[Read AXDescription as chatName]
    F -- Not found --> H{chatName non-empty?}
    G --> H
    H -- Yes --> I[title = chatName]
    H -- No --> J[title = AXTitle fallback]
    D -- No --> K[title stays undefined]
    C -- Exception --> L[Catch block - retry windows find]
    L --> M{mainWindow found?}
    M -- Yes --> N[title = AXTitle]
    M -- No --> O[title = undefined]
    B -- No --> P[switch on appName]
    I --> P
    J --> P
    N --> P
    O --> P
    K --> P
    P --> Q{default case - title undefined?}
    Q -- Yes --> R[Find AXMain window and set title = AXTitle]
    Q -- No --> S[title preserved]
    R --> T[JSON.stringify output]
    S --> T
Loading

Reviews (1): Last reviewed commit: "feat: add WhatsApp chat name tracking fo..." | Re-trigger Greptile


// The chat name appears as an AXHeading with identifier "NavigationBar_HeaderViewButton"
// In WhatsApp/Electron apps, the text is in AXDescription, not AXLabel
var allElements = mainWindow.entireContents();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 entireContents() is called on every poll — severe performance cost

mainWindow.entireContents() synchronously fetches the entire macOS Accessibility UI element tree for the WhatsApp window. For a complex Electron app like WhatsApp, this can return thousands of elements and is known to take several hundred milliseconds to multiple seconds per call.

Since printAppStatus.jxa is re-executed on every watcher poll (default: 1–2 seconds), this means the watcher will stall on every tick while WhatsApp is the frontmost app. In practice this will result in significant CPU usage and degraded watcher accuracy.

A much cheaper approach is to avoid a full tree traversal and instead use a targeted AX query:

// Cheaper: search only AXHeading elements directly
// rather than fetching every descendant via entireContents()
var headings = mainWindow.uiElements.whose({ role: "AXHeading" })();
for (var i = 0; i < headings.length; i++) {
  try {
    var identifier = headings[i].attributes.byName("AXIdentifier").value();
    if (identifier === "NavigationBar_HeaderViewButton") {
      chatName = headings[i].attributes.byName("AXDescription").value();
      break;
    }
  } catch (e) { /* continue */ }
}

Even if whose doesn't descend recursively, it avoids materializing thousands of elements in one shot and is meaningfully faster for common-case searches.

try {
// Check if it has the NavigationBar identifier (WhatsApp-specific)
var identifier = elem.attributes.byName("AXIdentifier").value();
if (identifier && identifier === "NavigationBar_HeaderViewButton") {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Redundant truthy check before strict equality

identifier && identifier === "NavigationBar_HeaderViewButton" — the leading identifier && guard is unnecessary because if identifier is null, undefined, or "", the strict === comparison already evaluates to false. The guard adds no safety but slightly obscures intent.

Suggested change
if (identifier && identifier === "NavigationBar_HeaderViewButton") {
if (identifier === "NavigationBar_HeaderViewButton") {

Comment on lines +70 to +82
// If anything fails, fall back to default title behavior
try {
mainWindow = oProcess.
windows().
find(w => w.attributes.byName("AXMain").value() === true);
if (mainWindow) {
title = mainWindow.attributes.byName("AXTitle").value();
}
} catch (e2) {
// Final fallback
title = undefined;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Catch block duplicates the windows().find() call unnecessarily

The outer catch (e) block repeats the same windows().find() call as the try block. If windows().find() itself threw, mainWindow is unset and the retry makes sense — but if mainWindow was found successfully and a later step threw (e.g., during entireContents() or the loop), the retry needlessly calls windows().find() a second time.

Consider a two-phase approach: get mainWindow unconditionally first, then attempt the chat-name extraction in a separate inner try/catch, falling back to AXTitle on failure. This removes the duplication and makes the control flow easier to follow.

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.

1 participant