Skip to content

feat: add per-file WakaTime heartbeats for write operations#449

Merged
pedramamini merged 12 commits intoRunMaestro:mainfrom
kianhub:feat/wakatime-file-heartbeats
Feb 25, 2026
Merged

feat: add per-file WakaTime heartbeats for write operations#449
pedramamini merged 12 commits intoRunMaestro:mainfrom
kianhub:feat/wakatime-file-heartbeats

Conversation

@kianhub
Copy link
Contributor

@kianhub kianhub commented Feb 24, 2026

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:

  • Maestro still sends app-level heartbeats while agents are active.
  • With Detailed file tracking enabled, Maestro also sends file-level heartbeats for write/edit tools.
  • WakaTime CLI setup is now handled in-app (auto-detect, auto-install fallback, API key validation).

What Actually Happens

1) App-level heartbeats (existing behavior, refined)

Heartbeats are sent from listener events:

  • data (stdout chunks)
  • thinking-chunk
  • query-complete

WakaTimeManager debounces 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 = true
  • wakatimeDetailedTracking = true

Flow:

  • tool-execution captures write operations and accumulates file paths per session.
  • On interactive turns, files flush after a 500ms debounced usage event.
  • On batch/auto-run, files flush on query-complete.
  • If both query-complete and usage happen, the pending usage flush is canceled to avoid duplicates.

Only file paths/metadata are sent (no file content).

3) Metadata included in heartbeats

  • Category: building for user-driven sessions, ai coding for auto-run/batch.
  • Branch: git branch attached when detectable.
  • Language:
    • App heartbeat language inferred from project markers (tsconfig.json, Cargo.toml, etc.).
    • File heartbeat language inferred from file extension map.

Supported Write Tools

Agent Tracked tools
Claude Code Write, Edit, NotebookEdit
Codex write_to_file, str_replace_based_edit_tool, create_file
OpenCode write, patch

Read tools and shell commands are ignored.

Settings / IPC / UX Updates

  • Added wakatimeDetailedTracking setting (types, defaults, persistence, hook wiring).
  • Settings modal now includes:
    • WakaTime enable toggle
    • Detailed file tracking toggle (visible only when WakaTime is enabled)
    • CLI availability check and version display
    • API key validation via IPC
  • Added WakaTime IPC namespace support for:
    • wakatime:checkCli
    • wakatime:validateApiKey

WakaTime CLI lifecycle changes

WakaTimeManager now:

  • Detects CLI on PATH (wakatime-cli / wakatime) and local fallback (~/.wakatime/...)
  • Auto-installs CLI from WakaTime GitHub releases if missing
  • Guards concurrent install attempts
  • Runs background update checks (max once/day)

Documentation Updated

  • docs/configuration.md (WakaTime section)
  • docs/features.md
  • CLAUDE-IPC.md

Test 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.ts
  • Result: 127 tests passed across 3 files.

Summary by CodeRabbit

  • New Features

    • Detailed WakaTime integration: optional per-file write activity tracking, batched per-session file heartbeats, language/branch-aware heartbeats, and a Settings toggle to enable it. Heartbeat source now distinguishes user vs auto.
  • Documentation

    • Added WakaTime integration docs and updated configuration/features pages (note: duplicate section inserted).
  • Tests

    • Expanded tests for WakaTime flows, language detection, file extraction, and heartbeat flushing.
  • Chores

    • Settings persist and expose the new detailed-tracking option.

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Documentation
CLAUDE-IPC.md, docs/configuration.md, docs/features.md
Added WakaTime integration docs and configuration entries (toggle, API key, detailed tracking). docs/configuration.md includes a duplicated WakaTime documentation block.
WakaTime Manager Core Logic
src/main/wakatime-manager.ts
Added extension->language map, detectLanguageFromPath(), WRITE_TOOL_NAMES, extractFilePathFromToolExecution(), branch cache entries with TTL, extended sendHeartbeat signature (adds source), and new sendFileHeartbeats() to batch per-file heartbeats with language/branch metadata via WakaTime CLI.
Process Listener Integration
src/main/process-listeners/wakatime-listener.ts
Collects per-session file paths from tool-execution, deduplicates by latest timestamp, debounced flush on usage, flush on query-complete, cleanup on exit, and integrates file-path extraction; adds usage flush timers and flushPendingFiles helper.
Settings Backend Defaults & Types
src/main/stores/defaults.ts, src/main/stores/types.ts
Added wakatimeDetailedTracking: boolean to MaestroSettings and defaulted it to false.
Renderer Settings Layer
src/renderer/stores/settingsStore.ts, src/renderer/hooks/settings/useSettings.ts, src/renderer/components/SettingsModal.tsx
Exposed wakatimeDetailedTracking state and setWakatimeDetailedTracking setter, persisted/loaded via settings store, and added UI toggle in SettingsModal shown when WakaTime is enabled.
Tests
src/__tests__/main/wakatime-manager.test.ts, src/main/process-listeners/__tests__/wakatime-listener.test.ts
Added extensive tests for new exports (detectLanguageFromPath, extractFilePathFromToolExecution, WRITE_TOOL_NAMES, sendFileHeartbeats) and listener behaviors: accumulation, deduplication, debounce flush, query-complete flush, exit cleanup, settings gating, and API key resolution.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add per-file WakaTime heartbeats for write operations' clearly and concisely describes the main addition across the changeset—introducing per-file heartbeat tracking for write operations in the WakaTime integration.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@kianhub kianhub force-pushed the feat/wakatime-file-heartbeats branch from 08e8da6 to 5b10a1c Compare February 24, 2026 17:55
@kianhub
Copy link
Contributor Author

