# Idle Detection Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Auto-pause the active TaskTracker task when the user is idle, and auto-resume it when they return. **Architecture:** Add `GetLastInputInfo` P/Invoke to `NativeMethods.cs`, check idle time on every existing poll cycle in `Worker.cs`, and call the TaskTracker pause/resume API on state transitions. No new threads, timers, or services needed. **Tech Stack:** .NET 10, Win32 `user32.dll` P/Invoke, existing `IHttpClientFactory` --- ### Task 1: Add GetLastInputInfo to NativeMethods **Files:** - Modify: `WindowWatcher/NativeMethods.cs` **Step 1: Add the LASTINPUTINFO struct and GetLastInputInfo import** Add the following to `NativeMethods.cs`: ```csharp [StructLayout(LayoutKind.Sequential)] internal struct LASTINPUTINFO { public uint cbSize; public uint dwTime; } [DllImport("user32.dll")] internal static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); ``` Note: `GetLastInputInfo` must use `DllImport`, not `LibraryImport`, because it takes a `ref` struct with `cbSize` that needs manual marshalling. **Step 2: Add a helper method for getting idle time** ```csharp internal static TimeSpan GetIdleTime() { var info = new LASTINPUTINFO { cbSize = (uint)Marshal.SizeOf() }; if (!GetLastInputInfo(ref info)) return TimeSpan.Zero; return TimeSpan.FromMilliseconds(Environment.TickCount64 - info.dwTime); } ``` **Step 3: Commit** ```bash git add WindowWatcher/NativeMethods.cs git commit -m "feat(watcher): add GetLastInputInfo P/Invoke for idle detection" ``` --- ### Task 2: Add IdleTimeoutMs to configuration **Files:** - Modify: `WindowWatcher/WindowWatcherOptions.cs` - Modify: `WindowWatcher/appsettings.json` **Step 1: Add IdleTimeoutMs property** In `WindowWatcherOptions.cs`, add: ```csharp public int IdleTimeoutMs { get; set; } = 300_000; ``` **Step 2: Add to appsettings.json** Add `"IdleTimeoutMs": 300000` to the `"WindowWatcher"` section: ```json "WindowWatcher": { "ApiBaseUrl": "http://localhost:5200", "PollIntervalMs": 2000, "DebounceMs": 3000, "IdleTimeoutMs": 300000 } ``` **Step 3: Commit** ```bash git add WindowWatcher/WindowWatcherOptions.cs WindowWatcher/appsettings.json git commit -m "feat(watcher): add configurable IdleTimeoutMs setting (default 5 min)" ``` --- ### Task 3: Add idle detection logic to Worker **Files:** - Modify: `WindowWatcher/Worker.cs` This is the main task. Add idle state tracking and API calls for pause/resume. **Step 1: Add idle tracking fields** Add these fields to the `Worker` class (after the existing `_lastChangeTime` field): ```csharp private bool _isIdle; private int? _pausedTaskId; ``` **Step 2: Add idle check to the polling loop** At the end of the `try` block in `ExecuteAsync` (after the window-change detection block, before the catch), add: ```csharp // Idle detection var idleTime = NativeMethods.GetIdleTime(); if (!_isIdle && idleTime.TotalMilliseconds >= config.IdleTimeoutMs) { _isIdle = true; logger.LogInformation("User idle for {IdleTime}, pausing active task", idleTime); await PauseActiveTaskAsync(ct: stoppingToken); } else if (_isIdle && idleTime.TotalMilliseconds < config.IdleTimeoutMs) { _isIdle = false; logger.LogInformation("User returned from idle"); await ResumeIdlePausedTaskAsync(ct: stoppingToken); } ``` **Step 3: Update the startup log to include idle timeout** Change the existing `LogInformation` line to: ```csharp logger.LogInformation( "WindowWatcher started. Polling every {Interval}ms, debounce {Debounce}ms, idle timeout {IdleTimeout}ms", config.PollIntervalMs, config.DebounceMs, config.IdleTimeoutMs); ``` **Step 4: Add PauseActiveTaskAsync method** ```csharp private async Task PauseActiveTaskAsync(CancellationToken ct) { try { var client = httpClientFactory.CreateClient("TaskTrackerApi"); // Get the active task var response = await client.GetFromJsonAsync>( "/api/tasks/active", ct); if (response?.Data is null) { logger.LogDebug("No active task to pause"); return; } _pausedTaskId = response.Data.Id; // Pause it var pauseResponse = await client.PutAsJsonAsync( $"/api/tasks/{_pausedTaskId}/pause", new { note = "Auto-paused: idle timeout" }, ct); if (pauseResponse.IsSuccessStatusCode) logger.LogInformation("Auto-paused task {TaskId}", _pausedTaskId); else logger.LogWarning("Failed to pause task {TaskId}: {Status}", _pausedTaskId, pauseResponse.StatusCode); } catch (Exception ex) { logger.LogWarning(ex, "Failed to pause active task on idle"); } } ``` **Step 5: Add ResumeIdlePausedTaskAsync method** ```csharp private async Task ResumeIdlePausedTaskAsync(CancellationToken ct) { if (_pausedTaskId is null) return; var taskId = _pausedTaskId.Value; _pausedTaskId = null; try { var client = httpClientFactory.CreateClient("TaskTrackerApi"); // Check the task is still paused (user may have manually switched tasks) var response = await client.GetFromJsonAsync>( $"/api/tasks/{taskId}", ct); if (response?.Data is null || response.Data.Status != "Paused") { logger.LogDebug("Task {TaskId} is no longer paused, skipping auto-resume", taskId); return; } // Resume it var resumeResponse = await client.PutAsJsonAsync( $"/api/tasks/{taskId}/resume", new { note = "Auto-resumed: user returned" }, ct); if (resumeResponse.IsSuccessStatusCode) logger.LogInformation("Auto-resumed task {TaskId}", taskId); else logger.LogWarning("Failed to resume task {TaskId}: {Status}", taskId, resumeResponse.StatusCode); } catch (Exception ex) { logger.LogWarning(ex, "Failed to resume task {TaskId} after idle", taskId); } } ``` **Step 6: Add the minimal DTO for deserializing API responses** Add a simple record at the bottom of `Worker.cs` (file-scoped, internal): ```csharp internal record ActiveTaskDto(int Id, string Status); ``` This is all we need to deserialize from the `ApiResponse` wrapper — `System.Text.Json` will ignore extra properties. **Step 7: Add the missing using for JSON deserialization** Verify `using System.Net.Http.Json;` already exists (it does — line 2 of Worker.cs). No changes needed. **Step 8: Commit** ```bash git add WindowWatcher/Worker.cs git commit -m "feat(watcher): add idle detection with auto-pause/resume" ``` --- ### Task 4: Build and verify **Step 1: Build the project** ```bash cd WindowWatcher && dotnet build ``` Expected: Build succeeded with 0 errors. **Step 2: Fix any build errors** If there are errors, fix them. **Step 3: Commit any fixes** If fixes were needed: ```bash git add -A && git commit -m "fix(watcher): fix build errors in idle detection" ```