docs: add idle detection implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
263
docs/plans/2026-02-27-idle-detection-plan.md
Normal file
263
docs/plans/2026-02-27-idle-detection-plan.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# 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<LASTINPUTINFO>() };
|
||||
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<ApiResponse<ActiveTaskDto>>(
|
||||
"/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<ApiResponse<ActiveTaskDto>>(
|
||||
$"/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<T>` 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"
|
||||
```
|
||||
Reference in New Issue
Block a user