feat: add WhatsApp chat name tracking for macOS#122
feat: add WhatsApp chat name tracking for macOS#122rossigiulio wants to merge 1 commit intoActivityWatch:masterfrom
Conversation
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 SummaryThis PR adds WhatsApp-specific handling to Key observations:
Confidence Score: 4/5Safe to merge with the caveat that The logic is correct and the fallback chain is solid, but the
Important Files Changed
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
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(); |
There was a problem hiding this comment.
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") { |
There was a problem hiding this comment.
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.
| if (identifier && identifier === "NavigationBar_HeaderViewButton") { | |
| if (identifier === "NavigationBar_HeaderViewButton") { |
| // 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
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
printAppStatus.jxa(before the switch statement)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
Example Output
{"app":"WhatsApp","title":"Alice","url":null,"incognito":null}