kianhub commented Feb 24, 2026

Branch detection fix (detectBranch in wakatime-manager.ts)

  • Failed git lookups are no longer cached. Previously an empty string was stored permanently, meaning a single transient git rev-parse failure (lock contention, timing during startup) would suppress the branch field for the entire session lifetime. Now failures are evicted from the cache so the next heartbeat retries.
  • Successful branch results now expire after 5 minutes, so mid-session branch switches are picked up instead of reporting the stale initial branch forever.
  • File-level heartbeats now cache branch per project directory (file:<cwd>) instead of a single shared 'file-heartbeat' key, preventing concurrent sessions in different repos from cross-contaminating each other's branch value.

@kianhub kianhub marked this pull request as ready for review February 24, 2026 22:11
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

setWakatimeDetailedTracking is missing from getSettingsActions().

setWakatimeEnabled and setWakatimeApiKey are both present in the returned map, but setWakatimeDetailedTracking is not. Any non-React code (main-process utilities, services) that calls getSettingsActions() will find the action undefined and 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 in onDidChange callback.

The enabled watcher on line 54 already uses !!v, but the detailedEnabled watcher assigns val as boolean directly. If the store ever emits null or undefined, the cast silently produces a falsy non-boolean. Using !!val is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 183a792 and d58ba72.

📒 Files selected for processing (12)
  • CLAUDE-IPC.md
  • docs/configuration.md
  • docs/features.md
  • src/__tests__/main/wakatime-manager.test.ts
  • src/main/process-listeners/__tests__/wakatime-listener.test.ts
  • src/main/process-listeners/wakatime-listener.ts
  • src/main/stores/defaults.ts
  • src/main/stores/types.ts
  • src/main/wakatime-manager.ts
  • src/renderer/components/SettingsModal.tsx
  • src/renderer/hooks/settings/useSettings.ts
  • src/renderer/stores/settingsStore.ts

@greptile-apps
Copy link

greptile-apps bot commented Feb 24, 2026

Greptile Summary

Added 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 query-complete (batch sessions) or after a 500ms debounced usage event (interactive sessions). Double-flush prevention ensures files aren't sent twice when both events fire.

Key changes:

  • wakatime-manager.ts — Added sendFileHeartbeats() with 50+ language mappings, extractFilePathFromToolExecution() for 8 write tool types, detectLanguageFromPath(), and improved branch caching with 5-minute TTL that doesn't cache failures
  • wakatime-listener.ts — Tool-execution event collection, per-session file accumulators, debounced flush on usage, immediate flush on query-complete with timer cancellation
  • Settings layer — New wakatimeDetailedTracking field with defaults, UI toggle (shown only when WakaTime enabled), store integration
  • Tests — ~1,100 lines of new coverage across manager (500 lines) and listener (570 lines) test files
  • Docs — Comprehensive setup instructions, privacy clarifications, supported tools table

Implementation quality:

  • Proper separation of concerns between collection (listener) and transmission (manager)
  • Efficient batching using WakaTime's --extra-heartbeats stdin API
  • Race condition handling for batch vs interactive flush
  • Memory-safe cleanup of timers and pending data on exit
  • Follows existing codebase patterns (tabs for indentation, settings flow, testing standards)

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • Well-architected feature with comprehensive test coverage (~1,100 new test lines), proper privacy controls (double opt-in, paths-only transmission), efficient batching, race condition handling, and clean integration with existing patterns. No logic errors, security issues, or architectural concerns found.
  • No files require special attention

