feat: add per-file WakaTime heartbeats for write operations#449
feat: add per-file WakaTime heartbeats for write operations#449pedramamini merged 12 commits intoRunMaestro:mainfrom
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds optional per-file WakaTime tracking: collects file paths from tool-execution events (deduplicated per-session), 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, source)
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 |
08e8da6 to
5b10a1c
Compare
|
Branch detection fix (
|
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 |
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
6b3c0d8 to
8932301
Compare
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/main/wakatime-manager.ts (1)
702-707:⚠️ Potential issue | 🟡 Minor
removeSessionleaksfile:${projectCwd}branch-cache entries.
sendFileHeartbeatscaches branches underfile:${projectCwd}(Line 632), butremoveSessiononly deletes bysessionId. Those entries persist until the 5-minute TTL elapses rather than being purged on session exit. For long-running applications with many distinct project directories, these entries accumulate.🔧 Proposed fix — add a reverse lookup or accept a `projectCwd` param
The simplest targeted fix is to also accept and purge the file-scoped key:
- removeSession(sessionId: string): void { + removeSession(sessionId: string, projectCwd?: string): void { this.lastHeartbeatPerSession.delete(sessionId); this.branchCache.delete(sessionId); + if (projectCwd) { + this.branchCache.delete(`file:${projectCwd}`); + } this.languageCache.delete(sessionId); }Then in
wakatime-listener.ts, pass the project dir:- wakaTimeManager.removeSession(sessionId); + const proc = processManager.get(sessionId); + wakaTimeManager.removeSession(sessionId, proc?.projectPath || proc?.cwd);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/wakatime-manager.ts` around lines 702 - 707, removeSession currently deletes only lastHeartbeatPerSession, branchCache and languageCache entries keyed by sessionId, but file-scoped branchCache entries stored by sendFileHeartbeats under keys like "file:${projectCwd}" are not removed; update removeSession(sessionId: string) to also purge file-scoped cache entries by either (A) accepting a second parameter projectCwd: string and deleting branchCache.delete(`file:${projectCwd}`) (and update callers such as wakatime-listener to pass projectCwd), or (B) implement a reverse lookup to find and delete any branchCache keys that reference the given sessionId (e.g., scanning branchCache keys with the "file:" prefix and removing ones tied to this sessionId), ensuring branchCache entries created by sendFileHeartbeats are removed immediately on session end.
🧹 Nitpick comments (2)
src/main/process-listeners/wakatime-listener.ts (1)
139-163: DoublependingFileslookup in theusagehandler.
pendingFiles.has(sessionId)followed immediately bypendingFiles.get(sessionId)!.sizeperforms two map lookups. Use a singlegetcall (consistent withflushPendingFiles).♻️ Proposed refactor
- if (!pendingFiles.has(sessionId) || pendingFiles.get(sessionId)!.size === 0) return; + if (!pendingFiles.get(sessionId)?.size) return;🤖 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 139 - 163, Replace the double lookup of pendingFiles in the processManager.on('usage', ...) handler by calling pendingFiles.get(sessionId) once and storing the result (e.g., const files = pendingFiles.get(sessionId)); then check files for falsy or files.size === 0 before proceeding, keeping the rest of the debounce logic (usageFlushTimers, clearTimeout, setTimeout, managedProcess lookup, and flushPendingFiles call) unchanged; reference pendingFiles, usageFlushTimers, flushPendingFiles, and USAGE_FLUSH_DELAY_MS when making the change.src/__tests__/main/wakatime-manager.test.ts (1)
907-1261: Missing test forsendFileHeartbeatswarning log on CLI failure.
sendFileHeartbeatsnow correctly logs a warning whenexitCode !== 0(Lines 692-694 in the implementation), but there is no test verifying that path. The analogous test exists forsendHeartbeatat Line 485. Consider adding:it('should log warning when file heartbeats CLI call fails', async () => { vi.mocked(execFileNoThrow).mockResolvedValueOnce({ exitCode: 1, stdout: '', stderr: 'API key invalid', }); await manager.sendFileHeartbeats( [{ filePath: '/project/src/index.ts', timestamp: 1708700000000 }], 'My Project' ); expect(logger.warn).toHaveBeenCalledWith( expect.stringContaining('File heartbeats failed'), '[WakaTime]', expect.objectContaining({ count: 1 }) ); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/main/wakatime-manager.test.ts` around lines 907 - 1261, Add a test for the error path in the sendFileHeartbeats suite: mock execFileNoThrow to resolve with exitCode: 1 and stderr (e.g., 'API key invalid'), call manager.sendFileHeartbeats with a single file, then assert logger.warn was called with a message containing "File heartbeats failed", the tag "[WakaTime]", and an object containing { count: 1 }; reference the sendFileHeartbeats test block, vi.mocked(execFileNoThrow), and logger.warn to locate where to add this test.
🤖 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/process-listeners/wakatime-listener.ts`:
- Around line 76-79: The code currently uses path.resolve(projectDir || '',
f.filePath) which, when projectDir is undefined, resolves relative paths against
process.cwd() (app install dir); change the logic in wakatime-listener.ts where
the heartbeat/file mapping is built (the object using filePath: ... and
timestamp: f.timestamp) to skip resolving relative paths if projectDir is falsy:
if path.isAbsolute(f.filePath) keep f.filePath, else if projectDir is provided
resolve against projectDir, otherwise keep the original relative f.filePath (do
not call path.resolve with ''), so heartbeats don't get converted to incorrect
absolute paths.
In `@src/main/wakatime-manager.ts`:
- Line 629: The sendFileHeartbeats method currently returns silently when the
WakaTime CLI is unavailable; update sendFileHeartbeats to mirror sendHeartbeat
by checking await this.ensureCliInstalled() and logging a warning using the same
message ('WakaTime CLI not available — skipping heartbeat') before returning so
missing CLI cases are visible; locate the call in sendFileHeartbeats and add the
processLogger.warn (or the same logger used in sendHeartbeat) with that exact
message prior to the early return.
---
Outside diff comments:
In `@src/main/wakatime-manager.ts`:
- Around line 702-707: removeSession currently deletes only
lastHeartbeatPerSession, branchCache and languageCache entries keyed by
sessionId, but file-scoped branchCache entries stored by sendFileHeartbeats
under keys like "file:${projectCwd}" are not removed; update
removeSession(sessionId: string) to also purge file-scoped cache entries by
either (A) accepting a second parameter projectCwd: string and deleting
branchCache.delete(`file:${projectCwd}`) (and update callers such as
wakatime-listener to pass projectCwd), or (B) implement a reverse lookup to find
and delete any branchCache keys that reference the given sessionId (e.g.,
scanning branchCache keys with the "file:" prefix and removing ones tied to this
sessionId), ensuring branchCache entries created by sendFileHeartbeats are
removed immediately on session end.
---
Nitpick comments:
In `@src/__tests__/main/wakatime-manager.test.ts`:
- Around line 907-1261: Add a test for the error path in the sendFileHeartbeats
suite: mock execFileNoThrow to resolve with exitCode: 1 and stderr (e.g., 'API
key invalid'), call manager.sendFileHeartbeats with a single file, then assert
logger.warn was called with a message containing "File heartbeats failed", the
tag "[WakaTime]", and an object containing { count: 1 }; reference the
sendFileHeartbeats test block, vi.mocked(execFileNoThrow), and logger.warn to
locate where to add this test.
In `@src/main/process-listeners/wakatime-listener.ts`:
- Around line 139-163: Replace the double lookup of pendingFiles in the
processManager.on('usage', ...) handler by calling pendingFiles.get(sessionId)
once and storing the result (e.g., const files = pendingFiles.get(sessionId));
then check files for falsy or files.size === 0 before proceeding, keeping the
rest of the debounce logic (usageFlushTimers, clearTimeout, setTimeout,
managedProcess lookup, and flushPendingFiles call) unchanged; reference
pendingFiles, usageFlushTimers, flushPendingFiles, and USAGE_FLUSH_DELAY_MS when
making the change.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (5)
docs/configuration.mdsrc/__tests__/main/wakatime-manager.test.tssrc/main/process-listeners/__tests__/wakatime-listener.test.tssrc/main/process-listeners/wakatime-listener.tssrc/main/wakatime-manager.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- docs/configuration.md
src/main/wakatime-manager.ts
Outdated
| } | ||
| if (!apiKey) return; | ||
|
|
||
| if (!(await this.ensureCliInstalled())) return; |
There was a problem hiding this comment.
Missing warning log when CLI is unavailable in sendFileHeartbeats.
Unlike sendHeartbeat (which logs 'WakaTime CLI not available — skipping heartbeat'), sendFileHeartbeats silently returns on CLI unavailability, making silent drops harder to diagnose.
🔧 Proposed fix
- if (!(await this.ensureCliInstalled())) return;
+ if (!(await this.ensureCliInstalled())) {
+ logger.warn('WakaTime CLI not available — skipping file heartbeats', LOG_CONTEXT);
+ return;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!(await this.ensureCliInstalled())) return; | |
| if (!(await this.ensureCliInstalled())) { | |
| logger.warn('WakaTime CLI not available — skipping file heartbeats', LOG_CONTEXT); | |
| return; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/wakatime-manager.ts` at line 629, The sendFileHeartbeats method
currently returns silently when the WakaTime CLI is unavailable; update
sendFileHeartbeats to mirror sendHeartbeat by checking await
this.ensureCliInstalled() and logging a warning using the same message
('WakaTime CLI not available — skipping heartbeat') before returning so missing
CLI cases are visible; locate the call in sendFileHeartbeats and add the
processLogger.warn (or the same logger used in sendHeartbeat) with that exact
message prior to the early return.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/main/wakatime-manager.ts (1)
629-629:⚠️ Potential issue | 🟡 MinorLog when file heartbeats are skipped due to missing CLI.
Line 629 returns silently on CLI unavailability, which makes dropped file heartbeats hard to diagnose.
Suggested patch
- if (!(await this.ensureCliInstalled())) return; + if (!(await this.ensureCliInstalled())) { + logger.warn('WakaTime CLI not available — skipping file heartbeats', LOG_CONTEXT); + return; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/wakatime-manager.ts` at line 629, Add a log message before the early return when the CLI is not available so skipped file heartbeats are visible; specifically, where you currently have "if (!(await this.ensureCliInstalled())) return;" call a logger (e.g., this.logger.warn or this.log.warn) with a clear message like "Skipping file heartbeat: Wakatime CLI not installed" and include any context (file path or heartbeat id) available in the surrounding scope, then return; keep the check using ensureCliInstalled() unchanged but do not return silently.src/main/process-listeners/wakatime-listener.ts (1)
75-79:⚠️ Potential issue | 🟠 MajorRelative paths can be resolved against the wrong base directory.
Line 78 uses
path.resolve(projectDir || '', f.filePath). WhenprojectDiris missing, this resolves relative paths fromprocess.cwd()(Electron app cwd), which can produce incorrect entities.#!/bin/bash # Verify the problematic resolution fallback and call path that can pass undefined projectDir. rg -n "path\.resolve\(projectDir \|\| '', f\.filePath\)|flushPendingFiles\(queryData\.sessionId, queryData\.projectPath" --type tsSuggested patch
- const filesArray = Array.from(sessionFiles.values()).map((f) => ({ - filePath: path.isAbsolute(f.filePath) - ? f.filePath - : path.resolve(projectDir || '', f.filePath), - timestamp: f.timestamp, - })); + const filesArray = Array.from(sessionFiles.values()) + .map((f) => ({ + filePath: path.isAbsolute(f.filePath) + ? f.filePath + : projectDir + ? path.resolve(projectDir, f.filePath) + : null, + timestamp: f.timestamp, + })) + .filter((f): f is { filePath: string; timestamp: number } => f.filePath !== null);🤖 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 75 - 79, The mapping that builds filesArray resolves relative paths using path.resolve(projectDir || '', f.filePath) which falls back to process.cwd() when projectDir is undefined; update the logic in the filesArray construction (the Array.from(sessionFiles.values()).map callback referencing f.filePath, projectDir, and path.resolve) so that you only call path.resolve with projectDir when projectDir is truthy (e.g., if projectDir use path.resolve(projectDir, f.filePath), otherwise preserve the original f.filePath or normalize it) and keep the existing path.isAbsolute check to avoid unintentionally resolving against the Electron app cwd.
🤖 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/process-listeners/wakatime-listener.ts`:
- Around line 124-126: When detailedEnabled is false we currently skip
flushing/clearing pending file paths which lets old pending files leak across
turns; update the control flow so that whenever detailedEnabled is false
(including the branch that early-returns later) you call
flushPendingFiles(queryData.sessionId, queryData.projectPath, projectName,
queryData.source) or invoke a new clearPendingFiles helper to both flush and
clear queued paths before returning. Ensure this change is applied around the
detailedEnabled checks in the Wakatime listener function (the block using
detailedEnabled and the later early-return path) so pending files are never left
queued when detailed tracking is disabled.
---
Duplicate comments:
In `@src/main/process-listeners/wakatime-listener.ts`:
- Around line 75-79: The mapping that builds filesArray resolves relative paths
using path.resolve(projectDir || '', f.filePath) which falls back to
process.cwd() when projectDir is undefined; update the logic in the filesArray
construction (the Array.from(sessionFiles.values()).map callback referencing
f.filePath, projectDir, and path.resolve) so that you only call path.resolve
with projectDir when projectDir is truthy (e.g., if projectDir use
path.resolve(projectDir, f.filePath), otherwise preserve the original f.filePath
or normalize it) and keep the existing path.isAbsolute check to avoid
unintentionally resolving against the Electron app cwd.
In `@src/main/wakatime-manager.ts`:
- Line 629: Add a log message before the early return when the CLI is not
available so skipped file heartbeats are visible; specifically, where you
currently have "if (!(await this.ensureCliInstalled())) return;" call a logger
(e.g., this.logger.warn or this.log.warn) with a clear message like "Skipping
file heartbeat: Wakatime CLI not installed" and include any context (file path
or heartbeat id) available in the surrounding scope, then return; keep the check
using ensureCliInstalled() unchanged but do not return silently.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (5)
docs/configuration.mdsrc/__tests__/main/wakatime-manager.test.tssrc/main/process-listeners/__tests__/wakatime-listener.test.tssrc/main/process-listeners/wakatime-listener.tssrc/main/wakatime-manager.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- docs/configuration.md
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/main/process-listeners/wakatime-listener.ts (1)
59-61:⚠️ Potential issue | 🟠 MajorCancel pending usage flushes when detailed tracking is toggled off.
At Line 59, the setting change only updates
detailedEnabled. If a timer was already scheduled (Line 157), the callback can still flush file heartbeats after tracking is disabled. Clear pending state immediately on toggle-off and re-checkdetailedEnabledinside the timer callback before flushing.🔧 Proposed patch
- // Cache detailed tracking state for file-level heartbeats - let detailedEnabled = settingsStore.get('wakatimeDetailedTracking', false) as boolean; - settingsStore.onDidChange('wakatimeDetailedTracking', (val: unknown) => { - detailedEnabled = !!val; - }); - // 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>>(); + + // Cache detailed tracking state for file-level heartbeats + let detailedEnabled = settingsStore.get('wakatimeDetailedTracking', false) as boolean; + settingsStore.onDidChange('wakatimeDetailedTracking', (val: unknown) => { + detailedEnabled = !!val; + if (!detailedEnabled) { + for (const timer of usageFlushTimers.values()) { + clearTimeout(timer); + } + usageFlushTimers.clear(); + pendingFiles.clear(); + } + }); @@ setTimeout(() => { usageFlushTimers.delete(sessionId); + if (!detailedEnabled) { + pendingFiles.delete(sessionId); + return; + } const managedProcess = processManager.get(sessionId); if (!managedProcess || managedProcess.isTerminal) return;Also applies to: 157-169
🤖 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, When wakatimeDetailedTracking changes, the code only flips detailedEnabled but does not cancel any already-scheduled flush, so a pending timer can still call the flush routine after tracking is disabled; update the settingsStore.onDidChange handler for 'wakatimeDetailedTracking' to clear and nullify the scheduled timer (the variable holding the pending timeout) and clear any pending heartbeat/usage queue when toggled off, and additionally guard the timer callback (the function that runs at lines ~157-169 which calls the flush routine) with a runtime check of detailedEnabled before performing any flush/heartbeat send (or early return) to ensure no flush runs after disabling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/main/process-listeners/wakatime-listener.ts`:
- Around line 59-61: When wakatimeDetailedTracking changes, the code only flips
detailedEnabled but does not cancel any already-scheduled flush, so a pending
timer can still call the flush routine after tracking is disabled; update the
settingsStore.onDidChange handler for 'wakatimeDetailedTracking' to clear and
nullify the scheduled timer (the variable holding the pending timeout) and clear
any pending heartbeat/usage queue when toggled off, and additionally guard the
timer callback (the function that runs at lines ~157-169 which calls the flush
routine) with a runtime check of detailedEnabled before performing any
flush/heartbeat send (or early return) to ensure no flush runs after disabling.
…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.
…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.
- 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
Thread querySource/source through WakaTime heartbeats so the category reflects how the session was initiated: interactive (user) sessions send 'building', auto-run/batch sessions send 'ai coding'. https://claude.ai/code/session_01FR9j7wLSUgaS4WvoC7JM2X
- Skip relative file paths when projectDir is undefined instead of resolving against process.cwd() (app install dir) - Add warning log when CLI is unavailable in sendFileHeartbeats - Clear pending files when detailed tracking is toggled off to prevent stale paths leaking across turns
42c99e7 to
778cc8b
Compare
Summary
This PR upgrades Maestro's WakaTime integration from app-only activity tracking to optional per-file write tracking, and adds first-class CLI setup/validation in Settings.
Current behavior after all commits in this PR:
What Actually Happens
1) App-level heartbeats (existing behavior, refined)
Heartbeats are sent from listener events:
data(stdout chunks)thinking-chunkquery-completeWakaTimeManagerdebounces by session to 1 heartbeat every 2 minutes.2) File-level heartbeats (new, opt-in)
File heartbeats are only sent when both are true:
wakatimeEnabled = truewakatimeDetailedTracking = trueFlow:
tool-executioncaptures write operations and accumulates file paths per session.usageevent.query-complete.query-completeandusagehappen, the pendingusageflush is canceled to avoid duplicates.Only file paths/metadata are sent (no file content).
3) Metadata included in heartbeats
buildingfor user-driven sessions,ai codingfor auto-run/batch.tsconfig.json,Cargo.toml, etc.).Supported Write Tools
Write,Edit,NotebookEditwrite_to_file,str_replace_based_edit_tool,create_filewrite,patchRead tools and shell commands are ignored.
Settings / IPC / UX Updates
wakatimeDetailedTrackingsetting (types, defaults, persistence, hook wiring).wakatime:checkCliwakatime:validateApiKeyWakaTime CLI lifecycle changes
WakaTimeManagernow:wakatime-cli/wakatime) and local fallback (~/.wakatime/...)Documentation Updated
docs/configuration.md(WakaTime section)docs/features.mdCLAUDE-IPC.mdTest Coverage
Added/updated tests for manager, listener, and IPC handlers.
Validated in this branch with:
npm run test -- src/__tests__/main/wakatime-manager.test.ts src/main/process-listeners/__tests__/wakatime-listener.test.ts src/__tests__/main/ipc/handlers/wakatime.test.tsSummary by CodeRabbit
New Features
Documentation
Tests
Chores