feat: add per-file WakaTime heartbeats for write operations#449
feat: add per-file WakaTime heartbeats for write operations#449kianhub wants to merge 9 commits intoRunMaestro:mainfrom
Conversation
📝 WalkthroughWalkthroughAdds optional per-file WakaTime tracking: collects file paths from tool-execution events, detects language and branch (with TTL cache), batches per-file heartbeats via the WakaTime CLI, debounces flushes on usage, and exposes a UI setting to enable detailed tracking. Changes
Sequence DiagramsequenceDiagram
participant Process as Process Listener
participant Manager as WakaTime Manager
participant CLI as WakaTime CLI
participant Cache as Branch Cache
rect rgba(100, 150, 200, 0.5)
Note over Process,Cache: File path collection & debounce
Process->>Process: Receive tool-execution
Process->>Manager: extractFilePathFromToolExecution(...)
Process->>Process: Accumulate per-session (dedupe by path/timestamp)
Process->>Process: On usage -> schedule debounced flush
end
rect rgba(150, 100, 200, 0.5)
Note over Manager,CLI: Batch heartbeat send
Process->>Manager: sendFileHeartbeats(files[], projectPath, projectCwd)
Manager->>Manager: validate enabled / API key / CLI present
Manager->>Manager: detectLanguageFromPath(file)
Manager->>Cache: get/refresh branch (with TTL)
Manager->>CLI: execute wakatime-cli --extra-heartbeats ...
CLI-->>Manager: success / failure
Manager-->>Process: resolve
end
rect rgba(200, 100, 150, 0.5)
Note over Process: Cleanup
Process->>Process: On exit -> clear pending files & timers
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
…e map Add new `wakatimeDetailedTracking` boolean setting (default false) to types, defaults, settingsStore, and useSettings hook. This setting will gate file-level heartbeat collection in a future phase. Add EXTENSION_LANGUAGE_MAP (50+ extensions) and exported detectLanguageFromPath() helper to wakatime-manager.ts for resolving file paths to WakaTime language names. Includes 22 new tests for detectLanguageFromPath covering common extensions, case insensitivity, multi-dot paths, and unknown extensions.
…file heartbeats Add WRITE_TOOL_NAMES set (Write, Edit, write_to_file, str_replace_based_edit_tool, create_file, write, patch, NotebookEdit) and extractFilePathFromToolExecution() function that inspects tool-execution events and extracts file paths from input.file_path or input.path fields. Supports Claude Code, Codex, and OpenCode agent tool naming conventions.
…rtbeats Add public async method to WakaTimeManager that sends file-level heartbeats collected from tool executions. The first file is sent as the primary heartbeat via CLI args; remaining files are batched via --extra-heartbeats on stdin as a JSON array. Includes language detection per file, branch detection, and gating on both wakatimeEnabled and wakatimeDetailedTracking settings. 12 new tests cover all code paths.
…Time listener Add per-session file path accumulation from tool-execution events and flush as file-level heartbeats on query-complete. Controlled by the wakatimeDetailedTracking setting. Pending files are cleaned up on exit to prevent memory leaks.
Add WakaTime section to configuration.md covering setup, detailed file tracking, and per-agent supported tools. Add wakatime namespace to CLAUDE-IPC.md and feature bullet to features.md. Fix detailed file tracking toggle padding and shorten description to one line.
File heartbeats were only flushed on query-complete (batch/auto-run). Interactive chat sessions accumulated file paths but never sent them. Add a usage event handler with 500ms per-session debounce that flushes accumulated file heartbeats at end-of-turn for all session types. Extract shared flushPendingFiles() helper. query-complete cancels any pending usage timer to prevent double-flush.
08e8da6 to
5b10a1c
Compare
|
Branch detection fix (
|
…artbeats Failed git lookups are no longer cached, so transient failures retry on the next heartbeat instead of suppressing the branch field for the entire session. Successful results expire after 5 minutes to pick up branch switches. File-level heartbeats now cache per project directory instead of sharing a single key across all sessions.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/stores/settingsStore.ts (1)
1807-1811:⚠️ Potential issue | 🟠 Major
setWakatimeDetailedTrackingis missing fromgetSettingsActions().
setWakatimeEnabledandsetWakatimeApiKeyare both present in the returned map, butsetWakatimeDetailedTrackingis not. Any non-React code (main-process utilities, services) that callsgetSettingsActions()will find the actionundefinedand silently fail to persist the toggle.🐛 Proposed fix
setWakatimeApiKey: state.setWakatimeApiKey, setWakatimeEnabled: state.setWakatimeEnabled, + setWakatimeDetailedTracking: state.setWakatimeDetailedTracking, setUseNativeTitleBar: state.setUseNativeTitleBar,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/stores/settingsStore.ts` around lines 1807 - 1811, getSettingsActions() is returning a map of action setters but omits setWakatimeDetailedTracking, causing callers to receive undefined; update the returned object in settingsStore (the getSettingsActions() implementation) to include setWakatimeDetailedTracking: state.setWakatimeDetailedTracking alongside setWakatimeApiKey and setWakatimeEnabled so external code can persist the detailed-tracking toggle.
🧹 Nitpick comments (1)
src/main/process-listeners/wakatime-listener.ts (1)
59-61: Prefer explicit boolean coercion inonDidChangecallback.The
enabledwatcher on line 54 already uses!!v, but thedetailedEnabledwatcher assignsval as booleandirectly. If the store ever emitsnullorundefined, the cast silently produces a falsy non-boolean. Using!!valis consistent with the adjacent pattern.♻️ Proposed fix
- settingsStore.onDidChange('wakatimeDetailedTracking', (val: unknown) => { - detailedEnabled = val as boolean; - }); + settingsStore.onDidChange('wakatimeDetailedTracking', (val: unknown) => { + detailedEnabled = !!val; + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/process-listeners/wakatime-listener.ts` around lines 59 - 61, The watcher for 'wakatimeDetailedTracking' currently assigns detailedEnabled = val as boolean which can silently accept null/undefined; change the callback in settingsStore.onDidChange('wakatimeDetailedTracking', ...) to coerce to a true boolean (e.g. detailedEnabled = !!val) to match the pattern used by the 'enabled' watcher and ensure consistent, explicit boolean values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/wakatime-manager.ts`:
- Around line 682-689: The call to execFileNoThrow in the file-heartbeat send
path ignores its result so failures still produce the "Sent file heartbeats"
log; update the heartbeat-sending code that calls execFileNoThrow (using
this.cliPath and args) to inspect the returned result (exitCode and stderr), and
only log logger.info('Sent file heartbeats', LOG_CONTEXT, { count: files.length
}) when exitCode === 0; otherwise emit logger.warn (include exitCode and stderr)
and avoid the success message so failures aren't reported as successful. Ensure
you reference execFileNoThrow's result variable and use LOG_CONTEXT and
files.length in the logs for consistent context.
In `@src/renderer/components/SettingsModal.tsx`:
- Around line 2205-2235: The new detailed file tracking toggle button
(controlled by wakatimeDetailedTracking and toggled via
setWakatimeDetailedTracking) is missing focus accessibility props; update that
button element to include tabIndex={0} (or tabIndex={-1} if you intend to skip
it), add the "outline-none" class to its className, and if it should auto-focus
on mount add a ref={(el) => el?.focus()} (or a useRef/useEffect pattern) so
keyboard users can reach and see focus; keep the existing role="switch" and
aria-checked as-is.
---
Outside diff comments:
In `@src/renderer/stores/settingsStore.ts`:
- Around line 1807-1811: getSettingsActions() is returning a map of action
setters but omits setWakatimeDetailedTracking, causing callers to receive
undefined; update the returned object in settingsStore (the getSettingsActions()
implementation) to include setWakatimeDetailedTracking:
state.setWakatimeDetailedTracking alongside setWakatimeApiKey and
setWakatimeEnabled so external code can persist the detailed-tracking toggle.
---
Nitpick comments:
In `@src/main/process-listeners/wakatime-listener.ts`:
- Around line 59-61: The watcher for 'wakatimeDetailedTracking' currently
assigns detailedEnabled = val as boolean which can silently accept
null/undefined; change the callback in
settingsStore.onDidChange('wakatimeDetailedTracking', ...) to coerce to a true
boolean (e.g. detailedEnabled = !!val) to match the pattern used by the
'enabled' watcher and ensure consistent, explicit boolean values.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
CLAUDE-IPC.mddocs/configuration.mddocs/features.mdsrc/__tests__/main/wakatime-manager.test.tssrc/main/process-listeners/__tests__/wakatime-listener.test.tssrc/main/process-listeners/wakatime-listener.tssrc/main/stores/defaults.tssrc/main/stores/types.tssrc/main/wakatime-manager.tssrc/renderer/components/SettingsModal.tsxsrc/renderer/hooks/settings/useSettings.tssrc/renderer/stores/settingsStore.ts
Greptile SummaryAdded detailed file tracking to WakaTime integration. When enabled via two explicit opt-ins (WakaTime + detailed tracking toggle), Maestro sends per-file heartbeats for write operations across all supported agents (Claude Code, Codex, OpenCode). File paths are accumulated during agent turns and flushed as batched heartbeats either immediately on Key changes:
Implementation quality:
Confidence Score: 5/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Agent as AI Agent
participant PM as ProcessManager
participant WL as WakaTimeListener
participant WM as WakaTimeManager
participant CLI as WakaTime CLI
Note over Agent,CLI: Interactive Session (Usage Event)
Agent->>PM: tool-execution (Write/Edit)
PM->>WL: emit('tool-execution', toolExecution)
WL->>WL: extractFilePathFromToolExecution()
WL->>WL: accumulate in pendingFiles Map
Agent->>PM: usage (end of turn)
PM->>WL: emit('usage', usageStats)
WL->>WL: reset 500ms debounce timer
Note over WL: After 500ms debounce
WL->>WL: flushPendingFiles()
WL->>WM: sendFileHeartbeats(filesArray)
WM->>WM: detectLanguageFromPath() for each file
WM->>WM: detectBranch() with TTL cache
WM->>CLI: wakatime-cli --entity file1 --extra-heartbeats
WM->>CLI: stdin: JSON array of extra files
Note over Agent,CLI: Batch/Auto-Run Session (Query Complete)
Agent->>PM: tool-execution (Write/Edit)
PM->>WL: emit('tool-execution', toolExecution)
WL->>WL: accumulate in pendingFiles Map
Agent->>PM: query-complete
PM->>WL: emit('query-complete', queryData)
WL->>WL: flushPendingFiles() immediately
WL->>WL: cancel any pending usage timer
WL->>WM: sendFileHeartbeats(filesArray)
WM->>CLI: wakatime-cli with batched heartbeats
Note over Agent,CLI: Session Exit
Agent->>PM: exit
PM->>WL: emit('exit', sessionId)
WL->>WL: clear timers and pendingFiles
WL->>WM: removeSession(sessionId)
Last reviewed commit: d58ba72 |
- Check execFileNoThrow exit code and log warn on failure - Add tabIndex and outline-none to detailed tracking toggle - Export setWakatimeDetailedTracking from getSettingsActions() - Use !!val for consistent boolean coercion in listener
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/main/process-listeners/wakatime-listener.ts (1)
57-84: Clear queued file heartbeats when detailed tracking is disabled.Line 57-61 flips the flag but leaves
pendingFiles/usageFlushTimersintact; if detailed tracking is disabled for a while, stale entries can linger and later flush when re-enabled. Consider clearing queues/timers on disable to keep the buffer accurate.♻️ Suggested cleanup on disable
settingsStore.onDidChange('wakatimeDetailedTracking', (val: unknown) => { detailedEnabled = !!val; + if (!detailedEnabled) { + clearFileTracking(); + } }); // Per-session accumulator for file paths from tool-execution events. // Outer key: sessionId, inner key: filePath (deduplicates, keeping latest timestamp). const pendingFiles = new Map<string, Map<string, { filePath: string; timestamp: number }>>(); // Per-session debounce timers for usage-based file flush. const usageFlushTimers = new Map<string, ReturnType<typeof setTimeout>>(); + +function clearFileTracking(): void { + pendingFiles.clear(); + for (const timer of usageFlushTimers.values()) { + clearTimeout(timer); + } + usageFlushTimers.clear(); +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/process-listeners/wakatime-listener.ts` around lines 57 - 84, When the wakatimeDetailedTracking flag is toggled off the handler that updates detailedEnabled (the settingsStore.onDidChange callback) must also clear per-session state to avoid stale flushes: on receiving a falsy val, iterate and clear pendingFiles (Map pendingFiles) and cancel/clear all timers in usageFlushTimers (Map usageFlushTimers) and then delete their entries; ensure you don't call flushPendingFiles when disabling (only cancel), and keep existing behavior when enabling. Update the settingsStore.onDidChange callback to perform these cleanup steps atomically so stale entries won't later be flushed by flushPendingFiles or leftover timers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/main/process-listeners/wakatime-listener.ts`:
- Around line 57-84: When the wakatimeDetailedTracking flag is toggled off the
handler that updates detailedEnabled (the settingsStore.onDidChange callback)
must also clear per-session state to avoid stale flushes: on receiving a falsy
val, iterate and clear pendingFiles (Map pendingFiles) and cancel/clear all
timers in usageFlushTimers (Map usageFlushTimers) and then delete their entries;
ensure you don't call flushPendingFiles when disabling (only cancel), and keep
existing behavior when enabling. Update the settingsStore.onDidChange callback
to perform these cleanup steps atomically so stale entries won't later be
flushed by flushPendingFiles or leftover timers.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/main/process-listeners/wakatime-listener.tssrc/main/wakatime-manager.tssrc/renderer/components/SettingsModal.tsxsrc/renderer/stores/settingsStore.ts
Summary
Adds detailed file tracking to the WakaTime integration. When enabled, Maestro sends per-file heartbeats for every write operation across all supported agents — so your WakaTime dashboard shows exactly which files were modified, not just "time spent in Maestro."
When heartbeats are sent
High-level behavior
Maestro sends two kinds of WakaTime heartbeats:
App-level heartbeats — "the user is active in Maestro." These fire continuously while an agent is working (streaming output, thinking, completing a query). WakaTime sees this as time spent in the Maestro project. This has been shipping since the initial WakaTime integration.
File-level heartbeats (new, opt-in) — "the agent just wrote to
src/index.ts." These fire when an agent finishes a turn that included write operations. Your WakaTime dashboard breaks down time by individual file and language, just like it does for VS Code or JetBrains.File heartbeats require two explicit opt-ins: WakaTime enabled + "Detailed file tracking" toggled on. Only file paths are sent to WakaTime, never file content.
When exactly each heartbeat fires
dataevent)thinking-chunkevent)query-completeevent)usageevent)exitevent)How file paths are collected
During an agent turn, every
tool-executionevent is checked:When the turn ends (via
query-completefor batch, or debouncedusagefor interactive), the accumulated files are flushed as a single batched WakaTime CLI call. The first file goes as the primary--entity; additional files go via--extra-heartbeatson stdin.Double-flush prevention
For batch sessions, both
query-completeandusagecan fire.query-completeflushes first and cancels any pending usage debounce timer, so files are never sent twice.Changes
Core logic:
wakatime-manager.ts—sendFileHeartbeats(),WRITE_TOOL_NAMES,extractFilePathFromToolExecution(),detectLanguageFromPath()with 50+ extension→language mapwakatime-listener.ts—tool-executioncollection,usage-based debounced flush for interactive sessions,query-completeflush for batch, sharedflushPendingFiles()helper, timer cleanup on exitSettings:
wakatimeDetailedTrackingfield in store types, defaults, settings store, anduseSettingshookSettingsModal.tsx(shown only when WakaTime is enabled)Docs:
configuration.md— WakaTime section with setup instructions and tracked tools tablefeatures.md— feature bulletCLAUDE-IPC.md— integrations sectionTests:
wakatime-manager.test.tsandwakatime-listener.test.tsSupported write tools by agent
Test plan
npm run test— all WakaTime tests pass (72 manager + 44 listener)npm run lint— no type errorsSummary by CodeRabbit
New Features
Documentation
Tests