Existing ASP.NET API with vanilla JS SPA, WindowWatcher, Chrome extension, and MCP server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
112 lines
3.7 KiB
C#
112 lines
3.7 KiB
C#
using System.Diagnostics;
|
|
using System.Net.Http.Json;
|
|
using System.Text;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace WindowWatcher;
|
|
|
|
public class Worker(
|
|
IHttpClientFactory httpClientFactory,
|
|
IOptions<WindowWatcherOptions> options,
|
|
ILogger<Worker> logger) : BackgroundService
|
|
{
|
|
private string _lastAppName = string.Empty;
|
|
private string _lastWindowTitle = string.Empty;
|
|
private DateTime _lastChangeTime = DateTime.MinValue;
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
var config = options.Value;
|
|
logger.LogInformation("WindowWatcher started. Polling every {Interval}ms, debounce {Debounce}ms",
|
|
config.PollIntervalMs, config.DebounceMs);
|
|
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
var hwnd = NativeMethods.GetForegroundWindow();
|
|
if (hwnd == IntPtr.Zero)
|
|
{
|
|
await Task.Delay(config.PollIntervalMs, stoppingToken);
|
|
continue;
|
|
}
|
|
|
|
// Get window title
|
|
var sb = new StringBuilder(512);
|
|
NativeMethods.GetWindowText(hwnd, sb, sb.Capacity);
|
|
var windowTitle = sb.ToString();
|
|
|
|
// Get process name
|
|
NativeMethods.GetWindowThreadProcessId(hwnd, out var pid);
|
|
string appName;
|
|
try
|
|
{
|
|
var process = Process.GetProcessById((int)pid);
|
|
appName = process.ProcessName;
|
|
}
|
|
catch
|
|
{
|
|
appName = "Unknown";
|
|
}
|
|
|
|
// Check if changed
|
|
if (appName != _lastAppName || windowTitle != _lastWindowTitle)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
|
|
// Debounce: only report if last change was more than DebounceMs ago
|
|
if ((now - _lastChangeTime).TotalMilliseconds >= config.DebounceMs
|
|
&& !string.IsNullOrWhiteSpace(windowTitle))
|
|
{
|
|
await ReportContextEvent(appName, windowTitle, stoppingToken);
|
|
}
|
|
|
|
_lastAppName = appName;
|
|
_lastWindowTitle = windowTitle;
|
|
_lastChangeTime = now;
|
|
}
|
|
}
|
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
|
{
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Error polling foreground window");
|
|
}
|
|
|
|
await Task.Delay(config.PollIntervalMs, stoppingToken);
|
|
}
|
|
}
|
|
|
|
private async Task ReportContextEvent(string appName, string windowTitle, CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
var client = httpClientFactory.CreateClient("TaskTrackerApi");
|
|
var payload = new
|
|
{
|
|
source = "WindowWatcher",
|
|
appName,
|
|
windowTitle
|
|
};
|
|
|
|
var response = await client.PostAsJsonAsync("/api/context", payload, ct);
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
logger.LogDebug("Reported: {App} - {Title}", appName, windowTitle);
|
|
}
|
|
else
|
|
{
|
|
logger.LogWarning("API returned {StatusCode} for context event", response.StatusCode);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Failed to report context event to API");
|
|
}
|
|
}
|
|
}
|