Important Files Changed

Filename Overview
src/main/wakatime-manager.ts Added sendFileHeartbeats method, file extension language mapping, write tool detection, and improved branch cache with TTL. Well-structured with comprehensive test coverage.
src/main/process-listeners/wakatime-listener.ts Added file path accumulation on tool-execution events, flush logic for batch (query-complete) and interactive (debounced usage) sessions, proper cleanup of timers and pending data.
src/renderer/components/SettingsModal.tsx Added detailed tracking toggle UI (conditionally shown when WakaTime enabled), consistent with existing settings patterns.
src/tests/main/wakatime-manager.test.ts Added ~500 lines of tests covering WRITE_TOOL_NAMES, extractFilePathFromToolExecution, detectLanguageFromPath, and sendFileHeartbeats with extensive edge case coverage.
src/main/process-listeners/tests/wakatime-listener.test.ts Added ~570 lines of tests covering tool-execution collection, usage-based debounced flush, query-complete flush, double-flush prevention, and cleanup logic.
docs/configuration.md Added comprehensive WakaTime integration section documenting setup, what gets tracked, detailed file tracking, and supported tools per agent.

Sequence Diagram

sequenceDiagram
    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)
Loading

Last reviewed commit: d58ba72

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 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 / usageFlushTimers intact; 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

📥 Commits

Reviewing files that changed from the base of the PR and between d58ba72 and b13191a.

📒 Files selected for processing (4)
  • src/main/process-listeners/wakatime-listener.ts
  • src/main/wakatime-manager.ts
  • src/renderer/components/SettingsModal.tsx
  • src/renderer/stores/settingsStore.ts

@kianhub kianhub force-pushed the feat/wakatime-file-heartbeats branch from 6b3c0d8 to 8932301 Compare February 25, 2026 18:05
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

removeSession leaks file:${projectCwd} branch-cache entries.

sendFileHeartbeats caches branches under file:${projectCwd} (Line 632), but removeSession only deletes by sessionId. 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: Double pendingFiles lookup in the usage handler.

pendingFiles.has(sessionId) followed immediately by pendingFiles.get(sessionId)!.size performs two map lookups. Use a single get call (consistent with flushPendingFiles).

♻️ 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 for sendFileHeartbeats warning log on CLI failure.

sendFileHeartbeats now correctly logs a warning when exitCode !== 0 (Lines 692-694 in the implementation), but there is no test verifying that path. The analogous test exists for sendHeartbeat at 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

📥 Commits

Reviewing files that changed from the base of the PR and between b13191a and 6b3c0d8.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • docs/configuration.md
  • src/__tests__/main/wakatime-manager.test.ts
  • src/main/process-listeners/__tests__/wakatime-listener.test.ts
  • src/main/process-listeners/wakatime-listener.ts
  • src/main/wakatime-manager.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/configuration.md

}
if (!apiKey) return;

if (!(await this.ensureCliInstalled())) return;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/main/wakatime-manager.ts (1)

629-629: ⚠️ Potential issue | 🟡 Minor

Log 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 | 🟠 Major

Relative paths can be resolved against the wrong base directory.

Line 78 uses path.resolve(projectDir || '', f.filePath). When projectDir is missing, this resolves relative paths from process.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 ts
Suggested 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6b3c0d8 and 8932301.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • docs/configuration.md
  • src/__tests__/main/wakatime-manager.test.ts
  • src/main/process-listeners/__tests__/wakatime-listener.test.ts
  • src/main/process-listeners/wakatime-listener.ts
  • src/main/wakatime-manager.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/configuration.md

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/main/process-listeners/wakatime-listener.ts (1)

59-61: ⚠️ Potential issue | 🟠 Major

Cancel 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-check detailedEnabled inside 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.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8932301 and 142d37f.

📒 Files selected for processing (2)
  • src/main/process-listeners/wakatime-listener.ts
  • src/main/wakatime-manager.ts

…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
@kianhub kianhub force-pushed the feat/wakatime-file-heartbeats branch from 42c99e7 to 778cc8b Compare February 25, 2026 21:41
@pedramamini pedramamini merged commit c621a66 into RunMaestro:main Feb 25, 2026
1 of 3 checks passed
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