#requires -Version 5.1 [CmdletBinding()] param( [string] $ConfigPath, [switch] $DebugMode, [switch] $SelfTest, [switch] $Production, [int] $SmokeTestSeconds = 0, [switch] $LoadTypesOnly ) # 1. Config: all runtime choices belong here; JSON overrides are optional. $Config = @{ ApiBaseUrl = 'https://eventual-jany-vokes-c7b95b22.koyeb.app' AuthEndpoint = '/api/qa/login' TextEndpoint = '/api/qa/analyze-text' ImageEndpoint = '/api/qa/analyze-image' ClientVersion = '1.1.0' AnswerTimeoutSeconds = 15 AnswerPopupSeconds = 12 EnableLogin = $true EnableVisualMode = $true AccessToken = '' SettingsDirectory = '' EnableDeviceTelemetry = $true RemoveOwnRunHistory = $true CaptureVirtualScreen = $true SelectionTimeoutMilliseconds = 900 MaxTextCharacters = 16000 MaxAnswerCharacters = 1200 MaxScreenshotPixels = 40000000 MaxResponseBytes = 262144 MaxDetailsCharacters = 12000 MaxCodeCharacters = 32000 MaxImageDimension = 2048 MaxImageBytes = 3145728 AutoAnswerHotkey = 'Alt+A' AutoAnswerMarkerSeconds = 10 AutoAnswerMinConfidence = 0.75 AutoAnswerMarkerOffset = 18 RegionSelectHotkey = 'F2' MinimumSelectionSize = 20 AnalyzeRegionOnRelease = $false HideOverlaysFromCapture = $true ClearCurrentClipboardAfterLaunch = $true } Set-StrictMode -Version 2.0 $ErrorActionPreference = 'Stop' function Read-QaConfiguration { if ($Production) { $Config.AnswerTimeoutSeconds = 90 } if ($ConfigPath) { $overrides = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json foreach ($property in $overrides.PSObject.Properties) { if (-not $Config.ContainsKey($property.Name)) { throw "Unknown config key: $($property.Name)" } $Config[$property.Name] = $property.Value } } foreach ($name in @('EnableLogin', 'EnableVisualMode', 'CaptureVirtualScreen', 'AnalyzeRegionOnRelease', 'HideOverlaysFromCapture', 'ClearCurrentClipboardAfterLaunch', 'EnableDeviceTelemetry', 'RemoveOwnRunHistory')) { if ($Config[$name] -isnot [bool]) { throw "$name must be a JSON boolean." } } $ranges = @{ AnswerTimeoutSeconds = @(1, 120); AnswerPopupSeconds = @(1, 120) SelectionTimeoutMilliseconds = @(250, 5000); MaxTextCharacters = @(1, 100000) MaxAnswerCharacters = @(100, 8000); MaxScreenshotPixels = @(10000, 100000000) MaxResponseBytes = @(1024, 1048576) MaxDetailsCharacters = @(100, 12000); MaxCodeCharacters = @(100, 32000) MaxImageDimension = @(128, 4096); MaxImageBytes = @(65536, 12582912) AutoAnswerMarkerSeconds = @(1, 120); AutoAnswerMarkerOffset = @(0, 100) MinimumSelectionSize = @(2, 500) } $confidence = [double]$Config.AutoAnswerMinConfidence if ([double]::IsNaN($confidence) -or [double]::IsInfinity($confidence) -or $confidence -lt 0 -or $confidence -gt 1) { throw 'AutoAnswerMinConfidence must be between 0 and 1.' } $Config.AutoAnswerMinConfidence = $confidence foreach ($name in $ranges.Keys) { $number = 0L if (-not [long]::TryParse([string]$Config[$name], [ref]$number) -or $number -lt $ranges[$name][0] -or $number -gt $ranges[$name][1]) { throw "Invalid config value: $name" } $Config[$name] = $number } if ($SmokeTestSeconds -lt 0 -or $SmokeTestSeconds -gt 120) { throw 'SmokeTestSeconds must be 0..120.' } } # 2. Required assemblies: Windows PowerShell / built-in .NET Framework only. function Initialize-QaAssemblies { if ($PSVersionTable.PSEdition -ne 'Desktop') { throw 'Run with Windows PowerShell 5.1 (powershell.exe), not PowerShell 7.' } if ([Threading.Thread]::CurrentThread.ApartmentState -ne 'STA') { throw 'Use powershell.exe -NoProfile -STA to run this script.' } Add-Type -AssemblyName System.Windows.Forms, System.Drawing, System.Net.Http, System.Web.Extensions, System.Security [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 [Windows.Forms.Application]::EnableVisualStyles() [Windows.Forms.Application]::SetCompatibleTextRenderingDefault($false) } # 3. Embedded C# types. Native callbacks only queue work; they never call PowerShell. $nativeSource = @' using System.Diagnostics; using System.Text; using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace QaHelper { public sealed class NativeHost : NativeWindow, IDisposable { private const int WM_HOTKEY = 0x0312; private const int WM_KEYDOWN = 0x0100; private const int WM_KEYUP = 0x0101; private const int WM_SYSKEYDOWN = 0x0104; private const int WM_SYSKEYUP = 0x0105; private const int WM_LBUTTONDOWN = 0x0201; private const int WM_LBUTTONUP = 0x0202; private const int WM_RBUTTONDOWN = 0x0204; private const int WM_RBUTTONUP = 0x0205; private const int VK_LCONTROL = 0xA2; private const int VK_RCONTROL = 0xA3; private const int VK_ESCAPE = 0x1B; private const int VK_RETURN = 0x0D; private const uint MOD_ALT = 0x0001; private const uint MOD_NOREPEAT = 0x4000; private const uint LLKHF_INJECTED = 0x10; private const int QUICK_ID = 0x5141; private const int VISUAL_ID = 0x5142; private const int AUTO_ID = 0x5143; private const int DETAILS_ID = 0x5144; private const int COPY_CODE_ID = 0x5145; private const int CONTROL_ID = 0x5146; private static string dpiMode = "uninitialized"; private sealed class Signal { internal readonly string Name; internal readonly IntPtr Window; internal Signal(string name, IntPtr window) { Name = name; Window = window; } } private readonly Queue signals = new Queue(); private readonly object gate = new object(); private readonly HookProc keyboardCallback; private readonly HookProc mouseCallback; private IntPtr keyboardHook; private IntPtr mouseHook; private bool quickRegistered; private bool visualRegistered; private bool autoRegistered; private bool detailsRegistered; private bool copyCodeRegistered; private bool controlRegistered; private uint quickModifiers, quickKey, visualModifiers, visualKey, autoModifiers, autoKey; private uint detailsModifiers, detailsKey, copyModifiers, copyKey, controlModifiers, controlKey; private string detailsHotkeyLabel, copyHotkeyLabel; private bool detailsAvailable; private bool codeVisible; private string detailsHotkeyError = String.Empty; private string copyCodeHotkeyError = String.Empty; private string adaptiveHotkeyError = String.Empty; private bool disposed; private bool visualMode; private bool answerVisible; private bool leftControl; private bool rightControl; private bool escapeDown; private bool enterDown; private bool visualEscapeHeld; private bool visualEnterHeld; private bool swallowLeftUp; private bool swallowRightUp; private bool exitSent; private Point? redPoint; private Point? bluePoint; private IntPtr selectionWindow; private IntPtr captureWindow; private int regionKeyCode; private bool regionEnabled = true; private bool regionKeyDown; private bool regionActive; private bool regionSelected; private bool suppressRegionUntilRelease; private Point? pointA; private Point? pointB; public string DpiMode { get { return dpiMode; } } public string AdaptiveHotkeyError { get { return adaptiveHotkeyError; } } public bool VisualMode { get { return visualMode; } } public IntPtr SelectionWindow { get { return selectionWindow; } } public IntPtr CaptureWindow { get { return captureWindow; } } public Point? PointA { get { lock (gate) { return pointA; } } } public Point? PointB { get { lock (gate) { return pointB; } } } public bool SelectionKeyDown { get { return regionKeyDown; } } public bool RegionActive { get { return regionActive; } } public bool RegionEnabled { get { return regionEnabled; } set { regionEnabled = value; } } public Point? RedPoint { get { lock (gate) { return redPoint; } } } public Point? BluePoint { get { lock (gate) { return bluePoint; } } } public NativeHost(bool enableVisual) : this(enableVisual, new UserSettings()) { } public NativeHost(bool enableVisual, string autoHotkey, string regionKey) : this(enableVisual, LegacySettings(autoHotkey, regionKey)) { } private static UserSettings LegacySettings(string autoHotkey, string regionKey) { UserSettings settings = new UserSettings(); settings.AutoHotkey = autoHotkey; settings.RegionKey = regionKey; return settings; } public NativeHost(bool enableVisual, UserSettings settings) { keyboardCallback = KeyboardHook; mouseCallback = MouseHook; try { ValidateSettings(settings); UserSettings configured = settings.Clone(); ParseHotkey(configured.QuickHotkey, out quickModifiers, out quickKey); ParseHotkey(configured.VisualHotkey, out visualModifiers, out visualKey); ParseHotkey(configured.AutoHotkey, out autoModifiers, out autoKey); ParseHotkey(configured.DetailsHotkey, out detailsModifiers, out detailsKey); ParseHotkey(configured.CopyHotkey, out copyModifiers, out copyKey); ParseHotkey(configured.ControlHotkey, out controlModifiers, out controlKey); detailsHotkeyLabel = configured.DetailsHotkey; copyHotkeyLabel = configured.CopyHotkey; regionKeyCode = ParseSingleKey(configured.RegionKey); CreateParams parameters = new CreateParams(); parameters.Caption = "QA Helper Hotkeys"; parameters.Parent = new IntPtr(-3); CreateHandle(parameters); quickRegistered = RegisterHotKey(Handle, QUICK_ID, quickModifiers | MOD_NOREPEAT, quickKey); if (!quickRegistered) throw new Win32Exception(Marshal.GetLastWin32Error(), configured.QuickHotkey + " is already registered or unavailable."); if (enableVisual) { visualRegistered = RegisterHotKey(Handle, VISUAL_ID, visualModifiers | MOD_NOREPEAT, visualKey); if (!visualRegistered) throw new Win32Exception(Marshal.GetLastWin32Error(), configured.VisualHotkey + " is already registered or unavailable."); } autoRegistered = RegisterHotKey(Handle, AUTO_ID, autoModifiers | MOD_NOREPEAT, autoKey); if (!autoRegistered) throw new Win32Exception(Marshal.GetLastWin32Error(), configured.AutoHotkey + " is already registered or unavailable."); controlRegistered = RegisterHotKey(Handle, CONTROL_ID, controlModifiers | MOD_NOREPEAT, controlKey); if (!controlRegistered) throw new Win32Exception(Marshal.GetLastWin32Error(), configured.ControlHotkey + " is already registered or unavailable."); keyboardHook = SetWindowsHookEx(13, keyboardCallback, GetModuleHandle(null), 0); if (keyboardHook == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error(), "The global keyboard hook could not be installed."); leftControl = IsDown(VK_LCONTROL); rightControl = IsDown(VK_RCONTROL); regionKeyDown = IsDown(regionKeyCode); suppressRegionUntilRelease = regionKeyDown; CheckExitChord(); } catch { Dispose(); throw; } } public static void ValidateBindings(string autoHotkey, string regionKey) { ValidateSettings(LegacySettings(autoHotkey, regionKey)); } public static void ValidateSettings(UserSettings settings) { if (settings == null) throw new ArgumentNullException("settings"); settings.Validate(); int region = ParseSingleKey(settings.RegionKey); if (IsReservedRegionKey(region)) throw new ArgumentException("Region selection key must be an ordinary keyboard key other than Escape or Enter.", "RegionKey"); string[] bindings = { settings.QuickHotkey, settings.VisualHotkey, settings.AutoHotkey, settings.DetailsHotkey, settings.CopyHotkey, settings.ControlHotkey }; foreach (string binding in bindings) { uint modifiers, key; ParseHotkey(binding, out modifiers, out key); if (region == (int)key) throw new ArgumentException("Region selection key conflicts with another assistant hotkey.", "RegionKey"); } } private static int ParseSingleKey(string name) { Keys key; string text = (name ?? String.Empty).Trim(); if (text.Length == 1 && Char.IsDigit(text[0])) text = "D" + text; if (String.IsNullOrWhiteSpace(text) || !Enum.TryParse(text, true, out key) || !Enum.IsDefined(typeof(Keys), key) || (int)key <= 0 || (int)key > 255) throw new ArgumentException("Use one Windows key name, for example F2 or A."); return (int)key; } private static bool IsReservedRegionKey(int key) { return key == VK_ESCAPE || key == VK_RETURN || key == (int)Keys.LButton || key == (int)Keys.RButton || key == (int)Keys.MButton || key == (int)Keys.XButton1 || key == (int)Keys.XButton2 || key == (int)Keys.ControlKey || key == (int)Keys.LControlKey || key == (int)Keys.RControlKey || key == (int)Keys.ShiftKey || key == (int)Keys.LShiftKey || key == (int)Keys.RShiftKey || key == (int)Keys.Menu || key == (int)Keys.LMenu || key == (int)Keys.RMenu || key == (int)Keys.LWin || key == (int)Keys.RWin; } private static void ParseHotkey(string hotkey, out uint modifiers, out uint key) { if (String.IsNullOrWhiteSpace(hotkey)) throw new ArgumentException("A hotkey is required.", "hotkey"); string[] parts = hotkey.Split('+'); modifiers = 0; for (int i = 0; i < parts.Length - 1; i++) { uint modifier; switch (parts[i].Trim().ToUpperInvariant()) { case "ALT": modifier = 0x1; break; case "CTRL": case "CONTROL": modifier = 0x2; break; case "SHIFT": modifier = 0x4; break; case "WIN": case "WINDOWS": modifier = 0x8; break; default: throw new ArgumentException("Unknown hotkey modifier.", "hotkey"); } if ((modifiers & modifier) != 0) throw new ArgumentException("A hotkey modifier appears twice.", "hotkey"); modifiers |= modifier; } key = (uint)ParseSingleKey(parts[parts.Length - 1]); if ((IsReservedRegionKey((int)key) && key != VK_RETURN) || (key == VK_RETURN && modifiers == 0)) throw new ArgumentException("A hotkey needs a non-modifier key; Enter requires a modifier.", "hotkey"); } public static string InitializeDpiAwareness() { // The PowerShell host may already have fixed process awareness; a UI-thread override still works. try { if (SetProcessDpiAwarenessContext(new IntPtr(-4))) return dpiMode = "per-monitor-v2"; if (SetThreadDpiAwarenessContext(new IntPtr(-4)) != IntPtr.Zero) return dpiMode = "per-monitor-v2-thread"; } catch (EntryPointNotFoundException) { } catch (DllNotFoundException) { } try { if (SetProcessDpiAwareness(2) == 0) return dpiMode = "per-monitor"; int awareness; if (GetProcessDpiAwareness(IntPtr.Zero, out awareness) == 0 && awareness == 2) return dpiMode = "per-monitor-existing"; } catch (EntryPointNotFoundException) { } catch (DllNotFoundException) { } try { if (SetProcessDPIAware()) return dpiMode = "system-aware"; } catch (EntryPointNotFoundException) { } return dpiMode = "existing-host-awareness"; } public static Rectangle GetVirtualBounds() { return new Rectangle(GetSystemMetrics(76), GetSystemMetrics(77), GetSystemMetrics(78), GetSystemMetrics(79)); } public void SetContext(bool visual, bool popupVisible) { if (disposed) return; answerVisible = popupVisible; if (visual == visualMode) return; if (visual) { CancelSelection(); lock (gate) { redPoint = null; bluePoint = null; } enterDown = IsDown(VK_RETURN); escapeDown = IsDown(VK_ESCAPE); if (mouseHook == IntPtr.Zero) { mouseHook = SetWindowsHookEx(14, mouseCallback, GetModuleHandle(null), 0); if (mouseHook == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error(), "The visual-mode mouse hook could not be installed."); } visualMode = true; } else { visualMode = false; // Finish swallowed button pairs even if Enter/Esc ended visual mode while a button was held. ReleaseMouseHookWhenIdle(); lock (gate) { redPoint = null; bluePoint = null; } } } public void SetRegionSelected(bool selected) { if (!disposed) regionSelected = selected; } public void SetAdaptiveContext(bool detailsAvailable, bool codeVisible) { if (disposed) return; if (this.detailsAvailable != detailsAvailable) { this.detailsAvailable = detailsAvailable; UpdateAdaptiveRegistration(detailsAvailable, DETAILS_ID, detailsModifiers, detailsKey, detailsHotkeyLabel, ref detailsRegistered, ref detailsHotkeyError); } if (this.codeVisible != codeVisible) { this.codeVisible = codeVisible; UpdateAdaptiveRegistration(codeVisible, COPY_CODE_ID, copyModifiers, copyKey, copyHotkeyLabel, ref copyCodeRegistered, ref copyCodeHotkeyError); } adaptiveHotkeyError = detailsHotkeyError; if (!String.IsNullOrEmpty(copyCodeHotkeyError)) adaptiveHotkeyError += (String.IsNullOrEmpty(adaptiveHotkeyError) ? "" : "; ") + copyCodeHotkeyError; } private void UpdateAdaptiveRegistration(bool wanted, int id, uint modifiers, uint key, string label, ref bool registered, ref string error) { error = String.Empty; try { if (wanted && !registered) { registered = RegisterHotKey(Handle, id, modifiers | MOD_NOREPEAT, key); if (!registered) error = label + " unavailable (Win32 " + Marshal.GetLastWin32Error() + ")."; } else if (!wanted && registered) { if (UnregisterHotKey(Handle, id)) registered = false; else error = label + " could not be released (Win32 " + Marshal.GetLastWin32Error() + ")."; } } catch (DllNotFoundException) { error = label + " unavailable (Win32 126)."; } catch (EntryPointNotFoundException) { error = label + " unavailable (Win32 127)."; } } public void CancelSelection() { regionActive = false; regionSelected = false; lock (gate) { pointA = null; pointB = null; } // Keep regionKeyDown/suppression until the real release; key-repeat cannot restart a cancelled drag. } public string TakeSignal() { // Polling also preserves the exit chord if Windows ever removes a timed-out low-level hook. if (!disposed && !exitSent && IsDown(VK_LCONTROL) && IsDown(VK_RCONTROL)) { leftControl = true; rightControl = true; CheckExitChord(); } lock (gate) { while (signals.Count != 0) { Signal signal = signals.Dequeue(); if (signal.Name == "details" && !detailsAvailable) continue; if (signal.Name == "copy-code" && !codeVisible) continue; if (signal.Name == "quick") selectionWindow = signal.Window; else if (signal.Name == "auto") captureWindow = signal.Window; return signal.Name; } return null; } } private void Enqueue(string signal) { Enqueue(signal, IntPtr.Zero); } private void Enqueue(string signal, IntPtr window) { lock (gate) { // A stalled consumer must not create an unbounded native callback queue. if (signal == "exit") { signals.Clear(); signals.Enqueue(new Signal(signal, window)); } else if (signals.Count < 64 && !exitSent) signals.Enqueue(new Signal(signal, window)); } } private static bool IsDown(int key) { return (GetAsyncKeyState(key) & 0x8000) != 0; } private void CheckExitChord() { if (leftControl && rightControl && !exitSent) { exitSent = true; Enqueue("exit"); } } protected override void WndProc(ref Message message) { if (message.Msg == WM_HOTKEY && !disposed && !exitSent) { if (message.WParam.ToInt32() == QUICK_ID) { Enqueue("quick", GetForegroundWindow()); } else if (message.WParam.ToInt32() == VISUAL_ID) Enqueue("visual"); else if (message.WParam.ToInt32() == CONTROL_ID) Enqueue("control"); else if (message.WParam.ToInt32() == AUTO_ID) Enqueue("auto", GetForegroundWindow()); else if (message.WParam.ToInt32() == DETAILS_ID && detailsAvailable && detailsRegistered) Enqueue("details"); else if (message.WParam.ToInt32() == COPY_CODE_ID && codeVisible && copyCodeRegistered) Enqueue("copy-code"); return; } base.WndProc(ref message); } private IntPtr KeyboardHook(int code, IntPtr wParam, IntPtr lParam) { if (code >= 0 && !disposed) { KeyboardData data = (KeyboardData)Marshal.PtrToStructure(lParam, typeof(KeyboardData)); int message = wParam.ToInt32(); bool down = message == WM_KEYDOWN || message == WM_SYSKEYDOWN; bool up = message == WM_KEYUP || message == WM_SYSKEYUP; bool physical = (data.flags & LLKHF_INJECTED) == 0; if (physical && (down || up)) { if (data.vkCode == VK_LCONTROL) { leftControl = down; rightControl = IsDown(VK_RCONTROL); } else if (data.vkCode == VK_RCONTROL) { rightControl = down; leftControl = IsDown(VK_LCONTROL); } CheckExitChord(); if (data.vkCode == regionKeyCode) { if (down && !regionKeyDown) { regionKeyDown = true; suppressRegionUntilRelease = true; if (regionEnabled && !visualMode && !exitSent) { NativePoint cursor; if (GetCursorPos(out cursor)) { lock (gate) { pointA = new Point(cursor.x, cursor.y); pointB = null; } regionActive = true; regionSelected = false; Enqueue("region-start"); } } } if (up) { if (regionKeyDown && regionActive && !exitSent) { NativePoint cursor; if (GetCursorPos(out cursor)) { lock (gate) { pointB = new Point(cursor.x, cursor.y); } regionSelected = true; Enqueue("region-end"); } else { CancelSelection(); Enqueue("escape"); } regionActive = false; } regionKeyDown = false; bool consume = suppressRegionUntilRelease; suppressRegionUntilRelease = false; if (consume) return new IntPtr(1); } else if (suppressRegionUntilRelease) return new IntPtr(1); } if (data.vkCode == VK_ESCAPE) { bool first = down && !escapeDown; escapeDown = down; bool selectionContext = regionActive || regionSelected; if (first && (visualMode || answerVisible || selectionContext)) Enqueue("escape"); if (down && (visualMode || selectionContext)) visualEscapeHeld = true; bool consume = visualMode || selectionContext || visualEscapeHeld; if (up) visualEscapeHeld = false; if (consume) return new IntPtr(1); } else if (data.vkCode == VK_RETURN) { bool first = down && !enterDown; enterDown = down; bool selectionContext = regionActive || regionSelected; if (first && (visualMode || selectionContext)) Enqueue("analyze"); if (down && (visualMode || selectionContext)) visualEnterHeld = true; bool consume = visualMode || selectionContext || visualEnterHeld; if (up) visualEnterHeld = false; if (consume) return new IntPtr(1); } } } return CallNextHookEx(keyboardHook, code, wParam, lParam); } private IntPtr MouseHook(int code, IntPtr wParam, IntPtr lParam) { if (code >= 0 && !disposed) { int message = wParam.ToInt32(); if (visualMode && (message == WM_LBUTTONDOWN || message == WM_RBUTTONDOWN)) { MouseData data = (MouseData)Marshal.PtrToStructure(lParam, typeof(MouseData)); lock (gate) { if (message == WM_LBUTTONDOWN) redPoint = new Point(data.point.x, data.point.y); else bluePoint = new Point(data.point.x, data.point.y); } if (message == WM_LBUTTONDOWN) swallowLeftUp = true; else swallowRightUp = true; Enqueue("markers"); return new IntPtr(1); } if (message == WM_LBUTTONUP && swallowLeftUp) { swallowLeftUp = false; ReleaseMouseHookWhenIdle(); return new IntPtr(1); } if (message == WM_RBUTTONUP && swallowRightUp) { swallowRightUp = false; ReleaseMouseHookWhenIdle(); return new IntPtr(1); } } return CallNextHookEx(mouseHook, code, wParam, lParam); } private void ReleaseMouseHookWhenIdle() { if (!visualMode && !swallowLeftUp && !swallowRightUp && mouseHook != IntPtr.Zero) { UnhookWindowsHookEx(mouseHook); mouseHook = IntPtr.Zero; } } public void Dispose() { if (disposed) return; disposed = true; visualMode = false; CancelSelection(); regionKeyDown = false; suppressRegionUntilRelease = false; if (quickRegistered) { UnregisterHotKey(Handle, QUICK_ID); quickRegistered = false; } if (visualRegistered) { UnregisterHotKey(Handle, VISUAL_ID); visualRegistered = false; } if (autoRegistered) { UnregisterHotKey(Handle, AUTO_ID); autoRegistered = false; } if (controlRegistered) { UnregisterHotKey(Handle, CONTROL_ID); controlRegistered = false; } if (detailsRegistered) { UnregisterHotKey(Handle, DETAILS_ID); detailsRegistered = false; } if (copyCodeRegistered) { UnregisterHotKey(Handle, COPY_CODE_ID); copyCodeRegistered = false; } detailsAvailable = false; codeVisible = false; if (mouseHook != IntPtr.Zero) { UnhookWindowsHookEx(mouseHook); mouseHook = IntPtr.Zero; } if (keyboardHook != IntPtr.Zero) { UnhookWindowsHookEx(keyboardHook); keyboardHook = IntPtr.Zero; } lock (gate) { signals.Clear(); redPoint = null; bluePoint = null; } if (Handle != IntPtr.Zero) DestroyHandle(); GC.KeepAlive(keyboardCallback); GC.KeepAlive(mouseCallback); } private delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam); [StructLayout(LayoutKind.Sequential)] private struct NativePoint { public int x; public int y; } [StructLayout(LayoutKind.Sequential)] private struct KeyboardData { public uint vkCode; public uint scanCode; public uint flags; public uint time; public UIntPtr extraInfo; } [StructLayout(LayoutKind.Sequential)] private struct MouseData { public NativePoint point; public uint mouseData; public uint flags; public uint time; public UIntPtr extraInfo; } [DllImport("user32.dll", SetLastError = true)] private static extern bool RegisterHotKey(IntPtr handle, int id, uint modifiers, uint key); [DllImport("user32.dll", SetLastError = true)] private static extern bool UnregisterHotKey(IntPtr handle, int id); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SetWindowsHookEx(int hook, HookProc callback, IntPtr module, uint thread); [DllImport("user32.dll", SetLastError = true)] private static extern bool UnhookWindowsHookEx(IntPtr hook); [DllImport("user32.dll")] private static extern IntPtr CallNextHookEx(IntPtr hook, int code, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll")] private static extern short GetAsyncKeyState(int key); [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] private static extern bool GetCursorPos(out NativePoint point); [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr GetModuleHandle(string module); [DllImport("user32.dll", SetLastError = true)] private static extern bool SetProcessDpiAwarenessContext(IntPtr context); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr context); [DllImport("shcore.dll")] private static extern int SetProcessDpiAwareness(int awareness); [DllImport("shcore.dll")] private static extern int GetProcessDpiAwareness(IntPtr process, out int awareness); [DllImport("user32.dll")] private static extern bool SetProcessDPIAware(); [DllImport("user32.dll")] private static extern int GetSystemMetrics(int index); } public class PassiveForm : Form { private bool clickThrough; public PassiveForm() { FormBorderStyle = FormBorderStyle.None; StartPosition = FormStartPosition.Manual; ShowInTaskbar = false; TopMost = true; AutoScaleMode = AutoScaleMode.None; } public bool ClickThrough { get { return clickThrough; } set { if (clickThrough == value) return; clickThrough = value; if (IsHandleCreated) { int style = GetWindowLong(Handle, -20); style = value ? style | 0x20 : style & ~0x20; SetWindowLong(Handle, -20, style); } } } protected override bool ShowWithoutActivation { get { return true; } } protected override CreateParams CreateParams { get { CreateParams parameters = base.CreateParams; parameters.ExStyle |= 0x08000000 | 0x00000080; // NOACTIVATE | TOOLWINDOW. if (clickThrough) parameters.ExStyle |= 0x20; return parameters; } } protected override void WndProc(ref Message message) { if (message.Msg == 0x21) // WM_MOUSEACTIVATE. { message.Result = new IntPtr(3); // MA_NOACTIVATE: deliver the click without focus theft. return; } base.WndProc(ref message); } [DllImport("user32.dll", EntryPoint = "GetWindowLongW")] private static extern int GetWindowLong(IntPtr handle, int index); [DllImport("user32.dll", EntryPoint = "SetWindowLongW")] private static extern int SetWindowLong(IntPtr handle, int index, int value); } public sealed class CaptureAffinity : IDisposable { private const uint WDA_NONE = 0; private const uint WDA_EXCLUDEFROMCAPTURE = 0x11; private Form form; private bool enabled; private bool disposed; public CaptureAffinity(Form form, bool enabled) { if (form == null) throw new ArgumentNullException("form"); this.form = form; this.enabled = enabled; form.HandleCreated += HandleCreated; form.HandleDestroyed += HandleDestroyed; ApplyToCurrentHandle(); } public bool Enabled { get { return enabled; } set { if (disposed) return; enabled = value; ApplyToCurrentHandle(); } } public bool Applied { get; private set; } public int LastError { get; private set; } public uint ActualAffinity { get; private set; } private void HandleCreated(object sender, EventArgs args) { ApplyToCurrentHandle(); } private void HandleDestroyed(object sender, EventArgs args) { Applied = false; ActualAffinity = WDA_NONE; } private void ApplyToCurrentHandle() { Applied = false; ActualAffinity = WDA_NONE; LastError = 0; if (disposed || form == null || form.IsDisposed || !form.IsHandleCreated) return; try { IntPtr handle = form.Handle; uint wanted = enabled ? WDA_EXCLUDEFROMCAPTURE : WDA_NONE; bool set = SetWindowDisplayAffinity(handle, wanted); int setError = set ? 0 : Marshal.GetLastWin32Error(); uint actual; bool queried = GetWindowDisplayAffinity(handle, out actual); int queryError = queried ? 0 : Marshal.GetLastWin32Error(); ActualAffinity = queried ? actual : WDA_NONE; LastError = !set ? setError : queryError; Applied = enabled && set && queried && actual == wanted; } catch (DllNotFoundException) { LastError = 126; } catch (EntryPointNotFoundException) { LastError = 127; } catch (ObjectDisposedException) { LastError = 1400; } catch (InvalidOperationException) { LastError = 1400; } } public void Dispose() { if (disposed) return; form.HandleCreated -= HandleCreated; form.HandleDestroyed -= HandleDestroyed; enabled = false; ApplyToCurrentHandle(); disposed = true; form = null; Applied = false; } [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetWindowDisplayAffinity(IntPtr handle, uint affinity); [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GetWindowDisplayAffinity(IntPtr handle, out uint affinity); } public sealed class MarkerForm : PassiveForm { private Point? redPoint; private Point? bluePoint; public MarkerForm(Rectangle bounds) { Bounds = bounds; BackColor = Color.Magenta; TransparencyKey = Color.Magenta; ClickThrough = true; DoubleBuffered = true; } public void SetPoints(Point? red, Point? blue) { redPoint = red; bluePoint = blue; Invalidate(); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; MarkerDrawing.Draw(e.Graphics, redPoint, Color.Red, Bounds.Location); MarkerDrawing.Draw(e.Graphics, bluePoint, Color.RoyalBlue, Bounds.Location); } } public sealed class RegionForm : PassiveForm { private Rectangle? region; public RegionForm(Rectangle bounds) { Bounds = bounds; BackColor = Color.Magenta; TransparencyKey = Color.Magenta; ClickThrough = true; DoubleBuffered = true; } public void SetRegion(Rectangle? rectangle) { region = rectangle; Invalidate(); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (!region.HasValue || region.Value.Width <= 0 || region.Value.Height <= 0) return; Rectangle outline = region.Value; outline.Offset(-Bounds.X, -Bounds.Y); outline.Width = Math.Max(0, outline.Width - 1); outline.Height = Math.Max(0, outline.Height - 1); using (Pen shadow = new Pen(Color.FromArgb(28, 32, 43), 3)) using (Pen border = new Pen(Color.FromArgb(255, 220, 90), 1)) { e.Graphics.DrawRectangle(shadow, outline); e.Graphics.DrawRectangle(border, outline); } } } internal static class MarkerDrawing { internal static void Draw(Graphics graphics, Point? point, Color color, Point origin) { if (!point.HasValue) return; Rectangle marker = new Rectangle(point.Value.X - origin.X - 9, point.Value.Y - origin.Y - 9, 18, 18); using (SolidBrush fill = new SolidBrush(color)) using (Pen outline = new Pen(Color.White, 2)) { graphics.FillEllipse(fill, marker); graphics.DrawEllipse(outline, marker); } } } public sealed class CapturedImage { public byte[] Png; public int Width; public int Height; } public static class ScreenCapture { public static Task BeginCapture(Rectangle bounds, Point? red, Point? blue, long maxPixels) { return Task.Factory.StartNew(delegate { NativeHost.InitializeDpiAwareness(); return Capture(bounds, red, blue, maxPixels); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } public static Task BeginPreparedCapture(Rectangle bounds, Point? red, Point? blue, long maxPixels, int maxDimension, int maxBytes) { return Task.Factory.StartNew(delegate { NativeHost.InitializeDpiAwareness(); if (maxDimension < 1 || maxBytes < 1) throw new ArgumentException("The screenshot dimension and byte limits must be positive."); using (Bitmap bitmap = CaptureBitmap(bounds, red, blue, maxPixels)) return PrepareBitmap(bitmap, maxDimension, maxBytes); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } public static byte[] Capture(Rectangle bounds, Point? red, Point? blue, long maxPixels) { using (Bitmap bitmap = CaptureBitmap(bounds, red, blue, maxPixels)) return EncodePng(bitmap); } private static Bitmap CaptureBitmap(Rectangle bounds, Point? red, Point? blue, long maxPixels) { if (bounds.Width <= 0 || bounds.Height <= 0) throw new ArgumentException("The screen bounds are empty.", "bounds"); if (maxPixels <= 0 || (long)bounds.Width * bounds.Height > maxPixels) throw new ArgumentException("The virtual desktop exceeds the configured screenshot pixel limit.", "bounds"); Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb); try { using (Graphics graphics = Graphics.FromImage(bitmap)) { graphics.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size, CopyPixelOperation.SourceCopy); graphics.SmoothingMode = SmoothingMode.AntiAlias; MarkerDrawing.Draw(graphics, red, Color.Red, bounds.Location); MarkerDrawing.Draw(graphics, blue, Color.RoyalBlue, bounds.Location); } return bitmap; } catch { bitmap.Dispose(); throw; } } private static byte[] EncodePng(Bitmap bitmap) { using (MemoryStream stream = new MemoryStream()) { bitmap.Save(stream, ImageFormat.Png); return stream.ToArray(); } } private static CapturedImage PrepareBitmap(Bitmap source, int maxDimension, int maxBytes) { if (maxDimension < 1 || maxBytes < 1) throw new ArgumentException("The screenshot dimension and byte limits must be positive."); double scale = Math.Min(1.0, (double)maxDimension / Math.Max(source.Width, source.Height)); int width = Math.Max(1, (int)Math.Round(source.Width * scale)); int height = Math.Max(1, (int)Math.Round(source.Height * scale)); for (int attempt = 0; attempt <= 10; attempt++) { byte[] png; if (width == source.Width && height == source.Height) png = EncodePng(source); else { using (Bitmap resized = new Bitmap(width, height, PixelFormat.Format32bppArgb)) { using (Graphics graphics = Graphics.FromImage(resized)) using (ImageAttributes attributes = new ImageAttributes()) { graphics.CompositingQuality = CompositingQuality.HighQuality; graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; attributes.SetWrapMode(WrapMode.TileFlipXY); graphics.DrawImage(source, new Rectangle(0, 0, width, height), 0, 0, source.Width, source.Height, GraphicsUnit.Pixel, attributes); } png = EncodePng(resized); } } if (png.Length <= maxBytes) return new CapturedImage { Png = png, Width = width, Height = height }; Array.Clear(png, 0, png.Length); if (width == 1 && height == 1) break; scale *= 0.8; width = Math.Max(1, (int)Math.Round(source.Width * scale)); height = Math.Max(1, (int)Math.Round(source.Height * scale)); } throw new InvalidOperationException("The screenshot cannot fit within the configured image byte limit."); } } } namespace QaHelper { public sealed class SelectionResult { public string Text { get; set; } public string Error { get; set; } public bool ClipboardRestored { get; set; } } public sealed class SelectionCapture : IDisposable { private const int SnapshotLimit = 16 * 1024 * 1024; private const uint UnicodeText = 13; private const uint GlobalMoveable = 2; private const uint KeyUp = 2; private readonly IntPtr target; private readonly uint targetProcess; private readonly int timeout; private readonly int textLimit; private readonly object sync = new object(); private volatile bool cancelled; private bool disposed; private TaskCompletionSource completion; private Thread worker; private sealed class ClipboardEntry { public uint Format; public byte[] Bytes; public IntPtr RestoreHandle; } private sealed class ClipboardWindow : System.Windows.Forms.NativeWindow, IDisposable { public ClipboardWindow() { System.Windows.Forms.CreateParams parameters = new System.Windows.Forms.CreateParams(); parameters.Caption = "QA clipboard transaction"; parameters.Parent = new IntPtr(-3); CreateHandle(parameters); } public void Dispose() { DestroyHandle(); } } public SelectionCapture(IntPtr foreground, int timeoutMs, int maxTextChars) { if (foreground == IntPtr.Zero) throw new ArgumentException("An active window is required.", "foreground"); if (maxTextChars < 1 || maxTextChars > 1000000) throw new ArgumentOutOfRangeException("maxTextChars"); target = foreground; uint process; ClipGetWindowThreadProcessId(target, out process); targetProcess = process; timeout = Math.Max(150, Math.Min(timeoutMs, 5000)); textLimit = maxTextChars; } public Task Start() { lock (sync) { if (disposed) throw new ObjectDisposedException("SelectionCapture"); if (completion != null) return completion.Task; completion = new TaskCompletionSource(); worker = new Thread(Run); worker.IsBackground = true; worker.Name = "QA selection clipboard"; worker.SetApartmentState(ApartmentState.STA); worker.Start(); return completion.Task; } } public void Dispose() { Thread current; lock (sync) { if (disposed) return; disposed = true; cancelled = true; current = worker; } // Never abort a thread while it owns clipboard memory or a native lock. // Overlays/hooks are already gone at shutdown; allow an in-flight restore to finish. if (current != null && current != Thread.CurrentThread) current.Join(2000); } private void Run() { SelectionResult result = new SelectionResult(); result.ClipboardRestored = true; List snapshot = new List(); ClipboardWindow window = null; uint copiedSequence = 0; bool copyIssued = false; bool restoreNeeded = false; bool restoreStarted = false; try { window = new ClipboardWindow(); if (!WaitForShortcutRelease()) { result.Error = cancelled ? "Cancelled" : "Release shortcut keys"; return; } if (!IsTargetForeground()) { result.Error = "Active window changed"; return; } uint originalSequence; SaveClipboardState(window.Handle, snapshot, out originalSequence); if (cancelled) { result.Error = "Cancelled"; return; } PrepareRestoreHandles(snapshot); if (!IsTargetForeground() || ClipGetClipboardSequenceNumber() != originalSequence) { result.Error = "Clipboard or active window changed"; return; } bool fullInjection = SendCopy(out copyIssued); if (!fullInjection) { result.Error = "Cannot copy from this window"; if (!copyIssued) return; } Stopwatch wait = Stopwatch.StartNew(); bool clipboardChanged = false; while (wait.ElapsedMilliseconds < timeout) { uint observed = ClipGetClipboardSequenceNumber(); if (observed != originalSequence) { copiedSequence = observed; clipboardChanged = true; break; } // Cancellation cannot skip restoration after a copy has been injected. if (cancelled && wait.ElapsedMilliseconds >= Math.Min(timeout, 750)) break; Thread.Sleep(15); } if (!clipboardChanged) { if (cancelled) result.Error = "Cancelled"; return; } if (!OpenWithRetry(window.Handle, 220, false)) { restoreNeeded = true; result.ClipboardRestored = false; result.Error = "Clipboard busy; original could not be restored"; return; } try { if (ClipGetClipboardSequenceNumber() != copiedSequence || !ClipboardOwnerIsTarget()) { result.ClipboardRestored = false; result.Error = "Clipboard changed by another application"; return; } restoreNeeded = true; result.ClipboardRestored = false; if (!cancelled && fullInjection) result.Text = ReadCopiedText(); } finally { // Rendering CF_UNICODETEXT can increment the sequence even when text validation fails. if (restoreNeeded) copiedSequence = ClipGetClipboardSequenceNumber(); ClipCloseClipboard(); } result.ClipboardRestored = RestoreClipboardState(window.Handle, snapshot, ref copiedSequence, ref restoreStarted); restoreNeeded = !result.ClipboardRestored; if (!result.ClipboardRestored) result.Error = "Clipboard changed or could not be restored"; if (cancelled) { result.Text = null; result.Error = "Cancelled"; } } catch (Exception ex) { if (String.IsNullOrEmpty(result.Error)) result.Error = ex is InvalidOperationException ? ex.Message : "Cannot capture selected text"; } finally { if (restoreNeeded && window != null) { // Keep the eager snapshot available for one last bounded restoration attempt. try { if (RestoreClipboardState(window.Handle, snapshot, ref copiedSequence, ref restoreStarted)) { result.ClipboardRestored = true; if (result.Error == "Clipboard changed or could not be restored" || result.Error == "Clipboard busy; original could not be restored") result.Error = null; } } catch { result.ClipboardRestored = false; } } if (!copyIssued) result.ClipboardRestored = true; try { FreeSnapshot(snapshot); } finally { try { if (window != null) window.Dispose(); } finally { completion.TrySetResult(result); } } } } private bool WaitForShortcutRelease() { Stopwatch watch = Stopwatch.StartNew(); while (watch.ElapsedMilliseconds < 900 && !cancelled) { if (ShortcutKeysReleased()) return true; Thread.Sleep(10); } return false; } private static bool ShortcutKeysReleased() { int[] keys = new int[] { 0x12, 0x51, 0x11, 0x10, 0x5B, 0x5C }; for (int index = 0; index < keys.Length; index++) if ((ClipGetAsyncKeyState(keys[index]) & 0x8000) != 0) return false; return true; } private bool IsTargetForeground() { uint process; ClipGetWindowThreadProcessId(target, out process); return ClipGetForegroundWindow() == target && targetProcess != 0 && process == targetProcess && ClipIsWindow(target); } private bool ClipboardOwnerIsTarget() { IntPtr owner = ClipGetClipboardOwner(); if (owner == IntPtr.Zero) return false; uint process; ClipGetWindowThreadProcessId(owner, out process); return process == targetProcess; } private bool OpenWithRetry(IntPtr owner, int milliseconds, bool observeCancellation) { Stopwatch watch = Stopwatch.StartNew(); do { if (observeCancellation && cancelled) return false; if (ClipOpenClipboard(owner)) return true; Thread.Sleep(15); } while (watch.ElapsedMilliseconds < milliseconds); return false; } private void SaveClipboardState(IntPtr owner, List entries, out uint sequence) { sequence = 0; if (!OpenWithRetry(owner, 220, true)) throw new InvalidOperationException("Clipboard busy; try again"); try { List formats = new List(); uint format = 0; while (true) { ClipSetLastError(0); format = ClipEnumClipboardFormats(format); if (format == 0) { if (Marshal.GetLastWin32Error() != 0) throw new InvalidOperationException("Cannot preserve clipboard"); break; } if (formats.Count >= 64 || !IsSafeMemoryFormat(format)) throw new InvalidOperationException("Clipboard has unsupported data; copy plain text first"); formats.Add(format); } // No IDataObject or lazy reference survives this read: every byte is eagerly copied. long total = 0; foreach (uint item in formats) { IntPtr data = ClipGetClipboardData(item); if (data == IntPtr.Zero) throw new InvalidOperationException("Cannot preserve clipboard"); ulong count = ClipGlobalSize(data).ToUInt64(); if (count == 0 || count > SnapshotLimit || total + (long)count > SnapshotLimit) throw new InvalidOperationException("Clipboard is too large to preserve safely"); IntPtr pointer = ClipGlobalLock(data); if (pointer == IntPtr.Zero) throw new InvalidOperationException("Cannot preserve clipboard"); byte[] bytes = new byte[(int)count]; try { Marshal.Copy(pointer, bytes, 0, bytes.Length); } finally { ClipGlobalUnlock(data); } entries.Add(new ClipboardEntry { Format = item, Bytes = bytes }); total += (long)count; } sequence = ClipGetClipboardSequenceNumber(); } finally { ClipCloseClipboard(); } } private static bool IsSafeMemoryFormat(uint format) { switch (format) { case 1: case 4: case 5: case 6: case 7: case 8: case 11: case 12: case 13: case 15: case 16: case 17: case 0x81: return true; } if (format < 0xC000) return false; StringBuilder name = new StringBuilder(256); if (ClipGetClipboardFormatName(format, name, name.Capacity) == 0) return false; switch (name.ToString()) { case "HTML Format": case "Rich Text Format": case "Rich Text Format Without Objects": case "UniformResourceLocator": case "UniformResourceLocatorW": case "CSV": case "Preferred DropEffect": case "Performed DropEffect": case "Logical Performed DropEffect": case "Paste Succeeded": case "FileName": case "FileNameW": case "FileGroupDescriptor": case "FileGroupDescriptorW": case "Shell IDList Array": case "Shell Object Offsets": case "TargetCLSID": case "ExcludeClipboardContentFromMonitorProcessing": case "CanIncludeInClipboardHistory": case "CanUploadToCloudClipboard": return true; default: return false; } } private static void PrepareRestoreHandles(List entries) { foreach (ClipboardEntry entry in entries) { entry.RestoreHandle = ClipGlobalAlloc(GlobalMoveable, new UIntPtr((uint)entry.Bytes.Length)); if (entry.RestoreHandle == IntPtr.Zero) throw new InvalidOperationException("Cannot preserve clipboard"); IntPtr pointer = ClipGlobalLock(entry.RestoreHandle); if (pointer == IntPtr.Zero) throw new InvalidOperationException("Cannot preserve clipboard"); try { Marshal.Copy(entry.Bytes, 0, pointer, entry.Bytes.Length); } finally { ClipGlobalUnlock(entry.RestoreHandle); } } } private string ReadCopiedText() { if (!ClipIsClipboardFormatAvailable(UnicodeText)) return null; IntPtr data = ClipGetClipboardData(UnicodeText); if (data == IntPtr.Zero) return null; ulong bytes = ClipGlobalSize(data).ToUInt64(); if (bytes < 2) return null; // One extra character detects truncation, so a partial question is never sent silently. int count = (int)Math.Min(bytes / 2, (ulong)textLimit + 2UL); IntPtr pointer = ClipGlobalLock(data); if (pointer == IntPtr.Zero) throw new InvalidOperationException("Cannot read selected text"); string value; try { value = Marshal.PtrToStringUni(pointer, count); } finally { ClipGlobalUnlock(data); } int terminator = value.IndexOf('\0'); if (terminator >= 0) value = value.Substring(0, terminator); if (value.Length > textLimit) throw new InvalidOperationException("Selected text is too long"); return String.IsNullOrWhiteSpace(value) ? null : value.Trim(); } private bool RestoreClipboardState(IntPtr owner, List entries, ref uint expectedSequence, ref bool started) { Stopwatch watch = Stopwatch.StartNew(); do { if (!ClipOpenClipboard(owner)) { Thread.Sleep(20); continue; } bool done = true; try { if (ClipGetClipboardSequenceNumber() != expectedSequence) return false; if (started) { if (ClipGetClipboardOwner() != owner) return false; } else { if (!ClipboardOwnerIsTarget() || !ClipEmptyClipboard()) return false; started = true; } foreach (ClipboardEntry entry in entries) { if (entry.RestoreHandle == IntPtr.Zero) continue; if (ClipSetClipboardData(entry.Format, entry.RestoreHandle) != IntPtr.Zero) entry.RestoreHandle = IntPtr.Zero; // Ownership transferred to Windows. else done = false; } expectedSequence = ClipGetClipboardSequenceNumber(); } finally { ClipCloseClipboard(); } if (done) return true; Thread.Sleep(20); } while (watch.ElapsedMilliseconds < 450); return false; } private static void FreeSnapshot(List entries) { foreach (ClipboardEntry entry in entries) { if (entry.RestoreHandle != IntPtr.Zero) ClipGlobalFree(entry.RestoreHandle); if (entry.Bytes != null) Array.Clear(entry.Bytes, 0, entry.Bytes.Length); } entries.Clear(); } private bool SendCopy(out bool issued) { issued = false; if (cancelled || !ShortcutKeysReleased() || !IsTargetForeground()) return false; ClipInput[] inputs = new ClipInput[] { Key(0x11, false), Key(0x43, false), Key(0x43, true), Key(0x11, true) }; uint sent = ClipSendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(ClipInput))); issued = sent >= 2; if (sent == (uint)inputs.Length) return true; if (sent > 0) { ClipInput[] releases = new ClipInput[] { Key(0x43, true), Key(0x11, true) }; ClipSendInput((uint)releases.Length, releases, Marshal.SizeOf(typeof(ClipInput))); } return false; } private static ClipInput Key(ushort key, bool up) { ClipInput input = new ClipInput(); input.Type = 1; input.Data.Keyboard.VirtualKey = key; input.Data.Keyboard.Flags = up ? KeyUp : 0; return input; } [StructLayout(LayoutKind.Sequential)] private struct ClipInput { public uint Type; public ClipInputUnion Data; } [StructLayout(LayoutKind.Explicit)] private struct ClipInputUnion { [FieldOffset(0)] public ClipKeyboardInput Keyboard; [FieldOffset(0)] public ClipMouseInput Mouse; } [StructLayout(LayoutKind.Sequential)] private struct ClipKeyboardInput { public ushort VirtualKey, ScanCode; public uint Flags, Time; public UIntPtr ExtraInfo; } [StructLayout(LayoutKind.Sequential)] private struct ClipMouseInput { public int X, Y; public uint MouseData, Flags, Time; public UIntPtr ExtraInfo; } [DllImport("user32.dll", EntryPoint = "OpenClipboard", SetLastError = true)] private static extern bool ClipOpenClipboard(IntPtr owner); [DllImport("user32.dll", EntryPoint = "CloseClipboard", SetLastError = true)] private static extern bool ClipCloseClipboard(); [DllImport("user32.dll", EntryPoint = "EmptyClipboard", SetLastError = true)] private static extern bool ClipEmptyClipboard(); [DllImport("user32.dll", EntryPoint = "EnumClipboardFormats", SetLastError = true)] private static extern uint ClipEnumClipboardFormats(uint format); [DllImport("user32.dll", EntryPoint = "GetClipboardData", SetLastError = true)] private static extern IntPtr ClipGetClipboardData(uint format); [DllImport("user32.dll", EntryPoint = "SetClipboardData", SetLastError = true)] private static extern IntPtr ClipSetClipboardData(uint format, IntPtr data); [DllImport("user32.dll", EntryPoint = "GetClipboardSequenceNumber")] private static extern uint ClipGetClipboardSequenceNumber(); [DllImport("user32.dll", EntryPoint = "GetClipboardOwner")] private static extern IntPtr ClipGetClipboardOwner(); [DllImport("user32.dll", EntryPoint = "GetClipboardFormatNameW", CharSet = CharSet.Unicode)] private static extern int ClipGetClipboardFormatName(uint format, StringBuilder name, int count); [DllImport("user32.dll", EntryPoint = "IsClipboardFormatAvailable")] private static extern bool ClipIsClipboardFormatAvailable(uint format); [DllImport("user32.dll", EntryPoint = "GetForegroundWindow")] private static extern IntPtr ClipGetForegroundWindow(); [DllImport("user32.dll", EntryPoint = "GetWindowThreadProcessId")] private static extern uint ClipGetWindowThreadProcessId(IntPtr window, out uint process); [DllImport("user32.dll", EntryPoint = "IsWindow")] private static extern bool ClipIsWindow(IntPtr window); [DllImport("user32.dll", EntryPoint = "GetAsyncKeyState")] private static extern short ClipGetAsyncKeyState(int key); [DllImport("user32.dll", EntryPoint = "SendInput", SetLastError = true)] private static extern uint ClipSendInput(uint count, ClipInput[] inputs, int size); [DllImport("kernel32.dll", EntryPoint = "GlobalAlloc", SetLastError = true)] private static extern IntPtr ClipGlobalAlloc(uint flags, UIntPtr size); [DllImport("kernel32.dll", EntryPoint = "GlobalFree", SetLastError = true)] private static extern IntPtr ClipGlobalFree(IntPtr data); [DllImport("kernel32.dll", EntryPoint = "GlobalLock", SetLastError = true)] private static extern IntPtr ClipGlobalLock(IntPtr data); [DllImport("kernel32.dll", EntryPoint = "GlobalUnlock", SetLastError = true)] private static extern bool ClipGlobalUnlock(IntPtr data); [DllImport("kernel32.dll", EntryPoint = "GlobalSize", SetLastError = true)] private static extern UIntPtr ClipGlobalSize(IntPtr data); [DllImport("kernel32.dll", EntryPoint = "SetLastError")] private static extern void ClipSetLastError(uint error); } } namespace QaHelper { public sealed class LoginPanel : System.Windows.Forms.Form { protected override bool ShowWithoutActivation { get { return true; } } protected override System.Windows.Forms.CreateParams CreateParams { get { var p = base.CreateParams; p.ExStyle |= 0x80; return p; } } } public sealed class ApiReply { public string Model = "", RateLimits = "", UserId = "", Username = "", RequestId = "", SessionId = "", DeviceId = "", ServerConfigJson = ""; public long InputTokens, OutputTokens, CachedTokens, LatencyMs; public decimal CostUsd; public bool UsageRecorded, CostKnown, ChargeUncertain; public bool Success; public int StatusCode; public string Error; public string Answer; public string Token; public string Type = "short_answer"; public string Details = ""; public string Language = ""; public string Code = ""; public string Question; public int OptionIndex; public double Confidence = -1; public AutoBox Box; public AutoPoint Target; public bool HasInvalidAutoMetadata; } public sealed class AutoBox { public double X1, Y1, X2, Y2; } public sealed class AutoPoint { public double X, Y; } public sealed class AutoPlacement { public bool Valid; public string Message; public int X, Y; } public static class AutoAnswerGeometry { public static AutoPlacement Resolve(ApiReply reply, int imageWidth, int imageHeight, double minimumConfidence, int markerOffset) { AutoPlacement unsure = new AutoPlacement { Message = "Not sure" }; if (reply == null || !reply.Success || reply.HasInvalidAutoMetadata || reply.OptionIndex < 1 || imageWidth < 20 || imageHeight < 20 || markerOffset < 0 || !Finite(minimumConfidence) || minimumConfidence < 0 || minimumConfidence > 1 || !Finite(reply.Confidence) || reply.Confidence < 0 || reply.Confidence > 1 || reply.Confidence < minimumConfidence) return unsure; double x, y; if (reply.Box != null) { AutoBox box = reply.Box; if (!Finite(box.X1) || !Finite(box.Y1) || !Finite(box.X2) || !Finite(box.Y2) || box.X1 < 0 || box.Y1 < 0 || box.X2 <= box.X1 || box.Y2 <= box.Y1 || box.X2 > imageWidth || box.Y2 > imageHeight) return unsure; x = box.X1 - markerOffset; y = box.Y1 + (box.Y2 - box.Y1) / 2; } else if (reply.Target != null) { if (!Finite(reply.Target.X) || !Finite(reply.Target.Y) || reply.Target.X < 0 || reply.Target.Y < 0 || reply.Target.X >= imageWidth || reply.Target.Y >= imageHeight) return unsure; x = reply.Target.X; y = reply.Target.Y; } else return unsure; return new AutoPlacement { Valid = true, Message = "", X = (int)System.Math.Round(System.Math.Max(9, System.Math.Min(imageWidth - 10, x))), Y = (int)System.Math.Round(System.Math.Max(9, System.Math.Min(imageHeight - 10, y))) }; } private static bool Finite(double value) { return !System.Double.IsNaN(value) && !System.Double.IsInfinity(value); } } public sealed class ApiClient : System.IDisposable { private readonly System.Net.Http.HttpClient client; private readonly System.Uri authUri, textUri, imageUri; private readonly int timeoutSeconds, maxResponseBytes; private readonly System.Threading.CancellationTokenSource shutdown = new System.Threading.CancellationTokenSource(); private readonly object cancellationGate = new object(); private System.Threading.CancellationTokenSource generation = new System.Threading.CancellationTokenSource(); private volatile bool disposed; private System.Threading.CancellationTokenSource CreateRequestCancellation() { lock (cancellationGate) { if (disposed) return null; return System.Threading.CancellationTokenSource.CreateLinkedTokenSource(shutdown.Token, generation.Token); } } public void Cancel() { System.Threading.CancellationTokenSource previous; lock (cancellationGate) { if (disposed) return; previous = generation; generation = new System.Threading.CancellationTokenSource(); } try { previous.Cancel(); } catch (System.AggregateException) { } finally { previous.Dispose(); } } private const string AdaptiveInstruction = " The server must classify the request and return a trusted type: choice, short_answer, code, explanation, or unknown. Return JSON with success, type, answer, and optional details, language, and code. Keep answer concise; put extended explanation in details. For code requests provide complete code without Markdown fences, language, and only a short optional answer. Do not classify every request as a multiple-choice question. If unsure return unknown rather than guessing."; private const string QuickInstruction = "Answer directly. Do not repeat the question. Put the option or result first; ordinary answers need at most 1-3 short sentences." + AdaptiveInstruction; private const string VisualInstruction = "The red point marks object A; the blue point marks object B. Analyze the marked objects directly. Coordinates are absolute physical desktop pixels; subtract screenOrigin to locate them in the image." + AdaptiveInstruction; private const string RegionInstruction = "Read the question in the selected screenshot region and answer directly. Do not repeat the question. Put the option or result first." + AdaptiveInstruction; private const string AutoInstruction = "Find the visible question in this screenshot and answer according to its task type. For type choice return question, answer, option_index (1-based), confidence (0 to 1), and box {x1,y1,x2,y2} tightly surrounding the correct option. A target {x,y} may replace box if a box is unavailable. All box and target coordinates must be local pixels of the actual sent PNG; its width and height are screenWidth and screenHeight. Use low confidence and null geometry when unsure. Do not guess a location." + AdaptiveInstruction; public ApiClient(string baseUrl, string auth, string text, string image, int timeout, int responseLimit) : this(baseUrl, auth, text, image, timeout, responseLimit, "1.0.0") { } public ApiClient(string baseUrl, string auth, string text, string image, int timeout, int responseLimit, string clientVersion) { var root = new System.Uri(baseUrl.TrimEnd('/') + "/", System.UriKind.Absolute); ValidateUri(root); authUri = ResolveEndpoint(root, auth); textUri = ResolveEndpoint(root, text); imageUri = ResolveEndpoint(root, image); timeoutSeconds = timeout; maxResponseBytes = responseLimit; if (System.String.IsNullOrWhiteSpace(clientVersion) || clientVersion.Length > 64) throw new System.ArgumentException("Invalid client version."); foreach (char c in clientVersion) if (!(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') && !(c >= '0' && c <= '9') && c != '.' && c != '-' && c != '_') throw new System.ArgumentException("Invalid client version."); var handler = new System.Net.Http.HttpClientHandler { AllowAutoRedirect = false, UseCookies = false }; client = new System.Net.Http.HttpClient(handler); client.Timeout = System.Threading.Timeout.InfiniteTimeSpan; client.DefaultRequestHeaders.Add("X-QA-Version", clientVersion); } public static void ValidateUri(System.Uri uri) { if (uri.Scheme != "https" && !(uri.Scheme == "http" && (uri.Host == "localhost" || uri.Host == "127.0.0.1" || uri.Host == "::1"))) throw new System.ArgumentException("API URL must use HTTPS (HTTP is allowed only for exact loopback hosts in tests)."); if (!System.String.IsNullOrEmpty(uri.UserInfo) || !System.String.IsNullOrEmpty(uri.Fragment)) throw new System.ArgumentException("API URL must not contain credentials or a fragment."); } private static System.Uri ResolveEndpoint(System.Uri root, string endpoint) { var uri = new System.Uri(root, endpoint); ValidateUri(uri); if (!System.String.Equals(uri.Scheme, root.Scheme, System.StringComparison.OrdinalIgnoreCase) || !System.String.Equals(uri.Host, root.Host, System.StringComparison.OrdinalIgnoreCase) || uri.Port != root.Port) throw new System.ArgumentException("All endpoints must use the same API origin."); return uri; } public System.Threading.Tasks.Task LoginAsync(string login, string password) { return LoginAsync(login, password, null); } public System.Threading.Tasks.Task LoginAsync(string login, string password, object device) { var requestCancellation = CreateRequestCancellation(); return System.Threading.Tasks.Task.Run(async delegate { using (requestCancellation) { if (requestCancellation == null || requestCancellation.IsCancellationRequested) return new ApiReply { Error = "Cancelled" }; var payload = new System.Collections.Generic.Dictionary { { "login", login }, { "password", password } }; if (device != null) payload.Add("device", device); string json = Serialize(payload); payload.Clear(); password = null; return await SendAsync(authUri, json, null, true, requestCancellation).ConfigureAwait(false); } }); } public System.Threading.Tasks.Task AnalyzeTextAsync(string text, string token) { var requestCancellation = CreateRequestCancellation(); return System.Threading.Tasks.Task.Run(async delegate { using (requestCancellation) { if (requestCancellation == null || requestCancellation.IsCancellationRequested) return new ApiReply { Error = "Cancelled" }; return await SendAsync(textUri, Serialize(new { text = text, mode = "quick", instruction = QuickInstruction }), token, false, requestCancellation).ConfigureAwait(false); } }); } public System.Threading.Tasks.Task AnalyzeImageAsync(byte[] png, System.Drawing.Rectangle bounds, System.Drawing.Point red, System.Drawing.Point blue, string token) { return AnalyzeImageAsync(png, bounds, bounds.Width, bounds.Height, red, blue, token, "visual"); } public System.Threading.Tasks.Task AnalyzeImageAsync(byte[] png, System.Drawing.Rectangle bounds, System.Drawing.Point? red, System.Drawing.Point? blue, string token, string mode) { return AnalyzeImageAsync(png, bounds, bounds.Width, bounds.Height, red, blue, token, mode); } public System.Threading.Tasks.Task AnalyzeImageAsync(byte[] png, System.Drawing.Rectangle bounds, int imageWidth, int imageHeight, System.Drawing.Point? red, System.Drawing.Point? blue, string token, string mode) { var requestCancellation = CreateRequestCancellation(); return System.Threading.Tasks.Task.Run(async delegate { try { if (requestCancellation == null || requestCancellation.IsCancellationRequested) return new ApiReply { Error = "Cancelled" }; if (png == null || png.Length == 0 || imageWidth <= 0 || imageHeight <= 0 || bounds.Width <= 0 || bounds.Height <= 0) return new ApiReply { Error = "Invalid image" }; if (mode != "visual" && mode != "region" && mode != "auto_answer") return new ApiReply { Error = "Invalid image mode" }; if (mode == "visual" && (!red.HasValue || !blue.HasValue)) return new ApiReply { Error = "Place RED and BLUE first" }; var payload = new System.Collections.Generic.Dictionary { { "mode", mode }, { "imageBase64", System.Convert.ToBase64String(png) }, { "mimeType", "image/png" }, { "screenWidth", imageWidth }, { "screenHeight", imageHeight }, { "captureWidth", bounds.Width }, { "captureHeight", bounds.Height }, { "screenOrigin", new { x = bounds.X, y = bounds.Y } }, { "instruction", mode == "auto_answer" ? AutoInstruction : (mode == "region" ? RegionInstruction : VisualInstruction + " If the PNG is resized, multiply the origin-relative X by screenWidth/captureWidth and Y by screenHeight/captureHeight.") } }; if (mode == "visual") { payload.Add("red", new { x = red.Value.X, y = red.Value.Y }); payload.Add("blue", new { x = blue.Value.X, y = blue.Value.Y }); } return await SendAsync(imageUri, Serialize(payload), token, false, requestCancellation).ConfigureAwait(false); } finally { if (png != null) System.Array.Clear(png, 0, png.Length); if (requestCancellation != null) requestCancellation.Dispose(); } }); } private static string Serialize(object value) { var serializer = new System.Web.Script.Serialization.JavaScriptSerializer { MaxJsonLength = 268435456, RecursionLimit = 32 }; return serializer.Serialize(value); } private static bool ValidToken(string token) { if (System.String.IsNullOrWhiteSpace(token) || token.Length > 8192) return false; foreach (char c in token) if (char.IsControl(c) || char.IsWhiteSpace(c)) return false; return true; } private async System.Threading.Tasks.Task SendAsync(System.Uri uri, string json, string token, bool login, System.Threading.CancellationTokenSource requestCancellation) { int bodyTimedOut = 0; int manuallyCancelled = 0; try { if (requestCancellation == null || requestCancellation.IsCancellationRequested) return new ApiReply { Error = "Cancelled" }; using (requestCancellation.Token.Register(delegate { System.Threading.Interlocked.Exchange(ref manuallyCancelled, 1); })) { using (var timeout = System.Threading.CancellationTokenSource.CreateLinkedTokenSource(requestCancellation.Token)) using (var request = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, uri)) { timeout.CancelAfter(System.TimeSpan.FromSeconds(timeoutSeconds)); request.Content = new System.Net.Http.StringContent(json, System.Text.Encoding.UTF8, "application/json"); json = null; if (!System.String.IsNullOrEmpty(token)) { if (!ValidToken(token)) return new ApiReply { Error = "Invalid access token" }; request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); } using (var response = await client.SendAsync(request, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, timeout.Token).ConfigureAwait(false)) using (timeout.Token.Register(delegate { if (!requestCancellation.IsCancellationRequested) System.Threading.Interlocked.Exchange(ref bodyTimedOut, 1); response.Dispose(); })) { int status = (int)response.StatusCode; if (!response.IsSuccessStatusCode) return new ApiReply { StatusCode = status, Error = MapHttpError(status, login) }; if (response.Content.Headers.ContentLength.HasValue && response.Content.Headers.ContentLength.Value > maxResponseBytes) return new ApiReply { StatusCode = status, Error = "Response too large" }; using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) using (var output = new System.IO.MemoryStream()) { byte[] buffer = new byte[4096]; int count; while ((count = await stream.ReadAsync(buffer, 0, buffer.Length, timeout.Token).ConfigureAwait(false)) != 0) { if (output.Length + count > maxResponseBytes) return new ApiReply { StatusCode = status, Error = "Response too large" }; output.Write(buffer, 0, count); } string body = new System.Text.UTF8Encoding(false, true).GetString(output.ToArray()).TrimStart('\uFEFF'); return ParseResponse(body, status, login); } } } } } catch (System.OperationCanceledException) { return new ApiReply { Error = disposed || manuallyCancelled != 0 ? "Cancelled" : "Request timed out" }; } catch (System.Net.Http.HttpRequestException) { return new ApiReply { Error = disposed || manuallyCancelled != 0 ? "Cancelled" : bodyTimedOut != 0 ? "Request timed out" : "No connection" }; } catch (System.IO.IOException) { return new ApiReply { Error = disposed || manuallyCancelled != 0 ? "Cancelled" : bodyTimedOut != 0 ? "Request timed out" : "No connection" }; } catch (System.ObjectDisposedException) { return new ApiReply { Error = bodyTimedOut != 0 && manuallyCancelled == 0 ? "Request timed out" : "Cancelled" }; } catch (System.ArgumentException) { return new ApiReply { Error = "Invalid response" }; } catch (System.InvalidOperationException) { return new ApiReply { Error = "Invalid response" }; } } private ApiReply ParseResponse(string body, int status, bool login) { var parser = new System.Web.Script.Serialization.JavaScriptSerializer { MaxJsonLength = maxResponseBytes, RecursionLimit = 16 }; var data = parser.DeserializeObject(body) as System.Collections.Generic.Dictionary; object value; if (data == null) return new ApiReply { StatusCode = status, Error = "Invalid response" }; if (data.TryGetValue("success", out value) && (!(value is bool) || !(bool)value)) return new ApiReply { StatusCode = status, Error = "Invalid response" }; if (login) { if (!data.TryGetValue("access_token", out value)) data.TryGetValue("token", out value); string access = value as string; if (!ValidToken(access)) return new ApiReply { StatusCode = status, Error = "Invalid response" }; var authenticated = new ApiReply { Success = true, StatusCode = status, Token = access }; if (data.TryGetValue("user", out value)) { var user = value as System.Collections.Generic.Dictionary; object field; if (user != null && user.TryGetValue("id", out field) && field is string) authenticated.UserId = (string)field; if (user != null && user.TryGetValue("username", out field) && field is string) authenticated.Username = (string)field; } ReplyUsage.Apply(authenticated, data); return authenticated; } string type = "short_answer"; if (data.TryGetValue("type", out value)) { type = value as string; if (type != "choice" && type != "short_answer" && type != "code" && type != "explanation" && type != "unknown") return new ApiReply { StatusCode = status, Error = "Invalid response" }; } string answer, details, language, code, question; if (!OptionalString(data, "answer", out answer) || !OptionalString(data, "details", out details) || !OptionalString(data, "language", out language) || !OptionalString(data, "code", out code) || !OptionalString(data, "question", out question)) return new ApiReply { StatusCode = status, Error = "Invalid response" }; if (language.Length > 64 || code.Length > 32000) return new ApiReply { StatusCode = status, Error = "Response too large" }; if (type == "code") { if (System.String.IsNullOrWhiteSpace(code) || code.IndexOf('\0') >= 0) return new ApiReply { StatusCode = status, Error = "Invalid response" }; } else if (type == "unknown" && System.String.IsNullOrWhiteSpace(answer)) answer = "Not sure"; else if (System.String.IsNullOrWhiteSpace(answer)) return new ApiReply { StatusCode = status, Error = "Invalid response" }; ApiReply reply = new ApiReply { Success = true, StatusCode = status, Type = type, Answer = LimitText(answer.Trim(), 1200), Details = LimitText(details, 12000), Language = language.Trim(), Code = code, Question = LimitText(question, 1200) }; double number; if (data.TryGetValue("option_index", out value)) { if (TryNumber(value, out number) && number >= 1 && number <= System.Int32.MaxValue && number == System.Math.Floor(number)) reply.OptionIndex = (int)number; else reply.HasInvalidAutoMetadata = true; } if (data.TryGetValue("confidence", out value)) { if (TryNumber(value, out number) && number >= 0 && number <= 1) reply.Confidence = number; else reply.HasInvalidAutoMetadata = true; } if (data.TryGetValue("box", out value) && value != null) { var box = value as System.Collections.Generic.Dictionary; double x1, y1, x2, y2; if (Coordinate(box, "x1", out x1) && Coordinate(box, "y1", out y1) && Coordinate(box, "x2", out x2) && Coordinate(box, "y2", out y2)) reply.Box = new AutoBox { X1 = x1, Y1 = y1, X2 = x2, Y2 = y2 }; else reply.HasInvalidAutoMetadata = true; } if (data.TryGetValue("target", out value) && value != null) { var target = value as System.Collections.Generic.Dictionary; double x, y; if (Coordinate(target, "x", out x) && Coordinate(target, "y", out y)) reply.Target = new AutoPoint { X = x, Y = y }; else reply.HasInvalidAutoMetadata = true; } ReplyUsage.Apply(reply, data); return reply; } private static bool OptionalString(System.Collections.Generic.Dictionary data, string name, out string text) { text = ""; object value; if (!data.TryGetValue(name, out value) || value == null) return true; text = value as string; return text != null; } private static string LimitText(string value, int limit) { return value.Length <= limit ? value : value.Substring(0, limit - 1) + "\u2026"; } private static bool Coordinate(System.Collections.Generic.Dictionary data, string name, out double number) { number = 0; object value; return data != null && data.TryGetValue(name, out value) && TryNumber(value, out number); } private static bool TryNumber(object value, out double number) { number = 0; if (!(value is int) && !(value is long) && !(value is double) && !(value is decimal) && !(value is float)) return false; number = System.Convert.ToDouble(value, System.Globalization.CultureInfo.InvariantCulture); return !System.Double.IsNaN(number) && !System.Double.IsInfinity(number); } public static string MapHttpError(int status, bool login) { if (login && (status == 400 || status == 401 || status == 403)) return "Invalid credentials"; if (status == 401) return "Session expired"; if (status == 403) return "Access denied"; if (status == 429) return "Too many requests"; if (status >= 500) return "Server error"; return "Request failed (" + status + ")"; } public void Dispose() { System.Threading.CancellationTokenSource previous; lock (cancellationGate) { if (disposed) return; disposed = true; previous = generation; generation = null; } try { shutdown.Cancel(); } catch (System.AggregateException) { } try { previous.Cancel(); } catch (System.AggregateException) { } finally { previous.Dispose(); client.Dispose(); shutdown.Dispose(); } } } public sealed class ClipboardOperationResult { public bool Success; public string Error; } public static class ClipboardTools { private static readonly object operationGate = new object(); private static long generation; public static void CancelPending() { // The write itself holds this gate, so cancellation returns only // after any write already underway has completed. lock (operationGate) { unchecked { generation++; } } } private sealed class ClipboardWindow : System.Windows.Forms.NativeWindow, System.IDisposable { public ClipboardWindow() { var parameters = new System.Windows.Forms.CreateParams(); parameters.Caption = "QA explicit clipboard action"; parameters.Parent = new System.IntPtr(-3); CreateHandle(parameters); } public void Dispose() { DestroyHandle(); } } public static System.Threading.Tasks.Task ClearCurrentAsync() { // Take this before queuing the worker; a newer user copy must survive startup cleanup. uint expectedSequence = GetClipboardSequenceNumber(); return BeginOperation(null, true, expectedSequence); } public static System.Threading.Tasks.Task CopyCodeAsync(string code) { if (System.String.IsNullOrWhiteSpace(code) || code.Length > 32000 || code.IndexOf('\0') >= 0) return System.Threading.Tasks.Task.FromResult(new ClipboardOperationResult { Error = "Cannot copy code" }); return BeginOperation(code, false, 0); } private static System.Threading.Tasks.Task BeginOperation(string code, bool clear, uint expectedSequence) { long expectedGeneration; lock (operationGate) expectedGeneration = generation; var completion = new System.Threading.Tasks.TaskCompletionSource(); var thread = new System.Threading.Thread(delegate() { ClipboardOperationResult result; try { result = Perform(code, clear, expectedSequence, expectedGeneration); } catch { result = new ClipboardOperationResult { Error = clear ? "Cannot clear clipboard" : "Cannot copy code" }; } code = null; completion.TrySetResult(result); }); thread.IsBackground = true; thread.Name = clear ? "QA clear current clipboard" : "QA copy generated code"; thread.SetApartmentState(System.Threading.ApartmentState.STA); thread.Start(); return completion.Task; } private static ClipboardOperationResult Perform(string code, bool clear, uint expectedSequence, long expectedGeneration) { System.IntPtr handle = System.IntPtr.Zero; byte[] bytes = null; ClipboardWindow window = null; bool opened = false; try { window = new ClipboardWindow(); if (!clear) { bytes = System.Text.Encoding.Unicode.GetBytes(code + "\0"); handle = GlobalAlloc(2, new System.UIntPtr((uint)bytes.Length)); if (handle == System.IntPtr.Zero) return new ClipboardOperationResult { Error = "Cannot copy code" }; System.IntPtr pointer = GlobalLock(handle); if (pointer == System.IntPtr.Zero) return new ClipboardOperationResult { Error = "Cannot copy code" }; try { System.Runtime.InteropServices.Marshal.Copy(bytes, 0, pointer, bytes.Length); } finally { GlobalUnlock(handle); } } var watch = System.Diagnostics.Stopwatch.StartNew(); do { if (OpenClipboard(window.Handle)) { opened = true; break; } System.Threading.Thread.Sleep(15); } while (watch.ElapsedMilliseconds < 220); if (!opened) return new ClipboardOperationResult { Error = "Clipboard busy; try again" }; lock (operationGate) { if (expectedGeneration != generation) return new ClipboardOperationResult { Error = "Cancelled" }; if (clear && GetClipboardSequenceNumber() != expectedSequence) return new ClipboardOperationResult { Error = "Clipboard changed; kept current contents" }; if (!EmptyClipboard()) return new ClipboardOperationResult { Error = clear ? "Cannot clear clipboard" : "Cannot copy code" }; if (!clear) { if (SetClipboardData(13, handle) == System.IntPtr.Zero) return new ClipboardOperationResult { Error = "Cannot copy code" }; handle = System.IntPtr.Zero; } return new ClipboardOperationResult { Success = true }; } } finally { if (opened) CloseClipboard(); if (handle != System.IntPtr.Zero) GlobalFree(handle); if (bytes != null) System.Array.Clear(bytes, 0, bytes.Length); if (window != null) window.Dispose(); } } [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)] private static extern bool OpenClipboard(System.IntPtr owner); [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)] private static extern bool CloseClipboard(); [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)] private static extern bool EmptyClipboard(); [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)] private static extern System.IntPtr SetClipboardData(uint format, System.IntPtr data); [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern uint GetClipboardSequenceNumber(); [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] private static extern System.IntPtr GlobalAlloc(uint flags, System.UIntPtr size); [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] private static extern System.IntPtr GlobalFree(System.IntPtr memory); [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] private static extern System.IntPtr GlobalLock(System.IntPtr memory); [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] private static extern bool GlobalUnlock(System.IntPtr memory); } } '@ $controlServicesSource = @' namespace QaHelper { using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Web.Script.Serialization; using System.Windows.Forms; public sealed class UserSettings { public string QuickHotkey { get; set; } public string VisualHotkey { get; set; } public string AutoHotkey { get; set; } public string DetailsHotkey { get; set; } public string CopyHotkey { get; set; } public string ControlHotkey { get; set; } public string RegionKey { get; set; } public int PopupSeconds { get; set; } public double Opacity { get; set; } public double FontSize { get; set; } public int LockMinutes { get; set; } public int ExitMinutes { get; set; } public bool ClearClipboard { get; set; } public bool AnalyzeOnRelease { get; set; } public bool RememberApiKey { get; set; } public string ApiMode { get; set; } public string Model { get; set; } public string ResponseStyle { get; set; } public decimal SessionLimitUsd { get; set; } public decimal DailyLimitUsd { get; set; } public int MaxRequestsPerMinute { get; set; } public decimal InputUsdPerMillion { get; set; } public decimal OutputUsdPerMillion { get; set; } public decimal CachedUsdPerMillion { get; set; } public UserSettings() { QuickHotkey = "Alt+Q"; VisualHotkey = "Alt+S"; AutoHotkey = "Alt+A"; DetailsHotkey = "Alt+D"; CopyHotkey = "Alt+C"; ControlHotkey = "Ctrl+Alt+P"; RegionKey = "F2"; PopupSeconds = 12; Opacity = 0.96; FontSize = 9; LockMinutes = 15; ExitMinutes = 60; ClearClipboard = true; AnalyzeOnRelease = false; RememberApiKey = false; ApiMode = "server"; Model = "gpt-4.1-mini"; ResponseStyle = "Very short"; SessionLimitUsd = 1; DailyLimitUsd = 3; MaxRequestsPerMinute = 10; InputUsdPerMillion = 0.4m; OutputUsdPerMillion = 1.6m; CachedUsdPerMillion = 0.1m; } public UserSettings Clone() { return (UserSettings)MemberwiseClone(); } public void Validate() { SettingsValidation.Range(PopupSeconds, 1, 120, "PopupSeconds"); SettingsValidation.Range(Opacity, 0.2, 1, "Opacity"); SettingsValidation.Range(FontSize, 6, 32, "FontSize"); SettingsValidation.Range(LockMinutes, 0, 10080, "LockMinutes"); SettingsValidation.Range(ExitMinutes, 0, 10080, "ExitMinutes"); if (LockMinutes > 0 && ExitMinutes > 0 && LockMinutes > ExitMinutes) throw new ArgumentException("Lock time must be no later than exit time.", "LockMinutes"); SettingsValidation.Range(SessionLimitUsd, 0, 100000, "SessionLimitUsd"); SettingsValidation.Range(DailyLimitUsd, 0, 100000, "DailyLimitUsd"); SettingsValidation.Range(MaxRequestsPerMinute, 1, 120, "MaxRequestsPerMinute"); SettingsValidation.Range(InputUsdPerMillion, 0, 100000, "InputUsdPerMillion"); SettingsValidation.Range(OutputUsdPerMillion, 0, 100000, "OutputUsdPerMillion"); SettingsValidation.Range(CachedUsdPerMillion, 0, 100000, "CachedUsdPerMillion"); if (!String.Equals(ApiMode, "server", StringComparison.OrdinalIgnoreCase) && !String.Equals(ApiMode, "openai", StringComparison.OrdinalIgnoreCase)) throw new ArgumentException("API mode must be server or openai.", "ApiMode"); if (String.IsNullOrWhiteSpace(Model) || Model.Length > 100 || !Regex.IsMatch(Model, "^[A-Za-z0-9._:-]+$")) throw new ArgumentException("Enter a valid model name.", "Model"); if (!new[] { "Very short", "Short", "Normal" }.Contains(ResponseStyle, StringComparer.OrdinalIgnoreCase)) throw new ArgumentException("Choose Very short, Short, or Normal.", "ResponseStyle"); int region = SettingsValidation.Key(RegionKey, "RegionKey"); if (region == (int)Keys.Return) throw new ArgumentException("Region selection key cannot be Enter.", "RegionKey"); HashSet bindings = new HashSet(StringComparer.Ordinal); string[] names = { "QuickHotkey", "VisualHotkey", "AutoHotkey", "DetailsHotkey", "CopyHotkey", "ControlHotkey" }; string[] values = { QuickHotkey, VisualHotkey, AutoHotkey, DetailsHotkey, CopyHotkey, ControlHotkey }; for (int index = 0; index < values.Length; index++) { int key; string binding = SettingsValidation.Binding(values[index], names[index], out key); if (!bindings.Add(binding)) throw new ArgumentException("Hotkeys must be different.", names[index]); if (key == region) throw new ArgumentException("A hotkey conflicts with the held region key.", names[index]); } } } internal static class SettingsValidation { internal static void Range(double value, double minimum, double maximum, string name) { if (Double.IsNaN(value) || Double.IsInfinity(value) || value < minimum || value > maximum) throw new ArgumentOutOfRangeException(name, name + " is outside its allowed range."); } internal static void Range(decimal value, decimal minimum, decimal maximum, string name) { if (value < minimum || value > maximum) throw new ArgumentOutOfRangeException(name, name + " is outside its allowed range."); } internal static void Range(int value, int minimum, int maximum, string name) { if (value < minimum || value > maximum) throw new ArgumentOutOfRangeException(name, name + " is outside its allowed range."); } internal static int Key(string name, string setting) { string text = (name ?? String.Empty).Trim(); if (text.Length == 1 && Char.IsDigit(text[0])) text = "D" + text; Keys value; if (!Regex.IsMatch(text, "^[A-Za-z][A-Za-z0-9]*$") || !Enum.TryParse(text, true, out value) || !Enum.IsDefined(typeof(Keys), value)) throw new ArgumentException("Enter a keyboard key such as Q or F2.", setting); int key = (int)value; if (key < 8 || key > 254 || key == (int)Keys.Escape || key == (int)Keys.ControlKey || key == (int)Keys.ShiftKey || key == (int)Keys.Menu || key == (int)Keys.LControlKey || key == (int)Keys.RControlKey || key == (int)Keys.LShiftKey || key == (int)Keys.RShiftKey || key == (int)Keys.LMenu || key == (int)Keys.RMenu || key == (int)Keys.LWin || key == (int)Keys.RWin || key == (int)Keys.Apps) throw new ArgumentException("Choose a non-modifier keyboard key other than Esc.", setting); return key; } internal static string Binding(string value, string setting, out int key) { string[] parts = (value ?? String.Empty).Split('+'); int modifiers = 0; if (parts.Length == 0) throw new ArgumentException("Enter a hotkey.", setting); for (int index = 0; index < parts.Length - 1; index++) { string part = parts[index].Trim().ToLowerInvariant(); int modifier = part == "alt" ? 1 : (part == "ctrl" || part == "control") ? 2 : part == "shift" ? 4 : (part == "win" || part == "windows") ? 8 : 0; if (modifier == 0 || (modifiers & modifier) != 0) throw new ArgumentException("Use each modifier only once: Ctrl, Alt, Shift, Win.", setting); modifiers |= modifier; } key = Key(parts[parts.Length - 1], setting); if ((modifiers == 1 && (key == (int)Keys.Tab || key == (int)Keys.F4)) || (modifiers == 3 && key == (int)Keys.Delete) || (modifiers == 8 && key == (int)Keys.L)) throw new ArgumentException("This shortcut is reserved by Windows.", setting); return modifiers.ToString(CultureInfo.InvariantCulture) + ":" + key.ToString(CultureInfo.InvariantCulture); } } internal static class ServiceStorage { internal static JavaScriptSerializer Json { get { return new JavaScriptSerializer { MaxJsonLength = 1048576, RecursionLimit = 24 }; } } internal static string DirectoryPath(string directory) { if (String.IsNullOrWhiteSpace(directory)) throw new ArgumentException("A settings directory is required.", "directory"); return Path.GetFullPath(directory); } internal static DateTime Utc(DateTime value) { return value.Kind == DateTimeKind.Local ? value.ToUniversalTime() : DateTime.SpecifyKind(value, DateTimeKind.Utc); } internal static void WriteAtomic(string path, byte[] bytes) { Directory.CreateDirectory(Path.GetDirectoryName(path)); string temporary = path + "." + Guid.NewGuid().ToString("N") + ".tmp"; try { using (FileStream stream = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)) { stream.Write(bytes, 0, bytes.Length); stream.Flush(true); } if (File.Exists(path)) File.Replace(temporary, path, null, true); else File.Move(temporary, path); } finally { if (File.Exists(temporary)) File.Delete(temporary); } } internal static void WriteJson(string path, object value) { WriteAtomic(path, new UTF8Encoding(false).GetBytes(Json.Serialize(value))); } } public sealed class SettingsManager { private readonly object sync = new object(); private readonly string settingsPath; private readonly string keyPath; private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("QuickAssistant.Settings.ApiKey.v1"); private UserSettings settings = new UserSettings(); private string apiKey = String.Empty; public UserSettings Settings { get { lock (sync) return settings; } } public string ApiKey { get { lock (sync) return apiKey; } } public string LastError { get; private set; } public SettingsManager(string directory) { string path = ServiceStorage.DirectoryPath(directory); settingsPath = Path.Combine(path, "settings.json"); keyPath = Path.Combine(path, "api-key.dpapi"); } public void Load() { lock (sync) { settings = new UserSettings(); apiKey = String.Empty; LastError = null; try { if (File.Exists(settingsPath)) { if (new FileInfo(settingsPath).Length > 65536) throw new FormatException("Settings are too large."); UserSettings loaded = ServiceStorage.Json.Deserialize(File.ReadAllText(settingsPath, Encoding.UTF8)); if (loaded == null) throw new FormatException("Settings are empty."); loaded.Validate(); settings = loaded; } if (settings.RememberApiKey && File.Exists(keyPath)) { if (new FileInfo(keyPath).Length > 65536) throw new FormatException("Saved key is too large."); byte[] plain = ProtectedData.Unprotect(File.ReadAllBytes(keyPath), Entropy, DataProtectionScope.CurrentUser); try { apiKey = Encoding.UTF8.GetString(plain); } finally { Array.Clear(plain, 0, plain.Length); } } } catch (Exception exception) { if (!(exception is IOException || exception is UnauthorizedAccessException || exception is ArgumentException || exception is InvalidOperationException || exception is FormatException || exception is CryptographicException)) throw; apiKey = String.Empty; LastError = "Saved settings or API key could not be loaded. Check Settings and enter the key again."; } } } public void Save(UserSettings value, string key) { if (value == null) throw new ArgumentNullException("value"); UserSettings copy = value.Clone(); copy.Validate(); string normalizedKey = (key ?? String.Empty).Trim(); if (normalizedKey.Length > 8192 || normalizedKey.IndexOf('\r') >= 0 || normalizedKey.IndexOf('\n') >= 0) throw new ArgumentException("Enter a valid single-line API key.", "key"); lock (sync) { // A blank editor field means "keep the current key". Clearing the // session key first is the explicit way to remove this fallback. if (normalizedKey.Length == 0) normalizedKey = apiKey; byte[] protectedKey = null; if (copy.RememberApiKey && normalizedKey.Length > 0) { byte[] plain = Encoding.UTF8.GetBytes(normalizedKey); try { protectedKey = ProtectedData.Protect(plain, Entropy, DataProtectionScope.CurrentUser); } finally { Array.Clear(plain, 0, plain.Length); } } byte[] previousKey = File.Exists(keyPath) ? File.ReadAllBytes(keyPath) : null; try { if (protectedKey != null) ServiceStorage.WriteAtomic(keyPath, protectedKey); ServiceStorage.WriteJson(settingsPath, copy); } catch { // Restore the encrypted file if the settings replacement failed. if (protectedKey != null) { if (previousKey != null) ServiceStorage.WriteAtomic(keyPath, previousKey); else if (File.Exists(keyPath)) File.Delete(keyPath); } throw; } if (protectedKey == null && File.Exists(keyPath)) File.Delete(keyPath); settings = copy; apiKey = normalizedKey; LastError = null; } } public void ClearSessionKey() { lock (sync) apiKey = String.Empty; } public void SetSessionKey(string key) { string value = (key ?? String.Empty).Trim(); if (value.Length > 8192 || value.IndexOf('\r') >= 0 || value.IndexOf('\n') >= 0) throw new ArgumentException("Enter a valid single-line API key.", "key"); lock (sync) apiKey = value; } } public sealed class SessionManager { private readonly object sync = new object(); private bool locked; private DateTime lastActivityUtc; public bool IsLocked { get { lock (sync) return locked; } } public DateTime LastActivityUtc { get { lock (sync) return lastActivityUtc; } } public SessionManager() : this(DateTime.UtcNow) { } public SessionManager(DateTime utc) { lastActivityUtc = ServiceStorage.Utc(utc); } public void Touch(DateTime utc) { lock (sync) { DateTime time = ServiceStorage.Utc(utc); // Only explicit unlock can resume a locked session. Polling cannot extend it. if (!locked && time > lastActivityUtc) lastActivityUtc = time; } } public void Lock() { lock (sync) locked = true; } public void Unlock() { Unlock(DateTime.UtcNow); } public void Unlock(DateTime utc) { Reset(utc); } public void Reset() { Reset(DateTime.UtcNow); } public void Reset(DateTime utc) { lock (sync) { locked = false; lastActivityUtc = ServiceStorage.Utc(utc); } } public string Evaluate(DateTime utc, int lockMinutes, int exitMinutes) { if (lockMinutes < 0) throw new ArgumentOutOfRangeException("lockMinutes"); if (exitMinutes < 0) throw new ArgumentOutOfRangeException("exitMinutes"); lock (sync) { TimeSpan idle = ServiceStorage.Utc(utc) - lastActivityUtc; if (exitMinutes > 0 && idle.TotalMinutes >= exitMinutes) return "exit"; if (lockMinutes > 0 && idle.TotalMinutes >= lockMinutes) locked = true; return locked ? "lock" : "active"; } } } public sealed class HistoryEntry { public string Id { get; set; } public DateTime CreatedAtUtc { get; set; } public string Kind { get; set; } public string Question { get; set; } public string Answer { get; set; } public string Details { get; set; } public string Code { get; set; } public string Language { get; set; } public string Model { get; set; } public long InputTokens { get; set; } public long OutputTokens { get; set; } public decimal CostUsd { get; set; } public int LatencyMs { get; set; } public double Confidence { get; set; } public bool Pinned { get; set; } public HistoryEntry() { Id = Guid.NewGuid().ToString("N"); CreatedAtUtc = DateTime.UtcNow; } internal HistoryEntry Clone() { return (HistoryEntry)MemberwiseClone(); } } public sealed class HistoryManager { private const int Capacity = 100; private readonly object sync = new object(); private readonly List entries = new List(); public int Count { get { lock (sync) return entries.Count; } } public void Add(HistoryEntry entry) { if (entry == null) throw new ArgumentNullException("entry"); HistoryEntry copy = entry.Clone(); if (String.IsNullOrWhiteSpace(copy.Id)) copy.Id = Guid.NewGuid().ToString("N"); if (copy.CreatedAtUtc == DateTime.MinValue) copy.CreatedAtUtc = DateTime.UtcNow; copy.CreatedAtUtc = ServiceStorage.Utc(copy.CreatedAtUtc); // Bound text even if a malformed backend result bypasses the client limits. copy.Question = Limit(copy.Question, 16000); copy.Answer = Limit(copy.Answer, 8000); copy.Details = Limit(copy.Details, 12000); copy.Code = Limit(copy.Code, 32000); lock (sync) { entries.RemoveAll(delegate(HistoryEntry existing) { return String.Equals(existing.Id, copy.Id, StringComparison.Ordinal); }); entries.Add(copy); while (entries.Count > Capacity) { int removable = entries.FindIndex(delegate(HistoryEntry existing) { return !existing.Pinned; }); // Pinned entries are preferred, but cannot disable the RAM bound. entries.RemoveAt(removable >= 0 ? removable : 0); } } } private static string Limit(string value, int maximum) { return value == null ? String.Empty : value.Length <= maximum ? value : value.Substring(0, maximum); } public HistoryEntry[] Search(string query, string kind) { string text = (query ?? String.Empty).Trim(); string filter = (kind ?? String.Empty).Trim(); lock (sync) { return entries.Where(delegate(HistoryEntry entry) { return KindMatches(entry.Kind, filter) && (text.Length == 0 || Has(entry.Question, text) || Has(entry.Answer, text) || Has(entry.Details, text) || Has(entry.Code, text) || Has(entry.Language, text) || Has(entry.Model, text)); }).OrderByDescending(delegate(HistoryEntry entry) { return entry.CreatedAtUtc; }).ToArray(); } } private static bool Has(string value, string text) { return value != null && value.IndexOf(text, StringComparison.OrdinalIgnoreCase) >= 0; } private static bool KindMatches(string value, string filter) { if (filter.Length == 0 || String.Equals(filter, "All", StringComparison.OrdinalIgnoreCase)) return true; if (String.Equals(value, filter, StringComparison.OrdinalIgnoreCase)) return true; if (String.Equals(filter, "Tests", StringComparison.OrdinalIgnoreCase)) return String.Equals(value, "choice", StringComparison.OrdinalIgnoreCase); if (String.Equals(filter, "Questions", StringComparison.OrdinalIgnoreCase)) return String.Equals(value, "short_answer", StringComparison.OrdinalIgnoreCase) || String.Equals(value, "explanation", StringComparison.OrdinalIgnoreCase); return false; } public bool Delete(string id) { lock (sync) return entries.RemoveAll(delegate(HistoryEntry entry) { return String.Equals(entry.Id, id, StringComparison.Ordinal); }) > 0; } public void Clear() { lock (sync) { foreach (HistoryEntry entry in entries) { entry.Question = entry.Answer = entry.Details = entry.Code = String.Empty; } entries.Clear(); } } } public sealed class UsageSummary { public long Requests { get; internal set; } public long InputTokens { get; internal set; } public long OutputTokens { get; internal set; } public long TotalTokens { get { return InputTokens + OutputTokens; } } public decimal CostUsd { get; internal set; } public long UnpricedRequests { get; internal set; } public double AvgLatencyMs { get { return Requests == 0 ? 0 : (double)TotalLatencyMs / Requests; } } internal long TotalLatencyMs { get; set; } internal UsageSummary Clone() { return (UsageSummary)MemberwiseClone(); } } public sealed class UsageDayRecord { public string Date { get; set; } public long Requests { get; set; } public long InputTokens { get; set; } public long OutputTokens { get; set; } public decimal CostUsd { get; set; } public long UnpricedRequests { get; set; } public long TotalLatencyMs { get; set; } } public sealed class UsageStorageRecord { public int Version { get; set; } public int UtcOffsetMinutes { get; set; } public UsageDayRecord[] Days { get; set; } } public sealed class UsageTracker { private readonly object sync = new object(); private readonly string directory; private readonly int utcOffsetMinutes; private string scope = "local"; private string storagePath; private bool storageUnreadable; private UsageSummary session = new UsageSummary(); private readonly Dictionary days = new Dictionary(StringComparer.Ordinal); private readonly Queue requestStarts = new Queue(); public string PersistenceError { get; private set; } public string CalendarLabel { get { return utcOffsetMinutes == 300 ? "Local usage (UTC+5)" : "Local usage"; } } public UsageSummary Session { get { lock (sync) return session.Clone(); } } public UsageSummary Today { get { return GetToday(DateTime.UtcNow); } } public UsageSummary Month { get { return GetMonth(DateTime.UtcNow); } } public UsageTracker() : this(null, 300) { } public UsageTracker(string directory) : this(directory, 300) { } public UsageTracker(string directory, int utcOffsetMinutes) { if (utcOffsetMinutes < -840 || utcOffsetMinutes > 840) throw new ArgumentOutOfRangeException("utcOffsetMinutes"); this.utcOffsetMinutes = utcOffsetMinutes; this.directory = String.IsNullOrWhiteSpace(directory) ? null : ServiceStorage.DirectoryPath(directory); SetStoragePath(); Load(); } public void SetAccountScope(string id) { string next = "local"; if (!String.IsNullOrWhiteSpace(id) && !String.Equals(id.Trim(), "local", StringComparison.Ordinal)) { using (SHA256 hash = SHA256.Create()) next = BitConverter.ToString(hash.ComputeHash(Encoding.UTF8.GetBytes(id.Trim()))).Replace("-", String.Empty).ToLowerInvariant(); } lock (sync) { if (scope == next) return; scope = next; session = new UsageSummary(); days.Clear(); SetStoragePath(); Load(); } } private void SetStoragePath() { storagePath = directory == null ? null : Path.Combine(directory, "usage." + scope + ".json"); } private string DateKey(DateTime utc) { return ServiceStorage.Utc(utc).AddMinutes(utcOffsetMinutes).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } public UsageSummary GetToday(DateTime utc) { lock (sync) { UsageSummary value; return days.TryGetValue(DateKey(utc), out value) ? value.Clone() : new UsageSummary(); } } public UsageSummary GetMonth(DateTime utc) { string month = DateKey(utc).Substring(0, 7); lock (sync) { UsageSummary sum = new UsageSummary(); foreach (KeyValuePair day in days) if (day.Key.StartsWith(month, StringComparison.Ordinal)) Add(sum, day.Value); return sum; } } public void Record(long input, long output, decimal cost, int latency, DateTime utc) { RecordCore(input, output, cost, latency, utc, false); } public void RecordUnknown(long input, long output, int latency, DateTime utc) { RecordCore(input, output, 0, latency, utc, true); } private void RecordCore(long input, long output, decimal cost, int latency, DateTime utc, bool unpriced) { if (input < 0 || input > 1000000000) throw new ArgumentOutOfRangeException("input"); if (output < 0 || output > 1000000000) throw new ArgumentOutOfRangeException("output"); if (cost < 0 || cost > 1000000) throw new ArgumentOutOfRangeException("cost"); if (latency < 0) throw new ArgumentOutOfRangeException("latency"); string date = DateKey(utc); lock (sync) { UsageSummary record = new UsageSummary { Requests = 1, InputTokens = input, OutputTokens = output, CostUsd = cost, TotalLatencyMs = latency, UnpricedRequests = unpriced ? 1 : 0 }; UsageSummary day; if (!days.TryGetValue(date, out day)) { day = new UsageSummary(); days.Add(date, day); } Add(session, record); Add(day, record); Trim(); Persist(); } } private static void Add(UsageSummary destination, UsageSummary source) { checked { destination.Requests += source.Requests; destination.InputTokens += source.InputTokens; destination.OutputTokens += source.OutputTokens; destination.CostUsd += source.CostUsd; destination.UnpricedRequests += source.UnpricedRequests; destination.TotalLatencyMs += source.TotalLatencyMs; } } public void ResetSession() { lock (sync) session = new UsageSummary(); } public string CanRequest(DateTime utc, UserSettings settings) { if (settings == null) throw new ArgumentNullException("settings"); lock (sync) { if (PersistenceError != null) return PersistenceError; if (settings.SessionLimitUsd < 0 || settings.DailyLimitUsd < 0 || settings.MaxRequestsPerMinute < 1) return "Correct the spending and request limits in Settings."; if (settings.SessionLimitUsd > 0 && session.UnpricedRequests > 0) return "Cost unavailable for an earlier request. Review spending limits before continuing."; if (settings.SessionLimitUsd > 0 && session.CostUsd >= settings.SessionLimitUsd) return "Session app limit reached. Review the local spending limit in Settings."; UsageSummary day; if (settings.DailyLimitUsd > 0 && days.TryGetValue(DateKey(utc), out day) && day.UnpricedRequests > 0) return "Cost unavailable for an earlier request. Review spending limits before continuing."; if (settings.DailyLimitUsd > 0 && days.TryGetValue(DateKey(utc), out day) && day.CostUsd >= settings.DailyLimitUsd) return "Daily app limit reached. New requests are paused until the next local day."; PruneStarts(ServiceStorage.Utc(utc)); if (requestStarts.Count >= settings.MaxRequestsPerMinute) return "Requests per minute limit reached. Wait a moment and try again."; return null; } } public void BeginRequest(DateTime utc) { lock (sync) { DateTime time = ServiceStorage.Utc(utc); PruneStarts(time); // Include failed and cancelled attempts in the speed guard. requestStarts.Enqueue(time); } } private void PruneStarts(DateTime utc) { DateTime cutoff = utc.AddSeconds(-60); while (requestStarts.Count > 0 && requestStarts.Peek() <= cutoff) requestStarts.Dequeue(); } private void Trim() { while (days.Count > 400) days.Remove(days.Keys.OrderBy(delegate(string value) { return value; }, StringComparer.Ordinal).First()); } private void Load() { PersistenceError = null; storageUnreadable = false; if (storagePath == null || !File.Exists(storagePath)) return; try { if (new FileInfo(storagePath).Length > 262144) throw new FormatException("Usage file is too large."); UsageStorageRecord saved = ServiceStorage.Json.Deserialize(File.ReadAllText(storagePath, Encoding.UTF8)); if (saved == null || saved.Version != 1 || saved.UtcOffsetMinutes != utcOffsetMinutes || saved.Days == null || saved.Days.Length > 400) throw new FormatException("Usage format is invalid."); Dictionary loaded = new Dictionary(StringComparer.Ordinal); foreach (UsageDayRecord day in saved.Days) { DateTime parsed; if (day == null || !DateTime.TryParseExact(day.Date, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed) || day.Requests < 0 || day.Requests > 1000000000 || day.InputTokens < 0 || day.InputTokens > 1000000000000000 || day.OutputTokens < 0 || day.OutputTokens > 1000000000000000 || day.CostUsd < 0 || day.CostUsd > 1000000000000 || day.TotalLatencyMs < 0 || day.TotalLatencyMs > 1000000000000000 || day.UnpricedRequests < 0 || day.UnpricedRequests > day.Requests || day.InputTokens > Int64.MaxValue - day.OutputTokens || loaded.ContainsKey(day.Date)) throw new FormatException("Usage counters are invalid."); loaded.Add(day.Date, new UsageSummary { Requests = day.Requests, InputTokens = day.InputTokens, OutputTokens = day.OutputTokens, CostUsd = day.CostUsd, TotalLatencyMs = day.TotalLatencyMs, UnpricedRequests = day.UnpricedRequests }); } foreach (KeyValuePair day in loaded) days.Add(day.Key, day.Value); } catch (Exception exception) { if (!(exception is IOException || exception is UnauthorizedAccessException || exception is ArgumentException || exception is InvalidOperationException || exception is FormatException || exception is OverflowException)) throw; days.Clear(); storageUnreadable = true; PersistenceError = "Local usage storage could not be loaded. Restore access to the saved usage file before requesting again."; } } private void Persist() { // An in-flight response must not replace unknown totals after a load // failure. Recovery requires restoring the file and loading it again. if (storagePath == null || storageUnreadable) return; try { UsageStorageRecord saved = new UsageStorageRecord { Version = 1, UtcOffsetMinutes = utcOffsetMinutes, Days = days.OrderBy(delegate(KeyValuePair value) { return value.Key; }, StringComparer.Ordinal) .Select(delegate(KeyValuePair value) { return new UsageDayRecord { Date = value.Key, Requests = value.Value.Requests, InputTokens = value.Value.InputTokens, OutputTokens = value.Value.OutputTokens, CostUsd = value.Value.CostUsd, TotalLatencyMs = value.Value.TotalLatencyMs, UnpricedRequests = value.Value.UnpricedRequests }; }).ToArray() }; ServiceStorage.WriteJson(storagePath, saved); PersistenceError = null; } catch (Exception exception) { if (!(exception is IOException || exception is UnauthorizedAccessException)) throw; PersistenceError = "Local usage storage could not be saved. Restore access before requesting again."; } } } } '@ $controlCenterSource = @' namespace QaHelper { using System; using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.Windows.Forms; // Every top-level helper window is registered before it can be displayed. public sealed class WindowManager : IDisposable { private readonly Dictionary windows = new Dictionary(); private Form captureProbe; public bool RequireProtection { get; private set; } public WindowManager(bool requireProtection) { RequireProtection = requireProtection; } public T CreateProtectedWindow() where T : Form, new() { return Register(new T()); } public Form CreateProtectedWindow(Type type, object[] arguments) { return Register((Form)Activator.CreateInstance(type, arguments)); } public CaptureAffinity GetBinding(Form form) { return windows[form]; } public T Register(T form) where T : Form { if (!windows.ContainsKey(form)) windows.Add(form, new CaptureAffinity(form, RequireProtection)); return form; } public bool ShowWindow(Form form) { if (form == null || form.IsDisposed || !windows.ContainsKey(form)) return false; IntPtr handle = form.Handle; CaptureAffinity binding = windows[form]; if (binding.Enabled != RequireProtection || (RequireProtection && !binding.Applied)) binding.Enabled = RequireProtection; if (RequireProtection && !binding.Applied) { form.Hide(); return false; } form.Show(); return true; } public void HideWindow(Form form) { if (form != null && !form.IsDisposed) form.Hide(); } public bool IsProtected(Form form) { CaptureAffinity binding; if (form == null || form.IsDisposed || !windows.TryGetValue(form, out binding)) return false; IntPtr handle = form.Handle; if (binding.Enabled != RequireProtection || (RequireProtection && !binding.Applied)) binding.Enabled = RequireProtection; return binding.Applied; } public bool AllProtected { get { foreach (var pair in windows) { if (pair.Key.IsDisposed) continue; IntPtr handle = pair.Key.Handle; if (pair.Value.Enabled != RequireProtection || (RequireProtection && !pair.Value.Applied)) pair.Value.Enabled = RequireProtection; if (!pair.Value.Applied) return false; } return windows.Count != 0; } } public void HideAll() { foreach (Form form in windows.Keys) if (!form.IsDisposed) form.Hide(); } public System.Threading.Tasks.Task BeginCaptureTest() { if (captureProbe != null) throw new InvalidOperationException("Capture test already running."); Rectangle area = Screen.PrimaryScreen.WorkingArea; var bounds = new Rectangle(area.Right - 92, area.Top + 20, 80, 44); Color probeColor = Color.FromArgb(250, 0, 245); captureProbe = Register(new PassiveForm { Bounds = bounds, BackColor = probeColor, ClickThrough = true }); if (!ShowWindow(captureProbe)) return System.Threading.Tasks.Task.FromResult(false); captureProbe.Update(); // Confirm the synthetic surface is painted and wholly on a monitor before sampling. if (!captureProbe.Visible || !Screen.PrimaryScreen.Bounds.Contains(bounds)) return System.Threading.Tasks.Task.FromResult(false); using (var painted = new Bitmap(bounds.Width, bounds.Height)) { captureProbe.DrawToBitmap(painted, new Rectangle(Point.Empty, painted.Size)); if (painted.GetPixel(bounds.Width / 2, bounds.Height / 2).ToArgb() != probeColor.ToArgb()) return System.Threading.Tasks.Task.FromResult(false); } return System.Threading.Tasks.Task.Run(delegate { System.Threading.Thread.Sleep(150); try { using (var bitmap = new Bitmap(60, 24)) using (Graphics graphics = Graphics.FromImage(bitmap)) { graphics.CopyFromScreen(bounds.Left + 10, bounds.Top + 10, 0, 0, bitmap.Size); int matching = 0; for (int x = 0; x < bitmap.Width; x++) for (int y = 0; y < bitmap.Height; y++) if (bitmap.GetPixel(x, y).ToArgb() == probeColor.ToArgb()) matching++; return matching < bitmap.Width * bitmap.Height / 20; } } catch { return false; } }); } public void EndCaptureTest() { if (captureProbe == null) return; captureProbe.Hide(); windows[captureProbe].Dispose(); windows.Remove(captureProbe); captureProbe.Dispose(); captureProbe = null; } public void Dispose() { EndCaptureTest(); HideAll(); foreach (CaptureAffinity binding in windows.Values) binding.Dispose(); foreach (Form form in windows.Keys) if (!form.IsDisposed) form.Dispose(); windows.Clear(); } } public static class ReplyUsage { private static long Count(Dictionary data, string key) { object value; if (data == null || !data.TryGetValue(key, out value) || (!(value is int) && !(value is long))) return -1; long number = Convert.ToInt64(value); return number >= 0 && number <= 1000000000 ? number : -1; } public static void Apply(ApiReply reply, Dictionary data) { object value; if (data.TryGetValue("request_id", out value) && value is string && ((string)value).Length <= 128) reply.RequestId = (string)value; if (data.TryGetValue("session_id", out value) && value is string && ((string)value).Length <= 128) reply.SessionId = (string)value; if (data.TryGetValue("device_id", out value) && value is string && ((string)value).Length <= 128) reply.DeviceId = (string)value; if (data.TryGetValue("config", out value) && value is Dictionary) reply.ServerConfigJson = new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(value); if (data.TryGetValue("model", out value) && value is string && ((string)value).Length <= 100) reply.Model = (string)value; if (data.TryGetValue("usage", out value)) { var usage = value as Dictionary; long input = Count(usage, "inputTokens"), output = Count(usage, "outputTokens"), cached = Count(usage, "cachedTokens"), total = Count(usage, "totalTokens"); object recorded; if (input >= 0 && output >= 0 && cached >= 0 && cached <= input && total == input + output && usage.TryGetValue("usageRecorded", out recorded) && recorded is bool && (bool)recorded) { reply.InputTokens = input; reply.OutputTokens = output; reply.CachedTokens = cached; reply.UsageRecorded = true; } } if (data.TryGetValue("estimatedCostUsd", out value) && (value is decimal || value is double || value is int || value is long)) { try { decimal cost = Convert.ToDecimal(value); if (cost >= 0 && cost <= 1000000) { reply.CostUsd = cost; reply.CostKnown = true; } } catch (OverflowException) { } } } } public static class ControlMemory { public static void DiscardCapture(System.Threading.Tasks.Task task) { if (task == null) return; task.ContinueWith(delegate(System.Threading.Tasks.Task finished) { if (finished.Status == System.Threading.Tasks.TaskStatus.RanToCompletion && finished.Result != null && finished.Result.Png != null) Array.Clear(finished.Result.Png, 0, finished.Result.Png.Length); else if (finished.IsFaulted) { var ignored = finished.Exception; } }, System.Threading.Tasks.TaskScheduler.Default); } } public sealed class ProtectedTextBox : TextBox { protected override void WndProc(ref Message message) { if (message.Msg == 0x7b) return; base.WndProc(ref message); } } public sealed class ProtectedRichTextBox : RichTextBox { protected override void WndProc(ref Message message) { if (message.Msg == 0x7b) return; base.WndProc(ref message); } } public sealed class ProtectedNumericUpDown : NumericUpDown { private readonly Dictionary blockers = new Dictionary(); protected override void OnHandleCreated(EventArgs args) { base.OnHandleCreated(args); foreach (Control child in Controls) if (child is TextBoxBase && !blockers.ContainsKey(child)) blockers.Add(child, new MenuBlocker(child)); } protected override void WndProc(ref Message message) { if (message.Msg == 0x7b) return; base.WndProc(ref message); } protected override void Dispose(bool disposing) { if (disposing) { foreach (MenuBlocker blocker in blockers.Values) blocker.Dispose(); blockers.Clear(); } base.Dispose(disposing); } private sealed class MenuBlocker : NativeWindow, IDisposable { private readonly Control child; public MenuBlocker(Control child) { this.child = child; child.HandleCreated += Created; child.HandleDestroyed += Destroyed; if (child.IsHandleCreated) AssignHandle(child.Handle); } private void Created(object sender, EventArgs args) { if (Handle != IntPtr.Zero) ReleaseHandle(); AssignHandle(child.Handle); } private void Destroyed(object sender, EventArgs args) { if (Handle != IntPtr.Zero) ReleaseHandle(); } protected override void WndProc(ref Message message) { if (message.Msg == 0x7b) return; base.WndProc(ref message); } public void Dispose() { child.HandleCreated -= Created; child.HandleDestroyed -= Destroyed; if (Handle != IntPtr.Zero) ReleaseHandle(); } } } public static class ControlFeaturePolicy { public static void SanitizeEntry(HistoryEntry entry, bool codeAllowed, bool detailsAllowed) { if (entry == null) return; if (!detailsAllowed) entry.Details = String.Empty; if (!codeAllowed) { entry.Code = entry.Language = String.Empty; if (entry.Kind == "code") { entry.Kind = "unknown"; entry.Answer = "Code answers disabled by the service."; entry.Details = String.Empty; } } } public static void SanitizeHistory(HistoryManager history, bool historyAllowed, bool codeAllowed, bool detailsAllowed) { if (history == null) return; if (!historyAllowed) { history.Clear(); return; } foreach (HistoryEntry entry in history.Search("", "All")) SanitizeEntry(entry, codeAllowed, detailsAllowed); } } public sealed class ControlCenterForm : Form { private readonly Queue actions = new Queue(); private readonly Dictionary fields = new Dictionary(); private readonly Dictionary pages = new Dictionary(); private readonly HistoryManager history; private readonly ListBox navigation = new ListBox(); private readonly Label notice = new Label(); private readonly Label home = new Label(); private readonly Label usage = new Label(); private readonly Label account = new Label(); private readonly Label privacy = new Label(); private Label about; private readonly ListBox chats = new ListBox(); private readonly TextBox chatBody = new ProtectedTextBox(); private readonly TextBox search = new ProtectedTextBox(); private readonly TextBox followup = new ProtectedTextBox(); private readonly TextBox key = new ProtectedTextBox(); private readonly Button filter = new Button(); private readonly string[] filterKinds = { "All", "short_answer", "choice", "code", "explanation", "unknown" }; private int filterIndex; private Font ownedFont; private readonly List retiredFonts = new List(); private string selectedId; private bool refreshingHistory; private bool historyAllowed = true, codeAllowed = true, detailsAllowed = true; private string accountText = "Sign in to see your account and service limits."; private string remoteUsage = "Service usage has not been refreshed."; private string rateLimits = "Provider rate limits: unavailable until a direct API response."; private string dataPolicy = "Server history is governed by the account's retention policy."; public UserSettings PendingSettings { get; private set; } public string PendingApiKey { get; private set; } public string FollowupText { get; private set; } public HistoryEntry SelectedEntry { get; private set; } public DateTime LastInteractionUtc { get; private set; } protected override bool ShowWithoutActivation { get { return true; } } protected override CreateParams CreateParams { get { var p = base.CreateParams; p.ExStyle |= 0x08000080; return p; } } public ControlCenterForm(UserSettings settings, HistoryManager history) { this.history = history; Text = "Quick Assistant"; ShowInTaskbar = false; TopMost = true; StartPosition = FormStartPosition.CenterScreen; FormBorderStyle = FormBorderStyle.None; AutoScaleMode = AutoScaleMode.None; MinimumSize = new Size(850, 640); ClientSize = new Size(900, 650); BackColor = Color.White; ForeColor = Color.FromArgb(55, 55, 55); ownedFont = new Font("Courier New", 9); Font = ownedFont; KeyPreview = true; LastInteractionUtc = DateTime.UtcNow; FormClosing += delegate(object sender, FormClosingEventArgs e) { if (e.CloseReason == CloseReason.UserClosing) { e.Cancel = true; ClearSecrets(); Hide(); } }; KeyDown += delegate(object sender, KeyEventArgs e) { Touch(); if (e.KeyCode == Keys.Escape) { ClearSecrets(); Hide(); e.Handled = true; } }; navigation.Bounds = new Rectangle(12, 16, 138, 560); navigation.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Bottom; navigation.BorderStyle = BorderStyle.FixedSingle; navigation.Items.AddRange(new object[] { "Home", "Chats", "Usage", "Hotkeys", "AI / API", "Interface", "Privacy", "Account", "About" }); Controls.Add(navigation); notice.Bounds = new Rectangle(164, 604, 706, 38); notice.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom; notice.ForeColor = Color.FromArgb(135, 40, 40); Controls.Add(notice); foreach (string name in navigation.Items) { var page = new Panel { Bounds = new Rectangle(164, 16, 710, 577), AutoScroll = true, Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, Visible = false }; pages.Add(name, page); Controls.Add(page); } var close = ButtonAt(this, "x", 874, 2, 24, delegate { ClearSecrets(); Hide(); }); close.Height = 22; close.Anchor = AnchorStyles.Top | AnchorStyles.Right; close.BringToFront(); navigation.SelectedIndexChanged += delegate { Touch(); foreach (var pair in pages) pair.Value.Visible = pair.Key == (string)navigation.SelectedItem; if ((string)navigation.SelectedItem == "Chats") RefreshHistory(); }; BuildHome(); BuildChats(); BuildUsage(); BuildHotkeys(); BuildApi(); BuildInterface(); BuildPrivacy(); BuildAccount(); BuildAbout(); ApplySettings(settings); navigation.SelectedIndex = 0; } private void Touch() { LastInteractionUtc = DateTime.UtcNow; } private void Queue(string action) { Touch(); actions.Enqueue(action); } public string TakeAction() { return actions.Count == 0 ? null : actions.Dequeue(); } public void Notify(string message) { notice.Text = message ?? ""; } private Label LabelAt(Control page, string text, int y, int height) { var label = new Label { Text = text, Bounds = new Rectangle(8, y, 665, height), AutoSize = false }; page.Controls.Add(label); return label; } private Button ButtonAt(Control page, string text, int x, int y, int width, Action click) { var button = new Button { Text = text, Bounds = new Rectangle(x, y, width, 32), FlatStyle = FlatStyle.Flat, BackColor = Color.White }; button.FlatAppearance.BorderColor = Color.FromArgb(170, 170, 170); button.Click += delegate { Touch(); click(); }; page.Controls.Add(button); return button; } private void BuildHome() { var p = pages["Home"]; LabelAt(p, "QUICK ASSISTANT", 5, 28); home.Bounds = new Rectangle(8, 48, 665, 380); p.Controls.Add(home); ButtonAt(p, "Refresh account", 8, 450, 174, delegate { Queue("refresh"); }); ButtonAt(p, "Lock", 194, 450, 100, delegate { Queue("lock"); }); ButtonAt(p, "Unlock / Sign in", 306, 450, 180, delegate { Queue("unlock"); }); } private void BuildChats() { var p = pages["Chats"]; LabelAt(p, "CHATS - text only; screenshots are not retained", 5, 28); search.Bounds = new Rectangle(8, 40, 442, 26); p.Controls.Add(search); search.TextChanged += delegate { Touch(); RefreshHistory(); }; filter.Bounds = new Rectangle(462, 40, 211, 26); filter.Text = "Filter: All"; filter.FlatStyle = FlatStyle.Flat; p.Controls.Add(filter); filter.Click += delegate { Touch(); filterIndex = (filterIndex + 1) % filterKinds.Length; filter.Text = "Filter: " + filterKinds[filterIndex]; RefreshHistory(); }; chats.Bounds = new Rectangle(8, 79, 215, 354); chats.DisplayMember = "Title"; p.Controls.Add(chats); chatBody.Bounds = new Rectangle(235, 79, 438, 354); chatBody.Multiline = true; chatBody.ReadOnly = true; chatBody.ScrollBars = ScrollBars.Both; chatBody.WordWrap = false; p.Controls.Add(chatBody); chats.SelectedIndexChanged += delegate { if (!refreshingHistory) Touch(); SelectChat(); }; ButtonAt(p, "Copy", 8, 445, 95, delegate { SelectedEntry = historyEntry(); Queue("copy-history"); }); ButtonAt(p, "Pin", 115, 445, 95, delegate { var e = historyEntry(); if (e != null) { e.Pinned = !e.Pinned; RefreshHistory(); } }); ButtonAt(p, "Delete", 222, 445, 95, delegate { if (selectedId != null) history.Delete(selectedId); RefreshHistory(); }); ButtonAt(p, "Clear RAM history", 329, 445, 185, delegate { history.Clear(); RefreshHistory(); }); ButtonAt(p, "Service history", 526, 445, 147, delegate { Queue("history"); }); followup.Bounds = new Rectangle(8, 495, 520, 26); p.Controls.Add(followup); ButtonAt(p, "Follow up", 540, 493, 133, delegate { SelectedEntry = historyEntry(); FollowupText = followup.Text.Trim(); followup.Clear(); Queue("followup"); }); LabelAt(p, "Delete/Clear affects this session. Server history remains for its retention period.", 537, 38); } private sealed class ChatItem { public HistoryEntry Entry; public string Title { get { return (Entry.Pinned ? "* " : "") + Entry.CreatedAtUtc.ToLocalTime().ToString("HH:mm") + " " + Entry.Kind + "\r\n" + Entry.Question; } } } private HistoryEntry historyEntry() { var item = historyAllowed ? chats.SelectedItem as ChatItem : null; return item == null ? null : item.Entry; } public void SetFeatures(bool allowHistory, bool allowCode, bool allowDetails) { bool changed = historyAllowed != allowHistory || codeAllowed != allowCode || detailsAllowed != allowDetails; historyAllowed = allowHistory; codeAllowed = allowCode; detailsAllowed = allowDetails; pages["Chats"].Enabled = historyAllowed; if (!changed) return; ControlFeaturePolicy.SanitizeHistory(history, historyAllowed, codeAllowed, detailsAllowed); SelectedEntry = null; FollowupText = null; if (!historyAllowed) { followup.Clear(); search.Clear(); selectedId = null; } RefreshHistory(); } public void RefreshHistory() { refreshingHistory = true; string keep = selectedId; chats.BeginUpdate(); chats.Items.Clear(); foreach (HistoryEntry entry in historyAllowed ? history.Search(search.Text, filterKinds[filterIndex]) : new HistoryEntry[0]) { var item = new ChatItem { Entry = entry }; int index = chats.Items.Add(item); if (entry.Id == keep) chats.SelectedIndex = index; } chats.EndUpdate(); if (chats.SelectedIndex < 0 && chats.Items.Count > 0) chats.SelectedIndex = 0; SelectChat(); refreshingHistory = false; } private void SelectChat() { var e = historyEntry(); selectedId = e == null ? null : e.Id; chatBody.Text = !historyAllowed ? "History disabled by the service." : e == null ? "No chats in this session." : "USER\r\n" + e.Question + "\r\n\r\nANSWER\r\n" + e.Answer + (!codeAllowed || String.IsNullOrEmpty(e.Code) ? "" : "\r\n\r\n" + e.Language + "\r\n" + e.Code) + (!detailsAllowed || String.IsNullOrEmpty(e.Details) ? "" : "\r\n\r\nDETAILS\r\n" + e.Details) + "\r\n\r\nModel: " + e.Model + "\r\nTokens: " + (e.InputTokens + e.OutputTokens).ToString("N0") + "\r\nEstimated cost: $" + e.CostUsd.ToString("0.000000", CultureInfo.InvariantCulture) + "\r\nLatency: " + e.LatencyMs + " ms"; } private void BuildUsage() { var p = pages["Usage"]; LabelAt(p, "USAGE / LIMITS", 5, 28); usage.Bounds = new Rectangle(8, 42, 665, 275); p.Controls.Add(usage); NumberField(p, "SessionLimitUsd", "Session spending limit ($; 0 disables)", 328, 0, 1000, 2); NumberField(p, "DailyLimitUsd", "Daily spending limit ($; 0 disables)", 378, 0, 1000, 2); NumberField(p, "MaxRequestsPerMinute", "Maximum requests per minute", 428, 1, 60, 0); ButtonAt(p, "Save settings", 8, 498, 166, SaveSettings); } private void TextField(Control p, string name, string label, int y, bool shortcut) { LabelAt(p, label, y, 23); var text = new ProtectedTextBox { Bounds = new Rectangle(8, y + 23, 440, 25), ReadOnly = shortcut }; fields.Add(name, text); p.Controls.Add(text); if (shortcut) { text.Enter += delegate { Notify("Press the new shortcut. Escape cancels. Emergency exit stays fixed."); }; text.KeyDown += delegate(object sender, KeyEventArgs e) { Touch(); e.SuppressKeyPress = true; e.Handled = true; if (e.KeyCode == Keys.Escape || e.KeyCode == Keys.ControlKey || e.KeyCode == Keys.Menu || e.KeyCode == Keys.ShiftKey) return; if (name == "RegionKey") { if (e.Modifiers == Keys.None) text.Text = e.KeyCode.ToString(); return; } if (e.Modifiers == Keys.None) return; text.Text = (e.Control ? "Ctrl+" : "") + (e.Alt ? "Alt+" : "") + (e.Shift ? "Shift+" : "") + e.KeyCode; }; } } private void BuildHotkeys() { var p = pages["Hotkeys"]; LabelAt(p, "HOTKEYS - select a field, press the new shortcut", 5, 28); string[] names = { "QuickHotkey", "VisualHotkey", "AutoHotkey", "RegionKey", "DetailsHotkey", "CopyHotkey", "ControlHotkey" }; string[] labels = { "Selected text", "Compare marked objects", "Find visible question", "Hold to select region", "More details", "Copy code", "Open Control Center" }; for (int i = 0; i < names.Length; i++) TextField(p, names[i], labels[i], 40 + i * 61, true); LabelAt(p, "Esc cancels. Left Ctrl + Right Ctrl always exits.", 480, 26); ButtonAt(p, "Save settings", 8, 522, 166, SaveSettings); ButtonAt(p, "Restore defaults", 190, 522, 190, delegate { var d = new UserSettings(); foreach (string n in names) ((TextBox)fields[n]).Text = Convert.ToString(typeof(UserSettings).GetProperty(n).GetValue(d, null)); Notify("Default shortcuts restored. Save to apply."); }); } private void ChoiceField(Control p, string name, string label, int y, params string[] options) { LabelAt(p, label + " (click to change)", y, 23); var button = new Button { Bounds = new Rectangle(8, y + 23, 440, 27), Text = options[0], Tag = options[0], FlatStyle = FlatStyle.Flat }; button.Click += delegate { Touch(); int index = Array.IndexOf(options, Convert.ToString(button.Tag)); button.Tag = options[(index + 1) % options.Length]; button.Text = Convert.ToString(button.Tag); }; fields.Add(name, button); p.Controls.Add(button); } private void CheckField(Control p, string name, string label, int y) { var check = new CheckBox { Text = label, Bounds = new Rectangle(8, y, 650, 28) }; fields.Add(name, check); p.Controls.Add(check); } private void NumberField(Control p, string name, string label, int y, decimal min, decimal max, int decimals) { LabelAt(p, label, y, 23); var number = new ProtectedNumericUpDown { Bounds = new Rectangle(8, y + 23, 210, 25), Minimum = min, Maximum = max, DecimalPlaces = decimals, Increment = decimals == 0 ? 1 : 0.1m }; fields.Add(name, number); p.Controls.Add(number); } private void BuildApi() { var p = pages["AI / API"]; LabelAt(p, "AI / API", 5, 28); ChoiceField(p, "ApiMode", "Provider", 40, "server", "openai"); LabelAt(p, "OpenAI API key (kept in RAM unless Remember is selected)", 109, 24); key.Bounds = new Rectangle(8, 136, 520, 25); key.UseSystemPasswordChar = true; p.Controls.Add(key); ButtonAt(p, "Test key", 540, 132, 133, delegate { PendingApiKey = key.Text.Trim(); key.Clear(); Queue("test-key"); }); CheckField(p, "RememberApiKey", "Remember key with Windows DPAPI (current Windows user)", 177); TextField(p, "Model", "Model ID for direct OpenAI requests", 218, false); ChoiceField(p, "ResponseStyle", "Response style", 282, "Very short", "Short", "Normal"); NumberField(p, "InputUsdPerMillion", "Estimate: input $ / million tokens", 347, 0, 10000, 3); NumberField(p, "OutputUsdPerMillion", "Estimate: output $ / million tokens", 408, 0, 10000, 3); NumberField(p, "CachedUsdPerMillion", "Estimate: cached input $ / million tokens", 469, 0, 10000, 3); ButtonAt(p, "Save settings + key", 8, 535, 244, SaveSettings); LabelAt(p, "Direct requests go to OpenAI. Adjust estimates when changing model.", 580, 40); } private void BuildInterface() { var p = pages["Interface"]; LabelAt(p, "INTERFACE", 5, 28); NumberField(p, "PopupSeconds", "Answer popup seconds", 40, 1, 120, 0); NumberField(p, "Opacity", "Window opacity", 111, 0.2m, 1m, 2); NumberField(p, "FontSize", "Font size", 182, 7, 20, 0); CheckField(p, "AnalyzeOnRelease", "Analyze selected region immediately when key is released", 264); NumberField(p, "LockMinutes", "Lock after inactivity (minutes; 0 disables)", 324, 0, 1440, 0); NumberField(p, "ExitMinutes", "Log out and exit after inactivity (minutes; 0 disables)", 395, 0, 1440, 0); LabelAt(p, "Inactivity is measured inside the assistant. Windows mouse movement does not reset it.", 471, 45); ButtonAt(p, "Save settings", 8, 530, 166, SaveSettings); } private void BuildPrivacy() { var p = pages["Privacy"]; LabelAt(p, "PRIVACY", 5, 28); privacy.Bounds = new Rectangle(8, 46, 665, 150); p.Controls.Add(privacy); LabelAt(p, "All helper windows are protected before display.\r\nScreenshots: never saved; sent only for an explicit request.\r\nLocal chat history: RAM only, cleared on logout/exit.\r\nNo taskbar or tray icon.\r\nClipboard selection is restored when safe.\r\nWin+V history is preserved.", 210, 170); CheckField(p, "ClearClipboard", "Clear current Clipboard after successful launch", 397); ButtonAt(p, "Test capture protection", 8, 447, 270, delegate { Queue("test-protection"); }); ButtonAt(p, "Save settings", 8, 510, 166, SaveSettings); } private void BuildAccount() { var p = pages["Account"]; LabelAt(p, "ACCOUNT", 5, 28); account.Bounds = new Rectangle(8, 48, 665, 338); p.Controls.Add(account); ButtonAt(p, "Refresh", 8, 418, 126, delegate { Queue("refresh"); }); ButtonAt(p, "Sign in / Unlock", 146, 418, 195, delegate { Queue("unlock"); }); ButtonAt(p, "Log out", 353, 418, 126, delegate { Queue("logout"); }); ButtonAt(p, "Exit", 491, 418, 126, delegate { Queue("exit"); }); } private void BuildAbout() { about = LabelAt(pages["About"], "", 10, 470); } public void ApplySettings(UserSettings settings) { if (about != null) about.Text = "QUICK ASSISTANT\r\n\r\n" + settings.QuickHotkey + ": selected text\r\nHold " + settings.RegionKey + ": screen region\r\n" + settings.DetailsHotkey + ": details\r\n" + settings.CopyHotkey + ": copy code\r\n" + settings.ControlHotkey + ": Control Center\r\nLeft Ctrl + Right Ctrl: exit\r\n\r\nCapture exclusion depends on the capture application.\r\nUse the Privacy test and verify your screen-sharing app.\r\n\r\nAPI token totals belong to this application.\r\nThey do not represent a ChatGPT subscription balance."; foreach (var pair in fields) { var property = typeof(UserSettings).GetProperty(pair.Key); if (property == null) continue; object value = property.GetValue(settings, null); var text = pair.Value as TextBox; var number = pair.Value as NumericUpDown; var check = pair.Value as CheckBox; var button = pair.Value as Button; if (text != null) text.Text = Convert.ToString(value, CultureInfo.InvariantCulture); if (number != null) number.Value = Math.Max(number.Minimum, Math.Min(number.Maximum, Convert.ToDecimal(value))); if (check != null) check.Checked = Convert.ToBoolean(value); if (button != null) { button.Tag = Convert.ToString(value); button.Text = Convert.ToString(value); } } } private void SaveSettings() { try { var settings = new UserSettings(); foreach (var pair in fields) { var property = typeof(UserSettings).GetProperty(pair.Key); if (property == null) continue; object value; var number = pair.Value as NumericUpDown; var check = pair.Value as CheckBox; var button = pair.Value as Button; if (number != null) value = Convert.ChangeType(number.Value, property.PropertyType, CultureInfo.InvariantCulture); else if (check != null) value = check.Checked; else value = button == null ? ((TextBox)pair.Value).Text.Trim() : Convert.ToString(button.Tag); property.SetValue(settings, value, null); } PendingSettings = settings; PendingApiKey = key.Text.Trim(); key.Clear(); Queue("save"); } catch { Notify("Check the settings values."); } } public string TakeApiKey() { string value = PendingApiKey; PendingApiKey = null; return value; } public void ClearSecrets() { key.Clear(); PendingApiKey = null; PendingSettings = null; FollowupText = null; SelectedEntry = null; followup.Clear(); actions.Clear(); } public void SetAccount(string text) { accountText = text ?? "Signed out"; account.Text = accountText; } public void SetRemoteUsage(string text) { remoteUsage = text ?? "Service usage unavailable"; } public void SetRateLimits(string text) { rateLimits = text ?? "Provider rate limits unavailable"; } public void SetDataPolicy(string text) { dataPolicy = text ?? "Service policy unavailable"; } public void ApplyAppearance(double opacity, float fontSize) { Opacity = opacity; var replacement = new Font("Courier New", fontSize); if (ownedFont != null && ownedFont.Equals(replacement)) { replacement.Dispose(); return; } for (int index = 0; index < retiredFonts.Count; index++) { if (!retiredFonts[index].Equals(replacement)) continue; replacement.Dispose(); replacement = retiredFonts[index]; retiredFonts.RemoveAt(index); break; } Font previous = ownedFont; Font = replacement; ownedFont = replacement; // Hidden controls can retain the previous Font until their handles are recreated. if (previous != null) retiredFonts.Add(previous); } public void SetProtection(bool active) { privacy.Text = (active ? "Capture protection: active on every helper window.\r\nRun the capture test for local evidence." : "Capture protection unavailable. Protected display is blocked.") + "\r\n\r\n" + dataPolicy; } public void UpdateDashboard(string state, UsageTracker tracker, int exitSeconds) { string deadline = exitSeconds < 0 ? "Disabled" : TimeSpan.FromSeconds(Math.Max(0, exitSeconds)).ToString(@"hh\:mm\:ss"); string stats = Summary("SESSION", tracker.Session) + "\r\n" + Summary("TODAY (UTC+5)", tracker.Today) + "\r\n" + Summary("THIS MONTH (UTC+5)", tracker.Month); home.Text = "Status: " + state + "\r\n\r\n" + stats + "\r\n\r\nAuto exit: " + deadline + "\r\n\r\nLocal chats: " + history.Count; usage.Text = stats + "\r\n\r\n" + rateLimits + "\r\n\r\n" + remoteUsage; } private static string Summary(string title, UsageSummary s) { return title + ": " + s.Requests + " requests / " + s.TotalTokens.ToString("N0") + " tokens / ~$" + s.CostUsd.ToString("0.0000", CultureInfo.InvariantCulture) + (s.UnpricedRequests > 0 ? " / " + s.UnpricedRequests + " unpriced" : ""); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); e.Graphics.DrawRectangle(Pens.Gray, 0, 0, ClientSize.Width - 1, ClientSize.Height - 1); } protected override void Dispose(bool disposing) { if (disposing) { ClearSecrets(); base.Dispose(true); if (ownedFont != null) { ownedFont.Dispose(); ownedFont = null; } foreach (Font font in retiredFonts) font.Dispose(); retiredFonts.Clear(); return; } base.Dispose(false); } } } '@ $controlApiSource = @' namespace QaHelper { using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Web.Script.Serialization; public sealed class ControlReply { public bool Success; public int StatusCode; public string Error = ""; public string Json = ""; } public sealed class ControlApiClient : IDisposable { private readonly HttpClient client; private readonly Uri accountUri, usageUri, logoutUri, modelsUri, responsesUri; private readonly int timeoutSeconds, maxResponseBytes; private readonly object gate = new object(); private readonly HashSet active = new HashSet(); private bool disposed; public decimal? EstimatedInputPricePerMillion { get; set; } public decimal? EstimatedOutputPricePerMillion { get; set; } public decimal? EstimatedCachedPricePerMillion { get; set; } public ControlApiClient(string baseUrl, int timeoutSeconds, int maxResponseBytes, string version) : this(baseUrl, timeoutSeconds, maxResponseBytes, version, "https://api.openai.com/v1/") { } // The alternate provider origin is restricted to loopback mocks or the official API. public ControlApiClient(string baseUrl, int timeoutSeconds, int maxResponseBytes, string version, string directApiBaseUrl) { if (timeoutSeconds < 1 || timeoutSeconds > 120 || maxResponseBytes < 1024 || maxResponseBytes > 1048576) throw new ArgumentException("Invalid HTTP limits."); if (String.IsNullOrWhiteSpace(version) || !Regex.IsMatch(version, "^[A-Za-z0-9._-]{1,64}\\z")) throw new ArgumentException("Invalid client version."); Uri backend = new Uri(baseUrl.TrimEnd('/') + "/", UriKind.Absolute); ApiClient.ValidateUri(backend); accountUri = new Uri(backend, "/api/qa/me"); usageUri = new Uri(backend, "/api/qa/usage"); logoutUri = new Uri(backend, "/api/qa/logout"); Uri direct = new Uri(directApiBaseUrl.TrimEnd('/') + "/", UriKind.Absolute); ApiClient.ValidateUri(direct); bool loopback = direct.Scheme == "http" && (direct.Host == "localhost" || direct.Host == "127.0.0.1" || direct.Host == "::1"); if (!loopback && !(direct.Scheme == "https" && direct.Host == "api.openai.com" && direct.Port == 443 && direct.AbsolutePath == "/v1/")) throw new ArgumentException("Provider origin must be the official API or an HTTP loopback mock."); modelsUri = new Uri(direct, "models"); responsesUri = new Uri(direct, "responses"); this.timeoutSeconds = timeoutSeconds; this.maxResponseBytes = maxResponseBytes; client = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false, UseCookies = false }); client.Timeout = Timeout.InfiniteTimeSpan; client.DefaultRequestHeaders.Add("X-QA-Version", version); } public Task AccountAsync(string token) { return SendControlAsync(accountUri, HttpMethod.Get, token, false); } public Task UsageAsync(string token) { return SendControlAsync(usageUri, HttpMethod.Get, token, false); } public Task HistoryAsync(string token, string before) { if (before != null && before.Length > 120) return Task.FromResult(new ControlReply { Error = "Invalid history cursor" }); var uri = new Uri(usageUri.AbsoluteUri + "?limit=2" + (String.IsNullOrEmpty(before) ? "" : "&before=" + Uri.EscapeDataString(before))); return SendControlAsync(uri, HttpMethod.Get, token, false); } public Task LogoutAsync(string token) { return SendControlAsync(logoutUri, HttpMethod.Post, token, false); } public Task TestKeyAsync(string key) { return SendControlAsync(modelsUri, HttpMethod.Get, key, true); } private static bool ValidSecret(string secret) { if (String.IsNullOrEmpty(secret) || secret.Length > 2048) return false; foreach (char c in secret) if (c <= 32 || c >= 127) return false; return true; } private static bool ValidBackendToken(string token) { // Provider credentials cannot be sent to the application's server. return token != null && Regex.IsMatch(token, "^(?:te_live_)?[A-Za-z0-9_-]{43}\\z"); } private async Task SendControlAsync(Uri uri, HttpMethod method, string secret, bool provider) { if (!(provider ? ValidSecret(secret) : ValidBackendToken(secret))) return new ControlReply { Error = provider ? "Invalid API key" : "Invalid account token" }; WireReply wire = await SendAsync(uri, method, method == HttpMethod.Post ? "{}" : null, secret).ConfigureAwait(false); if (!wire.Success) return new ControlReply { StatusCode = wire.StatusCode, Error = wire.Error }; try { Dictionary data = Parser().DeserializeObject(wire.Body) as Dictionary; object success; if (data == null || (data.TryGetValue("success", out success) && (!(success is bool) || !(bool)success))) return new ControlReply { StatusCode = wire.StatusCode, Error = "Invalid response" }; return new ControlReply { Success = true, StatusCode = wire.StatusCode, Json = wire.Body }; } catch (ArgumentException) { return new ControlReply { StatusCode = wire.StatusCode, Error = "Invalid response" }; } catch (InvalidOperationException) { return new ControlReply { StatusCode = wire.StatusCode, Error = "Invalid response" }; } } public Task AnalyzeTextAsync(string text, string key, string model, string responseStyle) { if (String.IsNullOrWhiteSpace(text) || text.Length > 20000) return Task.FromResult(new ApiReply { Error = "Question too large or empty" }); return AnalyzeAsync(new object[] { new { type = "input_text", text = text } }, key, model, responseStyle, 0, 0); } public Task AnalyzeImageAsync(byte[] png, int width, int height, string mode, string key, string model, string responseStyle) { if (png == null || png.Length < 24 || png.Length > 3 * 1024 * 1024 || width < 1 || height < 1 || width > 4096 || height > 4096 || png[0] != 137 || png[1] != 80 || png[2] != 78 || png[3] != 71 || png[4] != 13 || png[5] != 10 || png[6] != 26 || png[7] != 10 || png[12] != 73 || png[13] != 72 || png[14] != 68 || png[15] != 82 || ReadBigEndian(png, 16) != width || ReadBigEndian(png, 20) != height || (mode != "region" && mode != "auto_answer" && mode != "visual")) { if (png != null) Array.Clear(png, 0, png.Length); return Task.FromResult(new ApiReply { Error = "Invalid screenshot" }); } string image; try { image = "data:image/png;base64," + Convert.ToBase64String(png); } finally { Array.Clear(png, 0, png.Length); } string prompt = mode == "visual" ? "Compare object A marked red with object B marked blue and explain the relevant difference." : "Read and answer the visible question or coding task."; prompt += " Extract its question text into question. Image dimensions are " + width + " x " + height + ". All choice box coordinates must use LOCAL pixels of this supplied image; do not use desktop coordinates."; return AnalyzeAsync(new object[] { new { type = "input_text", text = prompt }, new { type = "input_image", image_url = image, detail = "auto" } }, key, model, responseStyle, width, height); } private static int ReadBigEndian(byte[] bytes, int offset) { return (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]; } private async Task AnalyzeAsync(object[] content, string key, string model, string responseStyle, int width, int height) { if (!ValidSecret(key)) return new ApiReply { Error = "Invalid API key" }; if (String.IsNullOrWhiteSpace(model) || !Regex.IsMatch(model, "^[A-Za-z0-9._:-]{1,128}\\z")) return new ApiReply { Error = "Invalid model" }; decimal? inputPrice = EstimatedInputPricePerMillion, outputPrice = EstimatedOutputPricePerMillion, cachedPrice = EstimatedCachedPricePerMillion; string instructions = "Classify the question as choice, short_answer, code, explanation, or unknown. Return only the required JSON object. Answer directly without repeating the question. Put explanations in details. " + "For code return complete runnable code without Markdown fences and its language; never execute or insert it. For choice return a 1-based option_index and confidence between 0 and 1; with images give a tight box around the correct option in local image pixels. " + "For text questions use box=null. Extract the original question into question. Use null for inapplicable language, code, option_index, and box. If uncertain return unknown, answer='Not sure', confidence=0 and null geometry. "; instructions += String.Equals(responseStyle, "Normal", StringComparison.OrdinalIgnoreCase) ? "Keep answer concise; use details for the complete explanation." : String.Equals(responseStyle, "Short", StringComparison.OrdinalIgnoreCase) ? "The answer must fit one or two short sentences." : "The answer must fit one or two lines, preferably just the result."; string json = new JavaScriptSerializer { MaxJsonLength = 8 * 1024 * 1024, RecursionLimit = 24 }.Serialize(new { model = model, store = false, instructions = instructions, max_output_tokens = 6000, input = new object[] { new { role = "user", content = content } }, text = new { format = new { type = "json_schema", name = "assistant_answer", strict = true, schema = AnswerSchema() } } }); WireReply wire = await SendAsync(responsesUri, HttpMethod.Post, json, key).ConfigureAwait(false); json = null; content = null; key = null; if (!wire.Success) return new ApiReply { StatusCode = wire.StatusCode, Error = wire.Error, RateLimits = wire.RateLimits, LatencyMs = wire.LatencyMs, ChargeUncertain = wire.ChargeUncertain }; ApiReply reply = Unknown(); reply.StatusCode = wire.StatusCode; reply.Model = model; reply.LatencyMs = wire.LatencyMs; reply.RateLimits = wire.RateLimits; try { Dictionary envelope = Parser().DeserializeObject(wire.Body) as Dictionary; if (envelope == null) { reply.Success = false; reply.Error = "Invalid response"; return reply; } object value; if (envelope.TryGetValue("model", out value) && value is string && ((string)value).Length <= 128) reply.Model = (string)value; ReadUsage(envelope, reply); if (reply.UsageRecorded && inputPrice.HasValue && outputPrice.HasValue && inputPrice.Value >= 0 && outputPrice.Value >= 0 && (!cachedPrice.HasValue || cachedPrice.Value >= 0)) { reply.CostUsd = ((reply.InputTokens - reply.CachedTokens) * inputPrice.Value + reply.CachedTokens * (cachedPrice ?? inputPrice.Value) + reply.OutputTokens * outputPrice.Value) / 1000000m; reply.CostKnown = true; } string status = envelope.TryGetValue("status", out value) ? value as string : null; if (status != null && status != "completed") return reply; var result = new StringBuilder(); object[] output = envelope.TryGetValue("output", out value) ? value as object[] : null; if (output != null) foreach (object item in output) { var message = item as Dictionary; object type, parts; if (message == null || !message.TryGetValue("type", out type) || (string)type != "message" || !message.TryGetValue("content", out parts)) continue; object[] blocks = parts as object[]; if (blocks == null) continue; foreach (object block in blocks) { var part = block as Dictionary; object text; if (part != null && part.TryGetValue("type", out type) && type is string && (string)type == "output_text" && part.TryGetValue("text", out text) && text is string) result.Append((string)text); } } ParseAdaptive(result.ToString(), reply, width, height); return reply; } catch (ArgumentException) { return reply; } catch (InvalidOperationException) { return reply; } catch (InvalidCastException) { return reply; } catch (OverflowException) { reply.CostKnown = false; reply.CostUsd = 0; return reply; } } private static Dictionary AnswerSchema() { var boxProperties = new Dictionary(); foreach (string name in new[] { "x1", "y1", "x2", "y2" }) boxProperties[name] = new { type = "integer" }; return new Dictionary { { "type", "object" }, { "additionalProperties", false }, { "required", new[] { "type", "answer", "details", "question", "language", "code", "confidence", "option_index", "box" } }, { "properties", new Dictionary { { "type", new Dictionary { { "type", "string" }, { "enum", new[] { "choice", "short_answer", "code", "explanation", "unknown" } } } }, { "answer", new { type = "string" } }, { "details", new { type = "string" } }, { "question", new { type = "string" } }, { "language", new { type = new[] { "string", "null" } } }, { "code", new { type = new[] { "string", "null" } } }, { "confidence", new { type = "number" } }, { "option_index", new { type = new[] { "integer", "null" } } }, { "box", new Dictionary { { "type", new[] { "object", "null" } }, { "properties", boxProperties }, { "required", new[] { "x1", "y1", "x2", "y2" } }, { "additionalProperties", false } } } } } }; } private static ApiReply Unknown() { return new ApiReply { Success = true, Type = "unknown", Answer = "Not sure", Confidence = 0 }; } private static void ParseAdaptive(string json, ApiReply reply, int width, int height) { var data = new JavaScriptSerializer { MaxJsonLength = 262144, RecursionLimit = 16 }.DeserializeObject(json) as Dictionary; if (data == null || data.Count != 9) return; foreach (string name in new[] { "type", "answer", "details", "question", "language", "code", "confidence", "option_index", "box" }) if (!data.ContainsKey(name)) return; string type = data["type"] as string, answer = data["answer"] as string, details = data["details"] as string, question = data["question"] as string; double confidence; if (type == null || answer == null || details == null || question == null || answer.Length > 1200 || details.Length > 12000 || question.Length > 4000 || !Number(data["confidence"], out confidence) || confidence < 0 || confidence > 1 || String.IsNullOrWhiteSpace(answer)) return; if (type != "choice" && type != "short_answer" && type != "code" && type != "explanation" && type != "unknown") return; string language = data["language"] as string, code = data["code"] as string; AutoBox box = null; int option = 0; if (type == "code") { if (String.IsNullOrWhiteSpace(code) || code.Length > 32000 || code.IndexOf('\0') >= 0 || Regex.IsMatch(code, "^\\s*```") || String.IsNullOrEmpty(language) || !Regex.IsMatch(language, "^[a-zA-Z][a-zA-Z0-9_+#.\\-]{0,63}$")) return; } else if (data["language"] != null || data["code"] != null) return; if (type == "choice") { double index; if (!Number(data["option_index"], out index) || index != Math.Floor(index) || index < 1 || index > 100) return; option = (int)index; if (width > 0) { var geometry = data["box"] as Dictionary; double x1, y1, x2, y2; if (geometry == null || geometry.Count != 4 || !Coordinate(geometry, "x1", out x1) || !Coordinate(geometry, "y1", out y1) || !Coordinate(geometry, "x2", out x2) || !Coordinate(geometry, "y2", out y2) || x1 < 0 || y1 < 0 || x2 > width || y2 > height || x1 >= x2 || y1 >= y2) return; box = new AutoBox { X1 = x1, Y1 = y1, X2 = x2, Y2 = y2 }; } else if (data["box"] != null) return; } else if (data["box"] != null || data["option_index"] != null) return; if (type == "unknown" && (confidence != 0 || !String.Equals(answer.Trim(), "Not sure", StringComparison.OrdinalIgnoreCase))) return; reply.Type = type; reply.Answer = answer.Trim(); reply.Details = details; reply.Question = question; reply.Language = language ?? ""; reply.Code = code ?? ""; reply.Confidence = confidence; reply.OptionIndex = option; reply.Box = box; } private static bool Coordinate(Dictionary data, string name, out double number) { object value; number = 0; return data.TryGetValue(name, out value) && Number(value, out number) && number == Math.Floor(number); } private static bool Number(object value, out double number) { number = 0; if (!(value is int) && !(value is long) && !(value is decimal) && !(value is double)) return false; number = Convert.ToDouble(value); return !Double.IsNaN(number) && !Double.IsInfinity(number); } private static bool TokenCount(Dictionary data, string name, out long count) { count = 0; object value; double number; if (!data.TryGetValue(name, out value) || !Number(value, out number) || number < 0 || number > 1000000000 || number != Math.Floor(number)) return false; count = (long)number; return true; } private static void ReadUsage(Dictionary envelope, ApiReply reply) { object raw; long input, output, cached = 0, total; var usage = envelope.TryGetValue("usage", out raw) ? raw as Dictionary : null; if (usage == null || !TokenCount(usage, "input_tokens", out input) || !TokenCount(usage, "output_tokens", out output) || !TokenCount(usage, "total_tokens", out total) || total != input + output) return; if (usage.TryGetValue("input_tokens_details", out raw) && raw != null) { var details = raw as Dictionary; if (details == null || !TokenCount(details, "cached_tokens", out cached) || cached > input) return; } reply.InputTokens = input; reply.OutputTokens = output; reply.CachedTokens = cached; reply.UsageRecorded = true; } private sealed class WireReply { public bool Success, ChargeUncertain; public int StatusCode; public string Error = "", Body = "", RateLimits = ""; public long LatencyMs; } private async Task SendAsync(Uri uri, HttpMethod method, string json, string secret) { CancellationTokenSource cancellation; lock (gate) { if (disposed) return new WireReply { Error = "Request cancelled" }; cancellation = new CancellationTokenSource(); active.Add(cancellation); } cancellation.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds)); Stopwatch watch = Stopwatch.StartNew(); var wire = new WireReply(); try { using (var request = new HttpRequestMessage(method, uri)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secret); if (json != null) request.Content = new StringContent(json, Encoding.UTF8, "application/json"); wire.ChargeUncertain = true; using (HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellation.Token).ConfigureAwait(false)) { wire.StatusCode = (int)response.StatusCode; wire.RateLimits = ReadRateLimits(response); if (!response.IsSuccessStatusCode) { wire.ChargeUncertain = wire.StatusCode >= 500; wire.Error = HttpError(wire.StatusCode); return wire; } if (response.Content.Headers.ContentLength.HasValue && response.Content.Headers.ContentLength.Value > maxResponseBytes) { wire.Error = "Response too large"; return wire; } using (Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) using (var bytes = new MemoryStream()) { byte[] buffer = new byte[8192]; try { int read; while ((read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellation.Token).ConfigureAwait(false)) != 0) { if (bytes.Length + read > maxResponseBytes) { wire.Error = "Response too large"; return wire; } bytes.Write(buffer, 0, read); } wire.Body = new UTF8Encoding(false, true).GetString(bytes.GetBuffer(), 0, (int)bytes.Length); wire.Success = true; } finally { Array.Clear(buffer, 0, buffer.Length); Array.Clear(bytes.GetBuffer(), 0, (int)bytes.Length); } } } } } catch (OperationCanceledException) { wire.Error = "Request cancelled or timed out"; } catch (HttpRequestException) { wire.Error = "Connection failed"; } catch (IOException) { wire.Error = "Connection failed"; } catch (DecoderFallbackException) { wire.Error = "Invalid response"; } catch (ObjectDisposedException) { wire.Error = "Request cancelled"; } finally { wire.LatencyMs = watch.ElapsedMilliseconds; lock (gate) { active.Remove(cancellation); } cancellation.Dispose(); } return wire; } private JavaScriptSerializer Parser() { return new JavaScriptSerializer { MaxJsonLength = maxResponseBytes, RecursionLimit = 24 }; } private static string ReadRateLimits(HttpResponseMessage response) { var values = new List(); foreach (string name in new[] { "x-ratelimit-limit-requests", "x-ratelimit-remaining-requests", "x-ratelimit-reset-requests", "x-ratelimit-limit-tokens", "x-ratelimit-remaining-tokens", "x-ratelimit-reset-tokens" }) { IEnumerable entries; if (!response.Headers.TryGetValues(name, out entries)) continue; foreach (string entry in entries) if (entry.Length <= 64 && Regex.IsMatch(entry, "^[A-Za-z0-9. :+\\-]+$")) { values.Add(name.Substring(12) + ": " + entry); break; } } return String.Join("; ", values.ToArray()); } private static string HttpError(int status) { if (status == 401) return "Invalid API key or expired session"; if (status == 403) return "Access denied"; if (status == 429) return "Rate limit reached"; if (status >= 500) return "Server error"; if (status >= 300 && status < 400) return "Redirect blocked"; if (status == 400) return "API rejected the request or model"; return "Request failed (" + status + ")"; } public void Cancel() { CancellationTokenSource[] snapshot; lock (gate) { snapshot = new CancellationTokenSource[active.Count]; active.CopyTo(snapshot); } foreach (CancellationTokenSource request in snapshot) try { request.Cancel(); } catch (ObjectDisposedException) { } } public void Dispose() { lock (gate) { if (disposed) return; disposed = true; } Cancel(); client.Dispose(); } } } '@ $telemetrySource = @' namespace QaHelper { using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Security; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Web.Script.Serialization; using System.Windows.Forms; using Timer = System.Threading.Timer; public sealed class TelemetryReply { public bool Success; public int StatusCode; public string Error = ""; // Only successful, bounded responses are exposed. HTTP error bodies are never retained. public string Json = ""; } // This module collects device metadata only. It does not capture screen contents or // read Windows usernames, MAC addresses, disk identifiers, or hardware serials. public sealed class TelemetryClient : IDisposable { private sealed class RequestLease { public readonly CancellationTokenSource Cancellation = new CancellationTokenSource(); public int Generation; } private const int MaxResponseBytes = 16384; private const int HeartbeatMilliseconds = 25000; private readonly object gate = new object(); private readonly HttpClient client; private readonly Uri deviceUri, logoutUri; private readonly string version, deviceId; private readonly int timeoutSeconds; private readonly bool identityPersisted; private readonly HashSet active = new HashSet(); private RequestLease heartbeat; private Timer timer; private string authorization = "", state = "idle"; private bool captureProtection, disposed; private int generation; private TelemetryReply lastReply = new TelemetryReply(); public TelemetryClient(string baseUrl, int timeoutSeconds, string version) : this(baseUrl, timeoutSeconds, version, null) { } // identityDirectory is intended for a portable settings directory or an isolated test. // The default is per-user LocalAppData; tokens are never written to this directory. public TelemetryClient(string baseUrl, int timeoutSeconds, string version, string identityDirectory) { if (timeoutSeconds < 1 || timeoutSeconds > 15) throw new ArgumentException("Invalid telemetry timeout."); if (String.IsNullOrEmpty(version) || !Regex.IsMatch(version, "^[A-Za-z0-9._-]{1,64}\\z")) throw new ArgumentException("Invalid client version."); Uri backend; if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out backend) || (backend.Scheme != "https" && !(backend.Scheme == "http" && (backend.Host == "localhost" || backend.Host == "127.0.0.1" || backend.Host == "::1"))) || !String.IsNullOrEmpty(backend.UserInfo) || !String.IsNullOrEmpty(backend.Query) || !String.IsNullOrEmpty(backend.Fragment)) throw new ArgumentException("Telemetry origin must use HTTPS, or exact HTTP loopback in tests, without credentials or a query."); string origin = backend.GetLeftPart(UriPartial.Authority).ToLowerInvariant(); deviceUri = new Uri(origin + "/api/qa/device"); logoutUri = new Uri(origin + "/api/qa/logout"); this.version = version; this.timeoutSeconds = timeoutSeconds; if (identityDirectory == null) identityDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "QuickAssistant", "telemetry"); deviceId = LoadIdentity(identityDirectory, origin, out identityPersisted); client = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false, UseCookies = false }); client.Timeout = Timeout.InfiniteTimeSpan; } public string DeviceId { get { return deviceId; } } public bool IdentityPersisted { get { return identityPersisted; } } public TelemetryReply LastReply { get { lock (gate) return lastReply; } } public bool IsRunning { get { lock (gate) return timer != null && !disposed; } } public Dictionary GetDevicePayload() { bool protection; lock (gate) protection = captureProtection; return DevicePayload(protection); } public string SerializeDeviceJson() { return Serializer().Serialize(GetDevicePayload()); } // The existing login transport can use this body without creating a second login request. public string LoginBody(string login, string password) { var payload = new Dictionary { { "login", login }, { "password", password }, { "device", GetDevicePayload() } }; try { return Serializer().Serialize(payload); } finally { payload.Clear(); password = null; } } public void SetAuthorization(string token) { if (token == null) token = ""; if (token.Length != 0 && !ValidToken(token)) throw new ArgumentException("Invalid account token."); RequestLease[] cancel; lock (gate) { if (disposed) return; authorization = token; generation++; lastReply = new TelemetryReply(); cancel = SnapshotActive(); } CancelLeases(cancel); } // Pass verified protection status (for example WindowManager.AllProtected), not its desired setting. public void SetState(string value, bool protection) { if (!ValidState(value)) throw new ArgumentException("Invalid activity state."); lock (gate) { state = value; captureProtection = protection; } } public void Start() { lock (gate) { if (disposed || timer != null) return; timer = new Timer(OnTimer, null, HeartbeatMilliseconds, HeartbeatMilliseconds); // All work is scheduled on the thread pool; no UI callback or PowerShell runspace is used. QueueHeartbeat(authorization, state, captureProtection, generation); } } private void OnTimer(object ignored) { // A timer callback already queued when Stop runs must not start a new request. lock (gate) if (timer != null && !disposed) QueueHeartbeat(authorization, state, captureProtection, generation); } public void Stop() { Timer previous; lock (gate) { previous = timer; timer = null; } if (previous != null) previous.Dispose(); } public Task HeartbeatAsync() { lock (gate) return QueueHeartbeat(authorization, state, captureProtection, generation); } public Task HeartbeatAsync(string token, string activityState, bool protection) { lock (gate) return QueueHeartbeat(token, activityState, protection, generation); } // Called with gate held; the task never reads a later account's authorization. private Task QueueHeartbeat(string token, string activityState, bool protection, int ownerGeneration) { if (!ValidToken(token)) return Completed("Invalid account token"); if (!ValidState(activityState)) return Completed("Invalid activity state"); if (disposed) return Completed("Cancelled"); if (heartbeat != null) return Completed("Heartbeat already running"); RequestLease lease = CreateLease(ownerGeneration); heartbeat = lease; return Task.Run(async delegate { try { string json = Serializer().Serialize(new { device = DevicePayload(protection), state = activityState }); return await SendAsync(deviceUri, token, json, lease).ConfigureAwait(false); } finally { FinishLease(lease); } }); } public Task LogoutAsync(string reason) { string token; lock (gate) token = authorization; return LogoutAsync(token, reason); } public Task LogoutAsync(string token, string reason) { if (!ValidToken(token)) return Completed("Invalid account token"); if (reason == null || !Regex.IsMatch(reason, "^[a-z][a-z0-9_-]{0,63}\\z")) return Completed("Invalid logout reason"); RequestLease lease; RequestLease[] cancel; Timer previous; lock (gate) { if (disposed) return Completed("Cancelled"); previous = timer; timer = null; if (authorization == token) authorization = ""; generation++; state = "logged_out"; cancel = SnapshotActive(); lease = CreateLease(generation); } if (previous != null) previous.Dispose(); CancelLeases(cancel); return Task.Run(async delegate { try { string json = Serializer().Serialize(new { reason = reason }); return await SendAsync(logoutUri, token, json, lease).ConfigureAwait(false); } finally { FinishLease(lease); } }); } private RequestLease CreateLease(int ownerGeneration) { var lease = new RequestLease { Generation = ownerGeneration }; lease.Cancellation.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds)); active.Add(lease); return lease; } private void FinishLease(RequestLease lease) { lock (gate) { active.Remove(lease); if (heartbeat == lease) heartbeat = null; } lease.Cancellation.Dispose(); } private async Task SendAsync(Uri uri, string token, string json, RequestLease lease) { TelemetryReply reply; try { lease.Cancellation.Token.ThrowIfCancellationRequested(); using (var request = new HttpRequestMessage(HttpMethod.Post, uri)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); request.Headers.Add("X-QA-Version", version); request.Content = new StringContent(json, Encoding.UTF8, "application/json"); using (HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, lease.Cancellation.Token).ConfigureAwait(false)) { int status = (int)response.StatusCode; if (status >= 300 && status < 400) reply = new TelemetryReply { StatusCode = status, Error = "Redirect blocked" }; else if (!response.IsSuccessStatusCode) reply = new TelemetryReply { StatusCode = status, Error = HttpError(status) }; else if (response.Content == null) reply = new TelemetryReply { StatusCode = status, Error = "Invalid response" }; else if (response.Content.Headers.ContentLength.HasValue && response.Content.Headers.ContentLength.Value > MaxResponseBytes) reply = new TelemetryReply { StatusCode = status, Error = "Response too large" }; else { // Framework streams can ignore ReadAsync cancellation; disposal bounds a stalled body too. using (lease.Cancellation.Token.Register(delegate { response.Dispose(); })) using (Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) using (var output = new MemoryStream()) { byte[] buffer = new byte[2048]; int count; bool oversized = false; while ((count = await stream.ReadAsync(buffer, 0, buffer.Length, lease.Cancellation.Token).ConfigureAwait(false)) > 0) { if (output.Length + count > MaxResponseBytes) { oversized = true; break; } output.Write(buffer, 0, count); } lease.Cancellation.Token.ThrowIfCancellationRequested(); if (oversized) reply = new TelemetryReply { StatusCode = status, Error = "Response too large" }; else { string body = new UTF8Encoding(false, true).GetString(output.ToArray()); var data = Serializer().DeserializeObject(body) as Dictionary; object success; reply = data != null && data.TryGetValue("success", out success) && success is bool && (bool)success ? new TelemetryReply { Success = true, StatusCode = status, Json = body } : new TelemetryReply { StatusCode = status, Error = "Invalid response" }; } } } } } } catch (OperationCanceledException) { reply = new TelemetryReply { Error = "Cancelled or timed out" }; } catch (ObjectDisposedException) { reply = new TelemetryReply { Error = "Cancelled or timed out" }; } catch (HttpRequestException) { reply = new TelemetryReply { Error = lease.Cancellation.IsCancellationRequested ? "Cancelled or timed out" : "No connection" }; } catch (IOException) { reply = new TelemetryReply { Error = lease.Cancellation.IsCancellationRequested ? "Cancelled or timed out" : "No connection" }; } catch (ArgumentException) { reply = new TelemetryReply { Error = "Invalid response" }; } catch (InvalidOperationException) { reply = new TelemetryReply { Error = "Invalid response" }; } lock (gate) if (!disposed && generation == lease.Generation) lastReply = reply; return reply; } private Dictionary DevicePayload(bool protection) { string screen = "unknown"; try { System.Drawing.Rectangle bounds = SystemInformation.VirtualScreen; if (bounds.Width > 0 && bounds.Height > 0) screen = bounds.Width.ToString(CultureInfo.InvariantCulture) + "x" + bounds.Height.ToString(CultureInfo.InvariantCulture); } catch (InvalidOperationException) { } catch (SecurityException) { } return new Dictionary { { "device_id", deviceId }, { "computer_name", ReadMetadata(delegate { return Environment.MachineName; }, 128) }, { "os", ReadMetadata(delegate { return Environment.OSVersion.VersionString + (Environment.Is64BitOperatingSystem ? " (64-bit)" : " (32-bit)"); }, 128) }, { "app_version", version }, { "screen", screen }, { "timezone", ReadMetadata(delegate { return TimeZoneInfo.Local.Id; }, 128) }, { "language", ReadMetadata(delegate { return String.IsNullOrEmpty(CultureInfo.CurrentUICulture.Name) ? "und" : CultureInfo.CurrentUICulture.Name; }, 32) }, { "capture_protection", protection } }; } private static string ReadMetadata(Func read, int limit) { try { return Clean(read(), limit); } catch (SecurityException) { return "unknown"; } catch (InvalidOperationException) { return "unknown"; } catch (TimeZoneNotFoundException) { return "unknown"; } catch (InvalidTimeZoneException) { return "unknown"; } } private static string Clean(string value, int limit) { if (value == null) return "unknown"; var output = new StringBuilder(); foreach (char character in value) { if (!Char.IsControl(character)) output.Append(character); if (output.Length >= limit) break; } string result = output.ToString().Trim(); return result.Length == 0 ? "unknown" : result; } private static string LoadIdentity(string directory, string origin, out bool persisted) { persisted = false; string fallback = Guid.NewGuid().ToString("D"); try { string path = Path.Combine(Path.GetFullPath(directory), Hash(origin) + ".device-id"); // The path hash only coordinates writes; it is not sent or used as a device fingerprint. using (var mutex = new Mutex(false, "Local\\QuickAssistantTelemetry-" + Hash(path.ToUpperInvariant()))) { bool owned = false; try { try { owned = mutex.WaitOne(1000); } catch (AbandonedMutexException) { owned = true; } if (!owned) return fallback; Directory.CreateDirectory(Path.GetDirectoryName(path)); if (File.Exists(path)) { using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { if (stream.Length <= 64) using (var reader = new StreamReader(stream, Encoding.ASCII)) { Guid existing; if (Guid.TryParseExact(reader.ReadToEnd().Trim(), "D", out existing) && existing != Guid.Empty) { persisted = true; return existing.ToString("D"); } } } } // A single flushed file is enough; a damaged ID is replaced with a new random ID. using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) { byte[] bytes = Encoding.ASCII.GetBytes(fallback); stream.Write(bytes, 0, bytes.Length); stream.Flush(true); } persisted = true; } finally { if (owned) mutex.ReleaseMutex(); } } } catch (IOException) { } catch (UnauthorizedAccessException) { } catch (SecurityException) { } catch (ArgumentException) { } catch (NotSupportedException) { } return fallback; } private static string Hash(string value) { using (SHA256 hash = SHA256.Create()) return BitConverter.ToString(hash.ComputeHash(Encoding.UTF8.GetBytes(value))).Replace("-", "").ToLowerInvariant(); } private static JavaScriptSerializer Serializer() { return new JavaScriptSerializer { MaxJsonLength = MaxResponseBytes, RecursionLimit = 16 }; } private static bool ValidToken(string token) { return token != null && Regex.IsMatch(token, "^(?:te_live_)?[A-Za-z0-9_-]{43}\\z"); } private static bool ValidState(string value) { return value == "idle" || value == "analyzing" || value == "locked" || value == "logged_out"; } private static Task Completed(string error) { return Task.FromResult(new TelemetryReply { Error = error }); } private static string HttpError(int status) { if (status == 401) return "Session expired"; if (status == 403) return "Access denied"; if (status == 426) return "Update required"; if (status == 429) return "Too many requests"; return "Request failed"; } private RequestLease[] SnapshotActive() { var snapshot = new RequestLease[active.Count]; active.CopyTo(snapshot); return snapshot; } private static void CancelLeases(RequestLease[] leases) { foreach (RequestLease lease in leases) try { lease.Cancellation.Cancel(); } catch (ObjectDisposedException) { } } public void Cancel() { RequestLease[] cancel; lock (gate) cancel = SnapshotActive(); CancelLeases(cancel); } public void Dispose() { Timer previous; RequestLease[] cancel; lock (gate) { if (disposed) return; disposed = true; authorization = ""; generation++; previous = timer; timer = null; cancel = SnapshotActive(); } if (previous != null) previous.Dispose(); CancelLeases(cancel); client.Dispose(); } } } '@ $runHistorySource = @' namespace QaHelper { using System; using System.IO; using System.Security; using System.Text.RegularExpressions; using Microsoft.Win32; public static class RunHistoryCleanup { private const string ClassicKey = @"Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU"; public static int RemoveOwnCommand(string backendUrl) { try { // Only the existing classic Run key is supported. Other stores are left alone. using (RegistryKey key = Registry.CurrentUser.OpenSubKey(ClassicKey, true)) return RemoveMatchingValues(key, backendUrl); } catch (SecurityException) { return 0; } catch (UnauthorizedAccessException) { return 0; } catch (IOException) { return 0; } catch (ArgumentException) { return 0; } } public static bool IsOwnCommand(string command, string backendUrl) { Uri backend; if (String.IsNullOrEmpty(command) || command.Length > 32768 || !Uri.TryCreate(backendUrl, UriKind.Absolute, out backend) || (backend.Scheme != "https" && backend.Scheme != "http") || !String.IsNullOrEmpty(backend.UserInfo)) return false; // Recognize the helper launcher, including an explicitly quoted executable path. if (!Regex.IsMatch(command.TrimStart(), @"^(?:powershell(?:\.exe)?|""[^""\r\n]*\\powershell\.exe"")\s", RegexOptions.IgnoreCase)) return false; MatchCollection calls = Regex.Matches(command, @"\bDownloadString\s*\(\s*['""](?https?://[^'""\s]+)['""]\s*\)", RegexOptions.IgnoreCase); foreach (Match call in calls) { Uri url; if (!Uri.TryCreate(call.Groups["url"].Value, UriKind.Absolute, out url)) continue; if (String.Equals(url.Scheme, backend.Scheme, StringComparison.OrdinalIgnoreCase) && String.Equals(url.Host, backend.Host, StringComparison.OrdinalIgnoreCase) && url.Port == backend.Port && String.IsNullOrEmpty(url.UserInfo) && url.AbsolutePath == "/qa.ps1" && String.IsNullOrEmpty(url.Query) && String.IsNullOrEmpty(url.Fragment) && Regex.IsMatch(command, @"\bscriptblock\s*\]\s*::\s*Create\s*\(", RegexOptions.IgnoreCase) && Regex.IsMatch(command, @"(?:^|\s)-Production(?:\s|[""']|\\1|$)", RegexOptions.IgnoreCase)) return true; } return false; } // Public for isolated registry tests; callers must pass an already opened key. public static int RemoveMatchingValues(RegistryKey key, string backendUrl) { if (key == null) return 0; int removed = 0; foreach (string name in key.GetValueNames()) { if (!Regex.IsMatch(name, "^[a-z]$")) continue; if (key.GetValueKind(name) != RegistryValueKind.String) continue; string value = key.GetValue(name, null, RegistryValueOptions.DoNotExpandEnvironmentNames) as string; if (!IsOwnCommand(value, backendUrl)) continue; // Recheck the value before a deletion so a changed entry is preserved. if (!String.Equals(value, key.GetValue(name) as string, StringComparison.Ordinal)) continue; key.DeleteValue(name, false); removed++; if (key.GetValue(name) != null) continue; string mru = key.GetValue("MRUList") as string; if (mru != null && mru.IndexOf(name, StringComparison.Ordinal) >= 0) key.SetValue("MRUList", mru.Replace(name, ""), RegistryValueKind.String); } return removed; } } } '@ $nativeSource += $controlServicesSource + $controlCenterSource + $controlApiSource + $telemetrySource + $runHistorySource function Initialize-QaTypes { if (-not ('QaHelper.NativeHost' -as [type])) { Add-Type -TypeDefinition $nativeSource -ReferencedAssemblies @( 'System.dll', 'System.Core.dll', 'System.Windows.Forms.dll', 'System.Drawing.dll', 'System.Net.Http.dll', 'System.Web.Extensions.dll', 'System.Security.dll' ) } } # 4. Application state. Only the UI thread accesses this table. $script:QaTheme = @{ Background = '#ffffff'; Foreground = '#555555'; Text = '#333333'; Border = '#aaaaaa' Hover = '#eeeeee'; Muted = '#777777'; Error = '#993333'; FontFamily = 'Courier New' } function Get-QaThemeColor([string] $Name) { return [Drawing.ColorTranslator]::FromHtml($script:QaTheme[$Name]) } function Write-QaDebug([string] $Message) { if ($DebugMode) { Write-Host ('[QA] ' + $Message) } } function ConvertTo-QaPixels([int] $Value) { return [int][Math]::Round($Value * $script:App.Scale) } function Set-AppStatus([string] $State, [string] $Label = '') { $script:App.State = $State if (-not $Label) { $Label = switch ($State) { 'LoggedOut' { 'Logged out' }; 'Ready' { 'QA Ready' }; 'CapturingSelection' { 'Capturing...' } 'Thinking' { 'Analyzing...' }; 'VisualMode' { 'Visual mode' }; 'Error' { 'Error' }; 'Exiting' { 'Exiting...' } 'Selecting' { 'Selecting...' }; 'RegionSelected' { 'Region selected' }; 'Capturing' { 'Capturing...' } 'AutoAnswerThinking' { 'Analyzing...' }; 'AutoAnswerVisible' { 'Answer ready' }; 'AnswerVisible' { 'Answer ready' } 'CodeVisible' { 'Code ready' } } } $script:App.IndicatorLabel.Text = [char]0x25CF + ' ' + $Label $script:App.IndicatorLabel.ForeColor = switch ($State) { 'Ready' { Get-QaThemeColor 'Text' } 'Error' { Get-QaThemeColor 'Error' } 'LoggedOut' { Get-QaThemeColor 'Muted' } default { Get-QaThemeColor 'Foreground' } } Write-QaDebug ('State: ' + $State) Update-QaTelemetryState } function Test-QaBusy { return ($null -ne $script:App.RequestTask -or $null -ne $script:App.SelectionTask -or $null -ne $script:App.CaptureTask -or $null -ne $script:App.CaptureDue -or $null -ne $script:App.ClipboardTask) } # 5. Authentication. Password is removed from the controls as soon as it is submitted. function Show-QaLogin([string] $Status = '') { if (-not $Config.EnableLogin) { return } $script:App.LoginStatus.Text = $Status Show-QaProtectedWindow 'Login' $script:App.Login.TopMost = $true } function Start-QaAuthentication { if (Test-QaBusy) { return } if ($script:App.Token) { $script:App.LoginPassword.Clear(); $script:App.LoginStatus.Text = 'Sign out before changing accounts'; return } $login = $script:App.LoginName.Text.Trim() $password = $script:App.LoginPassword.Text if (-not $login -or -not $password) { $script:App.LoginStatus.Text = 'Enter login and password' $password = $null return } $script:App.LoginPassword.Clear() $script:App.LoginButton.Enabled = $false $script:App.LoginName.Enabled = $false $script:App.LoginPassword.Enabled = $false $script:App.LoginStatus.Text = 'Connecting...' $script:App.PendingLogin = $login $script:App.RequestKind = 'auth' $device = if ($script:App.Telemetry) { $script:App.Telemetry.GetDevicePayload() } else { $null } $script:App.RequestTask = $script:App.Api.LoginAsync($login, $password, $device) $password = $null Set-AppStatus 'Thinking' } # 6. HTTP client completion. A WinForms timer observes Tasks on the UI thread. function Complete-QaRequest { $task = $script:App.RequestTask $kind = $script:App.RequestKind $script:App.RequestTask = $null $script:App.RequestKind = '' $script:App.LoginButton.Enabled = $true $script:App.LoginName.Enabled = $true $script:App.LoginPassword.Enabled = $true try { $reply = $task.GetAwaiter().GetResult() } catch { Show-QaError 'Request failed'; return } if ($kind -ne 'auth') { if ($script:App.RequestOwnerGeneration -ne $script:App.RequestGeneration -or $script:App.Session.IsLocked) { return } Add-QaCompletedRequest $reply $kind } if ($reply.Success) { if ($kind -eq 'auth') { $script:App.Token = $reply.Token $script:App.AccountId = $reply.UserId $script:App.AccountName = if ($reply.Username) { $reply.Username } else { $script:App.PendingLogin } $script:App.PendingLogin = '' Set-QaUsageScope $script:App.Session.Unlock() if ($reply.ServerConfigJson) { $script:App.ServerConfig = $reply.ServerConfigJson | ConvertFrom-Json } if ($script:App.Telemetry) { $script:App.Telemetry.SetAuthorization($reply.Token); $script:App.Telemetry.Start() } $script:App.Control.SetAccount('Signed in: ' + $script:App.AccountName) $script:App.LoginStatus.Text = 'Connected' $script:App.LoginPassword.Clear() $script:App.Login.Hide() Set-AppStatus 'Ready' } else { Show-QaAdaptiveAnswer $reply $kind } return } if ($kind -eq 'auth') { $script:App.Token = '' Set-AppStatus 'LoggedOut' Show-QaLogin $reply.Error return } if ($reply.StatusCode -eq 401 -and $script:App.Settings.Settings.ApiMode -eq 'server') { Start-QaLogout $false 'session_expired' return } Show-QaError $reply.Error } # 7. Clipboard helpers: the dedicated STA worker restores before text reaches HTTP. function Start-QaSelection { if (Test-QaBusy) { return } if (-not (Test-QaRequestAllowed)) { return } if ($script:App.State -in @('VisualMode', 'Selecting', 'RegionSelected')) { return } if ($Config.EnableLogin -and $script:App.Settings.Settings.ApiMode -eq 'server' -and -not $script:App.Token) { Show-QaLogin; return } Hide-QaAnswer Hide-QaAutoAnswer Hide-QaCodePanel $script:App.Selection = [QaHelper.SelectionCapture]::new( $script:App.Native.SelectionWindow, [int]$Config.SelectionTimeoutMilliseconds, [int]$Config.MaxTextCharacters) $script:App.SelectionTask = $script:App.Selection.Start() Set-AppStatus 'CapturingSelection' } function Complete-QaSelection { try { $result = $script:App.SelectionTask.GetAwaiter().GetResult() } catch { $result = $null } $script:App.Selection.Dispose() $script:App.Selection = $null $script:App.SelectionTask = $null if ($null -eq $result) { Show-QaError 'Cannot read selection'; return } if ($result.Error) { Show-QaError $result.Error; return } if ([string]::IsNullOrWhiteSpace($result.Text)) { Show-QaError 'No selected text'; return } Start-QaTextRequest $result.Text } # 8. Screenshot helpers. Hide helper windows, allow repaint, then capture off-thread. function Get-QaCaptureBounds { if ($Config.CaptureVirtualScreen) { return [QaHelper.NativeHost]::GetVirtualBounds() } return [Windows.Forms.Screen]::PrimaryScreen.Bounds } function Start-QaScreenCapture { $red = $script:App.Native.RedPoint $blue = $script:App.Native.BluePoint if ($null -eq $red -or $null -eq $blue) { $script:App.HintLabel.Text = "Place RED and BLUE first`nLeft click = RED Right click = BLUE`nEnter = Analyze Esc = Cancel" return } if (-not $script:App.VisualBounds.Contains($red) -or -not $script:App.VisualBounds.Contains($blue)) { $script:App.HintLabel.Text = "Click inside the captured screen`nLeft click = RED Right click = BLUE`nEnter = Analyze Esc = Cancel" return } $bounds = $script:App.VisualBounds Stop-QaVisualMode Start-QaCapture 'visual' $bounds $red $blue } function Start-QaCapture([string] $Mode, [Drawing.Rectangle] $Bounds, $Red = $null, $Blue = $null) { if (-not (Test-QaRequestAllowed)) { return } $script:App.CaptureMode = $Mode $script:App.Control.Hide() $script:App.CaptureRed = $Red $script:App.CaptureBlue = $Blue $script:App.CaptureBounds = $Bounds $script:App.DesktopAtCapture = [QaHelper.NativeHost]::GetVirtualBounds() Stop-QaRegionSelection Hide-QaAnswer Hide-QaAutoAnswer Hide-QaCodePanel $script:App.Login.Hide() Set-AppStatus 'Capturing' $script:App.Indicator.Hide() $script:App.CaptureDue = [DateTime]::UtcNow.AddMilliseconds(150) } function Complete-QaScreenCapture { try { $image = $script:App.CaptureTask.GetAwaiter().GetResult() } catch { $script:App.CaptureTask = $null Show-QaProtectedWindow 'Indicator' Show-QaError 'Cannot capture screen' return } $script:App.CaptureTask = $null Show-QaProtectedWindow 'Indicator' $script:App.ImageWidth = $image.Width $script:App.ImageHeight = $image.Height $script:App.RequestKind = if ($script:App.CaptureMode -eq 'auto_answer') { 'auto' } else { 'image' } if (-not (Test-QaRequestAllowed)) { [Array]::Clear($image.Png, 0, $image.Png.Length); return } $script:App.PendingQuestion = '' $script:App.RequestStarted = [DateTime]::UtcNow $script:App.RequestOwnerGeneration = $script:App.RequestGeneration $script:App.Usage.BeginRequest([DateTime]::UtcNow) $settings = $script:App.Settings.Settings if ($settings.ApiMode -eq 'openai') { $script:App.RequestTask = $script:App.ControlApi.AnalyzeImageAsync($image.Png, $image.Width, $image.Height, $script:App.CaptureMode, $script:App.Settings.ApiKey, $settings.Model, $settings.ResponseStyle) } else { $script:App.RequestTask = $script:App.Api.AnalyzeImageAsync( $image.Png, $script:App.CaptureBounds, $image.Width, $image.Height, $script:App.CaptureRed, $script:App.CaptureBlue, $script:App.Token, $script:App.CaptureMode) } $image = $null if ($script:App.RequestKind -eq 'auto') { Set-AppStatus 'AutoAnswerThinking' } else { Set-AppStatus 'Thinking' } } # 9. Overlay / UI forms. Coordinates below are logical UI sizes on the primary screen. function New-QaLabel([string] $Text, [int] $X, [int] $Y, [int] $Width, [int] $Height) { $label = [Windows.Forms.Label]::new() $label.Text = $Text $label.Location = [Drawing.Point]::new((ConvertTo-QaPixels $X), (ConvertTo-QaPixels $Y)) $label.Size = [Drawing.Size]::new((ConvertTo-QaPixels $Width), (ConvertTo-QaPixels $Height)) $label.ForeColor = Get-QaThemeColor 'Foreground' return $label } function New-QaButton([string] $Text, [int] $X, [int] $Y, [int] $Width, [int] $Height) { $button = [Windows.Forms.Button]::new() $button.Text = $Text $button.Location = [Drawing.Point]::new((ConvertTo-QaPixels $X), (ConvertTo-QaPixels $Y)) $button.Size = [Drawing.Size]::new((ConvertTo-QaPixels $Width), (ConvertTo-QaPixels $Height)) $button.FlatStyle = 'Flat' $button.FlatAppearance.BorderSize = 1 $button.FlatAppearance.BorderColor = Get-QaThemeColor 'Border' $button.FlatAppearance.MouseOverBackColor = Get-QaThemeColor 'Hover' $button.BackColor = Get-QaThemeColor 'Background' $button.ForeColor = Get-QaThemeColor 'Foreground' $button.TabStop = $false return $button } function Set-QaFormStyle($Form, [int] $Width, [int] $Height) { $Form.FormBorderStyle = 'None' $Form.AutoScaleMode = 'None' $Form.ClientSize = [Drawing.Size]::new((ConvertTo-QaPixels $Width), (ConvertTo-QaPixels $Height)) $Form.StartPosition = 'Manual' $Form.ShowInTaskbar = $false $Form.TopMost = $true $Form.BackColor = Get-QaThemeColor 'Background' $Form.ForeColor = Get-QaThemeColor 'Foreground' $Form.Font = $script:App.UiFont $Form.Opacity = 0.95 $Form.Add_Paint({ param($sender, $eventArgs) $pen = [Drawing.Pen]::new((Get-QaThemeColor 'Border'), 1) try { $eventArgs.Graphics.DrawRectangle($pen, 0, 0, $sender.ClientSize.Width - 1, $sender.ClientSize.Height - 1) } finally { $pen.Dispose() } }) } function Update-QaWindowPositions { $area = [Windows.Forms.Screen]::PrimaryScreen.WorkingArea $margin = ConvertTo-QaPixels 16 $script:App.Indicator.Location = [Drawing.Point]::new($area.Left + $margin, $area.Bottom - $script:App.Indicator.Height - $margin) $script:App.Login.Location = [Drawing.Point]::new($area.Left + $margin, $script:App.Indicator.Top - $script:App.Login.Height - (ConvertTo-QaPixels 8)) $script:App.Answer.Location = [Drawing.Point]::new($area.Right - $script:App.Answer.Width - $margin, $area.Bottom - $script:App.Answer.Height - $margin) $script:App.CodePanel.Location = [Drawing.Point]::new($area.Right - $script:App.CodePanel.Width - $margin, $area.Bottom - $script:App.CodePanel.Height - $margin) $script:App.Hint.Location = [Drawing.Point]::new($area.Right - $script:App.Hint.Width - $margin, $area.Bottom - $script:App.Hint.Height - $margin) } function Initialize-QaForms { $script:App.Indicator = New-QaProtectedWindow ([QaHelper.PassiveForm]) $script:App.Indicator.ClickThrough = $true Set-QaFormStyle $script:App.Indicator 140 30 $script:App.IndicatorLabel = New-QaLabel 'QA Helper' 8 0 132 30 $script:App.IndicatorLabel.TextAlign = 'MiddleLeft' $script:App.Indicator.Controls.Add($script:App.IndicatorLabel) $script:App.Login = New-QaProtectedWindow ([QaHelper.LoginPanel]) Set-QaFormStyle $script:App.Login 310 198 $script:App.Login.Text = 'Timer QA - Sign in' $script:App.Login.KeyPreview = $true $script:App.Login.Controls.Add((New-QaLabel 'Timer / QA' 14 10 220 22)) $close = New-QaButton ([string][char]0x00D7) 270 6 28 26 $close.FlatAppearance.BorderSize = 0 $close.Add_Click({ $script:App.LoginPassword.Clear(); $script:App.Login.Hide() }) $script:App.Login.Controls.Add($close) $script:App.Login.Controls.Add((New-QaLabel 'Login' 14 43 68 24)) $script:App.Login.Controls.Add((New-QaLabel 'Password' 14 80 68 24)) $script:App.LoginName = [QaHelper.ProtectedTextBox]::new() $script:App.LoginPassword = [QaHelper.ProtectedTextBox]::new() $script:App.LoginPassword.PasswordChar = [char]0x25CF $script:App.LoginPassword.MaxLength = 1024 $script:App.LoginName.MaxLength = 256 foreach ($entry in @(@($script:App.LoginName, 42), @($script:App.LoginPassword, 79))) { $entry[0].Location = [Drawing.Point]::new((ConvertTo-QaPixels 87), (ConvertTo-QaPixels $entry[1])) $entry[0].Size = [Drawing.Size]::new((ConvertTo-QaPixels 208), (ConvertTo-QaPixels 25)) $entry[0].BackColor = Get-QaThemeColor 'Background' $entry[0].ForeColor = Get-QaThemeColor 'Text' $entry[0].BorderStyle = 'FixedSingle' $script:App.Login.Controls.Add($entry[0]) } $script:App.LoginName.TabIndex = 0 $script:App.LoginPassword.TabIndex = 1 $script:App.LoginButton = New-QaButton 'Sign in' 87 117 208 30 $script:App.LoginButton.TabStop = $true $script:App.LoginButton.TabIndex = 2 $script:App.LoginButton.Add_Click({ Start-QaAuthentication }) $script:App.Login.Controls.Add($script:App.LoginButton) $script:App.Login.AcceptButton = $script:App.LoginButton $script:App.LoginStatus = New-QaLabel 'Sign in to your QA API' 14 162 280 24 $script:App.Login.Controls.Add($script:App.LoginStatus) $script:App.Login.Add_KeyDown({ param($sender, $eventArgs) if ($eventArgs.KeyCode -eq [Windows.Forms.Keys]::Escape) { $eventArgs.SuppressKeyPress = $true $script:App.LoginPassword.Clear() $script:App.Login.Hide() } }) $script:App.Login.Add_FormClosing({ param($sender, $eventArgs) if ($script:App.State -ne 'Exiting') { $eventArgs.Cancel = $true $script:App.LoginPassword.Clear() $script:App.Login.Hide() } }) $script:App.Answer = New-QaProtectedWindow ([QaHelper.PassiveForm]) Set-QaFormStyle $script:App.Answer 350 108 $script:App.AnswerTitle = New-QaLabel 'Answer' 12 6 290 22 $script:App.Answer.Controls.Add($script:App.AnswerTitle) $script:App.AnswerClose = New-QaButton ([string][char]0x00D7) 314 3 28 25 $answerClose = $script:App.AnswerClose $answerClose.FlatAppearance.BorderSize = 0 $answerClose.Add_Click({ Hide-QaAnswer }) $script:App.Answer.Controls.Add($answerClose) $script:App.AnswerText = [QaHelper.ProtectedRichTextBox]::new() $script:App.AnswerText.Location = [Drawing.Point]::new((ConvertTo-QaPixels 12), (ConvertTo-QaPixels 34)) $script:App.AnswerText.Size = [Drawing.Size]::new((ConvertTo-QaPixels 326), (ConvertTo-QaPixels 42)) $script:App.AnswerText.BorderStyle = 'None' $script:App.AnswerText.ReadOnly = $true $script:App.AnswerText.TabStop = $false $script:App.AnswerText.DetectUrls = $false $script:App.AnswerText.BackColor = $script:App.Answer.BackColor $script:App.AnswerText.ForeColor = Get-QaThemeColor 'Text' $script:App.AnswerText.ScrollBars = 'Vertical' $script:App.Answer.Controls.Add($script:App.AnswerText) $script:App.AnswerDetailsButton = New-QaButton 'Alt+D Details' 12 79 144 22 $script:App.AnswerDetailsButton.FlatAppearance.BorderSize = 0 $script:App.AnswerDetailsButton.Add_Click({ Toggle-QaDetails }) $script:App.Answer.Controls.Add($script:App.AnswerDetailsButton) $script:App.CodePanel = New-QaProtectedWindow ([QaHelper.PassiveForm]) Set-QaFormStyle $script:App.CodePanel 650 350 $script:App.CodeTitle = New-QaLabel 'Code' 12 7 590 23 $script:App.CodeTitle.AutoEllipsis = $true $script:App.CodePanel.Controls.Add($script:App.CodeTitle) $codeClose = New-QaButton ([string][char]0x00D7) 614 3 28 25 $codeClose.FlatAppearance.BorderSize = 0 $codeClose.Add_Click({ Hide-QaCodePanel }) $script:App.CodePanel.Controls.Add($codeClose) $script:App.CodeSummary = New-QaLabel '' 12 33 626 22 $script:App.CodeSummary.AutoEllipsis = $true $script:App.CodePanel.Controls.Add($script:App.CodeSummary) $script:App.CodeText = [QaHelper.ProtectedRichTextBox]::new() $script:App.CodeText.Location = [Drawing.Point]::new((ConvertTo-QaPixels 12), (ConvertTo-QaPixels 62)) $script:App.CodeText.Size = [Drawing.Size]::new((ConvertTo-QaPixels 626), (ConvertTo-QaPixels 238)) $script:App.CodeText.ReadOnly = $true $script:App.CodeText.WordWrap = $false $script:App.CodeText.DetectUrls = $false $script:App.CodeText.ScrollBars = 'Both' $script:App.CodeText.BorderStyle = 'None' $script:App.CodeText.Font = $script:App.UiFont $script:App.CodeText.BackColor = Get-QaThemeColor 'Background' $script:App.CodeText.ForeColor = Get-QaThemeColor 'Text' $script:App.CodeText.TabStop = $false $script:App.CodePanel.Controls.Add($script:App.CodeText) $script:App.CodeDetails = [QaHelper.ProtectedRichTextBox]::new() $script:App.CodeDetails.Location = [Drawing.Point]::new((ConvertTo-QaPixels 12), (ConvertTo-QaPixels 204)) $script:App.CodeDetails.Size = [Drawing.Size]::new((ConvertTo-QaPixels 626), (ConvertTo-QaPixels 96)) $script:App.CodeDetails.ReadOnly = $true $script:App.CodeDetails.BorderStyle = 'None' $script:App.CodeDetails.ScrollBars = 'Vertical' $script:App.CodeDetails.DetectUrls = $false $script:App.CodeDetails.BackColor = Get-QaThemeColor 'Background' $script:App.CodeDetails.ForeColor = Get-QaThemeColor 'Muted' $script:App.CodeDetails.Visible = $false $script:App.CodePanel.Controls.Add($script:App.CodeDetails) $script:App.CodeCopyButton = New-QaButton 'Alt+C Copy code' 12 314 160 24 $script:App.CodeCopyButton.Add_Click({ Copy-QaCode }) $script:App.CodePanel.Controls.Add($script:App.CodeCopyButton) $script:App.CodeDetailsButton = New-QaButton 'Alt+D Details' 182 314 148 24 $script:App.CodeDetailsButton.Add_Click({ Toggle-QaDetails }) $script:App.CodePanel.Controls.Add($script:App.CodeDetailsButton) $script:App.Hint = New-QaProtectedWindow ([QaHelper.PassiveForm]) Set-QaFormStyle $script:App.Hint 350 86 $script:App.HintLabel = New-QaLabel '' 12 9 330 70 $script:App.Hint.Controls.Add($script:App.HintLabel) $script:App.Markers = New-QaProtectedWindow ([QaHelper.MarkerForm]) @((Get-QaCaptureBounds)) $script:App.AutoMarker = New-QaProtectedWindow ([QaHelper.MarkerForm]) @(([QaHelper.NativeHost]::GetVirtualBounds())) $script:App.RegionPreview = New-QaProtectedWindow ([QaHelper.RegionForm]) @(([QaHelper.NativeHost]::GetVirtualBounds())) $script:App.CaptureBindings = [Collections.Generic.List[QaHelper.CaptureAffinity]]::new() foreach ($name in @('Login', 'Indicator', 'Answer', 'CodePanel', 'Hint', 'Markers', 'AutoMarker', 'RegionPreview')) { [void]$script:App.Windows.Register($script:App[$name]) $script:App.CaptureBindings.Add($script:App.Windows.GetBinding($script:App[$name])) } $script:App.Control = New-QaProtectedWindow ([QaHelper.ControlCenterForm]) @($script:App.Settings.Settings, $script:App.History) [void]$script:App.Windows.Register($script:App.Control) Apply-QaUserSettings $script:App.Settings.Settings Update-QaWindowPositions } function Set-QaAnswerLayout([bool] $Expanded) { $script:App.DetailsExpanded = $Expanded $width = if ($Expanded) { 450 } else { 350 } $height = if ($Expanded) { 280 } else { 108 } $script:App.Answer.ClientSize = [Drawing.Size]::new((ConvertTo-QaPixels $width), (ConvertTo-QaPixels $height)) $script:App.AnswerClose.Left = ConvertTo-QaPixels ($width - 36) $script:App.AnswerText.Size = [Drawing.Size]::new((ConvertTo-QaPixels ($width - 24)), (ConvertTo-QaPixels ($height - 66))) if (-not $Expanded) { $script:App.AnswerText.Height = [Math]::Min($script:App.AnswerText.Height, 2 * $script:App.AnswerText.Font.Height + 4) } $script:App.AnswerDetailsButton.Top = ConvertTo-QaPixels ($height - 29) $script:App.AnswerDetailsButton.Text = if ($Expanded) { $script:App.Settings.Settings.DetailsHotkey + ' Hide details' } else { $script:App.Settings.Settings.DetailsHotkey + ' Details' } $script:App.AnswerDetailsButton.Visible = -not [string]::IsNullOrEmpty($script:App.CurrentDetails) $script:App.AnswerText.Text = $script:App.CurrentAnswer if ($Expanded) { $script:App.AnswerText.Text += "`n`n" + $script:App.CurrentDetails } Update-QaWindowPositions } function Show-QaAnswer([string] $Text, [bool] $PreserveDetails = $false) { if ($Text.Length -gt $Config.MaxAnswerCharacters) { $Text = $Text.Substring(0, [int]$Config.MaxAnswerCharacters - 1) + [char]0x2026 } if (-not $PreserveDetails) { $script:App.CurrentDetails = '' } $script:App.CurrentAnswer = $Text Set-QaAnswerLayout $false $script:App.AnswerDeadline = [DateTime]::UtcNow.AddSeconds($Config.AnswerPopupSeconds) Show-QaProtectedWindow 'Answer' Update-QaInputContext } function Hide-QaAnswer { $script:App.Answer.Hide() $script:App.AnswerDeadline = $null Update-QaInputContext } function Show-QaError([string] $Message) { $label = if ($Message -eq 'No connection') { 'Network error' } elseif ($Message -eq 'No selected text') { 'No selection' } else { 'Error' } Set-AppStatus 'Error' $label $script:App.ErrorDeadline = [DateTime]::UtcNow.AddSeconds(4) Show-QaAnswer $Message } function Hide-QaCodePanel { $script:App.CodePanel.Hide() $script:App.CodeText.Clear() $script:App.CodeDetails.Clear() $script:App.CurrentCode = '' Update-QaInputContext } function Show-QaAdaptiveAnswer($Reply, [string] $Kind) { $script:App.CurrentDetails = [string]$Reply.Details if (-not (Test-QaFeature 'detailedAnswers')) { $script:App.CurrentDetails = '' } if ($script:App.CurrentDetails.Length -gt $Config.MaxDetailsCharacters) { $script:App.CurrentDetails = $script:App.CurrentDetails.Substring(0, [int]$Config.MaxDetailsCharacters) } $script:App.CurrentAnswer = [string]$Reply.Answer $script:App.DetailsExpanded = $false switch ($Reply.Type) { 'unknown' { Show-QaError 'Not sure'; return } 'choice' { if ($Reply.Confidence -lt (Get-QaConfidenceThreshold) -or $Reply.HasInvalidAutoMetadata) { Show-QaError 'Not sure'; return } if ($Kind -ne 'text') { Show-QaAutoAnswer $Reply; return } Set-AppStatus 'AnswerVisible' 'Answer found' } 'code' { if (-not (Test-QaFeature 'codeMode')) { Show-QaError 'Code answers disabled by the service'; return } if ([string]::IsNullOrWhiteSpace($Reply.Code) -or $Reply.Code.Length -gt $Config.MaxCodeCharacters) { Show-QaError 'Code too large or missing'; return } $script:App.CurrentCode = $Reply.Code $script:App.CodeTitle.Text = if ($Reply.Language) { $Reply.Language } else { 'Code' } $script:App.CodeSummary.Text = $Reply.Answer $script:App.CodeText.Text = $Reply.Code $script:App.CodeText.Height = ConvertTo-QaPixels 238 $script:App.CodeDetails.Visible = $false $script:App.CodeDetails.Text = $script:App.CurrentDetails $script:App.CodeDetailsButton.Text = $script:App.Settings.Settings.DetailsHotkey + ' Details' $script:App.CodeDetailsButton.Visible = -not [string]::IsNullOrEmpty($script:App.CurrentDetails) Show-QaProtectedWindow 'CodePanel' Set-AppStatus 'CodeVisible' $script:App.StatusDeadline = [DateTime]::UtcNow.AddSeconds(3) Update-QaInputContext return } default { Set-AppStatus 'AnswerVisible' } } $script:App.StatusDeadline = [DateTime]::UtcNow.AddSeconds(3) Show-QaAnswer $Reply.Answer $true } function Toggle-QaDetails { if (-not (Test-QaFeature 'detailedAnswers')) { return } if ([string]::IsNullOrEmpty($script:App.CurrentDetails)) { return } $script:App.Session.Touch([DateTime]::UtcNow) $expanded = -not $script:App.DetailsExpanded if ($script:App.CodePanel.Visible) { $script:App.DetailsExpanded = $expanded $script:App.CodeText.Height = if ($expanded) { ConvertTo-QaPixels 132 } else { ConvertTo-QaPixels 238 } $script:App.CodeDetails.Visible = $expanded $script:App.CodeDetailsButton.Text = if ($expanded) { $script:App.Settings.Settings.DetailsHotkey + ' Hide details' } else { $script:App.Settings.Settings.DetailsHotkey + ' Details' } } elseif ($script:App.Answer.Visible -or $script:App.AutoMarker.Visible) { Set-QaAnswerLayout $expanded Show-QaProtectedWindow 'Answer' $script:App.AnswerDeadline = [DateTime]::UtcNow.AddSeconds($Config.AnswerPopupSeconds) } Update-QaInputContext } function Clear-CurrentClipboard { if (Test-QaBusy) { return } $script:App.ClipboardKind = 'clear' $script:App.ClipboardTask = [QaHelper.ClipboardTools]::ClearCurrentAsync() } function Copy-QaCode { if (-not (Test-QaFeature 'codeMode')) { Hide-QaCodePanel; return } if ((Test-QaBusy) -or -not $script:App.CodePanel.Visible -or -not $script:App.CurrentCode) { return } $script:App.Session.Touch([DateTime]::UtcNow) $script:App.CodeCopyButton.Enabled = $false $script:App.ClipboardKind = 'code' $script:App.ClipboardTask = [QaHelper.ClipboardTools]::CopyCodeAsync($script:App.CurrentCode) } function Complete-QaClipboardOperation { try { $result = $script:App.ClipboardTask.GetAwaiter().GetResult() } catch { $result = $null } $kind = $script:App.ClipboardKind $script:App.ClipboardTask = $null $script:App.ClipboardKind = '' $script:App.CodeCopyButton.Enabled = $true if ($kind -eq 'code') { if ($result -and $result.Success) { Set-AppStatus 'CodeVisible' 'Code copied' } else { Set-AppStatus 'Error' 'Clipboard busy'; $script:App.ErrorDeadline = [DateTime]::UtcNow.AddSeconds(3) } $script:App.StatusDeadline = [DateTime]::UtcNow.AddSeconds(3) } elseif ($kind -eq 'history') { $script:App.Control.Notify($(if ($result -and $result.Success) { 'Answer copied.' } else { 'Clipboard busy; try again.' })) } elseif (-not $result -or -not $result.Success) { Write-QaDebug 'Startup Clipboard was unchanged or busy.' } } # 10. Marker overlay. Mouse hooks are active only while the user places marks. function Start-QaVisualMode { if (-not $Config.EnableVisualMode -or (Test-QaBusy)) { return } if (-not (Test-QaFeature 'visualAnswers')) { Show-QaError 'Visual answers disabled by the service'; return } if (-not (Test-QaRequestAllowed)) { return } if ($script:App.State -eq 'Selecting') { return } if ($Config.EnableLogin -and $script:App.Settings.Settings.ApiMode -eq 'server' -and -not $script:App.Token) { Show-QaLogin; return } if ($script:App.State -eq 'VisualMode') { return } Hide-QaAnswer Hide-QaAutoAnswer Hide-QaCodePanel Stop-QaRegionSelection $script:App.Login.Hide() $script:App.VisualBounds = Get-QaCaptureBounds $script:App.Markers.Bounds = $script:App.VisualBounds $script:App.Markers.SetPoints($null, $null) $script:App.HintLabel.Text = "Visual mode`nLeft click = RED Right click = BLUE`nEnter = Analyze Esc = Cancel" $script:App.Native.SetContext($true, $false) Set-AppStatus 'VisualMode' Show-QaProtectedWindow 'Markers' Show-QaProtectedWindow 'Hint' } function Stop-QaVisualMode { $script:App.Native.SetContext($false, $script:App.Answer.Visible) $script:App.Markers.Hide() $script:App.Markers.SetPoints($null, $null) $script:App.Hint.Hide() if ($script:App.State -eq 'VisualMode') { Set-AppStatus 'Ready' } } function Update-QaInputContext { if (-not $script:App -or -not $script:App.Native) { return } $hasAnswer = $script:App.Answer.Visible -or $script:App.AutoMarker.Visible -or $script:App.Login.Visible -or $script:App.CodePanel.Visible -or $script:App.Control.Visible -or ($script:App.RequestTask -and $script:App.RequestKind -ne 'auth') $script:App.Native.SetContext(($script:App.State -eq 'VisualMode'), $hasAnswer) $script:App.Native.SetRegionSelected(($script:App.State -eq 'RegionSelected')) $script:App.Native.RegionEnabled = -not (Test-QaBusy) -and $script:App.State -ne 'VisualMode' -and -not $script:App.Session.IsLocked -and (Test-QaFeature 'regionCapture') $detailsAvailable = (Test-QaFeature 'detailedAnswers') -and ($script:App.Answer.Visible -or $script:App.CodePanel.Visible -or $script:App.AutoMarker.Visible) -and -not [string]::IsNullOrEmpty($script:App.CurrentDetails) $script:App.Native.SetAdaptiveContext($detailsAvailable, ($script:App.CodePanel.Visible -and (Test-QaFeature 'codeMode'))) if ($script:App.Native.AdaptiveHotkeyError -and $script:App.Native.AdaptiveHotkeyError -ne $script:App.AdaptiveHotkeyError) { $script:App.AdaptiveHotkeyError = $script:App.Native.AdaptiveHotkeyError Write-QaDebug 'An adaptive hotkey is unavailable; use the panel buttons.' } } function Hide-QaAutoAnswer { $script:App.AutoMarker.Hide() $script:App.AutoMarker.SetPoints($null, $null) $script:App.AutoMarkerDeadline = $null Update-QaInputContext } function Show-QaAutoAnswer($Reply) { $placement = [QaHelper.AutoAnswerGeometry]::Resolve($Reply, $script:App.ImageWidth, $script:App.ImageHeight, [double](Get-QaConfidenceThreshold), 0) if (-not $placement.Valid) { Show-QaError 'Not sure'; return } if ([QaHelper.NativeHost]::GetVirtualBounds() -ne $script:App.DesktopAtCapture) { Show-QaError 'Display changed. Try again.' return } $bounds = $script:App.CaptureBounds $ratioX = $bounds.Width / [double]$script:App.ImageWidth $ratioY = $bounds.Height / [double]$script:App.ImageHeight if ($Reply.Box) { $x = $bounds.Left + $Reply.Box.X1 * $ratioX - $Config.AutoAnswerMarkerOffset $y = $bounds.Top + (($Reply.Box.Y1 + $Reply.Box.Y2) / 2.0) * $ratioY } else { $x = $bounds.Left + $Reply.Target.X * $ratioX $y = $bounds.Top + $Reply.Target.Y * $ratioY } $x = [int][Math]::Round([Math]::Max($bounds.Left + 10, [Math]::Min($bounds.Right - 10, $x))) $y = [int][Math]::Round([Math]::Max($bounds.Top + 10, [Math]::Min($bounds.Bottom - 10, $y))) $script:App.AutoMarker.Bounds = [QaHelper.NativeHost]::GetVirtualBounds() $script:App.AutoMarker.SetPoints([Drawing.Point]::new($x, $y), $null) Show-QaProtectedWindow 'AutoMarker' $script:App.AutoMarkerDeadline = [DateTime]::UtcNow.AddSeconds($Config.AutoAnswerMarkerSeconds) Set-AppStatus 'AutoAnswerVisible' 'Answer found' $script:App.StatusDeadline = [DateTime]::UtcNow.AddSeconds(3) Update-QaInputContext } function Start-QaAutoAnswer { if (Test-QaBusy) { return } if (-not (Test-QaFeature 'visualAnswers')) { Show-QaError 'Visual answers disabled by the service'; return } if ($script:App.State -in @('VisualMode', 'Selecting')) { return } if (-not (Test-QaRequestAllowed)) { return } if ($Config.EnableLogin -and $script:App.Settings.Settings.ApiMode -eq 'server' -and -not $script:App.Token) { Show-QaLogin; return } $bounds = [Windows.Forms.Screen]::FromHandle($script:App.Native.CaptureWindow).Bounds Start-QaCapture 'auto_answer' $bounds } function Get-QaNormalizedRegion([Drawing.Point] $A, [Drawing.Point] $B) { return [Drawing.Rectangle]::FromLTRB([Math]::Min($A.X, $B.X), [Math]::Min($A.Y, $B.Y), [Math]::Max($A.X, $B.X), [Math]::Max($A.Y, $B.Y)) } function Start-QaRegionSelection { if ((Test-QaBusy) -or $script:App.State -eq 'VisualMode') { $script:App.Native.CancelSelection(); return } if (-not (Test-QaFeature 'regionCapture')) { $script:App.Native.CancelSelection(); Show-QaError 'Region capture disabled by the service'; return } if (-not (Test-QaRequestAllowed)) { $script:App.Native.CancelSelection(); return } if ($Config.EnableLogin -and $script:App.Settings.Settings.ApiMode -eq 'server' -and -not $script:App.Token) { $script:App.Native.CancelSelection() Show-QaLogin return } Hide-QaAnswer Hide-QaAutoAnswer Hide-QaCodePanel $script:App.Login.Hide() $script:App.RegionRectangle = $null $script:App.RegionStart = $script:App.Native.PointA if ($null -eq $script:App.RegionStart) { return } $script:App.RegionPreview.Bounds = [QaHelper.NativeHost]::GetVirtualBounds() $script:App.RegionPreview.SetRegion((Get-QaNormalizedRegion $script:App.RegionStart $script:App.RegionStart)) Show-QaProtectedWindow 'RegionPreview' Set-AppStatus 'Selecting' } function Complete-QaRegionSelection { if ($script:App.State -ne 'Selecting') { return } $end = $script:App.Native.PointB if ($null -eq $end -or $null -eq $script:App.RegionStart) { Stop-QaRegionSelection; return } $rectangle = Get-QaNormalizedRegion $script:App.RegionStart $end $rectangle = [Drawing.Rectangle]::Intersect($rectangle, [QaHelper.NativeHost]::GetVirtualBounds()) if ($rectangle.Width -lt $Config.MinimumSelectionSize -or $rectangle.Height -lt $Config.MinimumSelectionSize) { Stop-QaRegionSelection Set-AppStatus 'Ready' 'No selection' $script:App.StatusDeadline = [DateTime]::UtcNow.AddSeconds(3) return } $script:App.RegionRectangle = $rectangle $script:App.RegionPreview.SetRegion($rectangle) Set-AppStatus 'RegionSelected' Update-QaInputContext if ($Config.AnalyzeRegionOnRelease) { Start-QaRegionAnalysis } } function Stop-QaRegionSelection { $script:App.Native.CancelSelection() $script:App.Native.SetRegionSelected($false) $script:App.RegionPreview.Hide() $script:App.RegionPreview.SetRegion($null) $script:App.RegionStart = $null $script:App.RegionRectangle = $null if ($script:App.State -in @('Selecting', 'RegionSelected')) { Set-AppStatus 'Ready' } } function Start-QaRegionAnalysis { if ((Test-QaBusy) -or $null -eq $script:App.RegionRectangle) { return } $bounds = $script:App.RegionRectangle Start-QaCapture 'region' $bounds } # 11-13. Global input, Quick Answer, Visual Mode: one queue, one UI timer. function Invoke-QaUiTick { if ($script:App.State -eq 'Exiting') { return } Invoke-QaControlTick if ($script:App.State -eq 'Exiting' -or $script:App.LogoutDeadline -or -not $script:App.Native) { return } $signal = $script:App.Native.TakeSignal() while ($signal) { if ($signal -ne 'exit') { $script:App.Session.Touch([DateTime]::UtcNow) } switch ($signal) { 'exit' { Start-QaLogout $true; return } 'control' { Show-QaControl $true } 'quick' { Start-QaSelection } 'visual' { Start-QaVisualMode } 'auto' { Start-QaAutoAnswer } 'region-start' { Start-QaRegionSelection } 'region-end' { Complete-QaRegionSelection } 'details' { Toggle-QaDetails } 'copy-code' { Copy-QaCode } 'escape' { if ($script:App.Control.Visible) { $script:App.Control.ClearSecrets(); $script:App.Control.Hide() } if ($script:App.RequestTask -and $script:App.RequestKind -ne 'auth') { Record-QaCancelledRequest $script:App.Api.Cancel(); $script:App.ControlApi.Cancel() $script:App.RequestGeneration++; $script:App.RequestTask = $null; $script:App.PendingQuestion = '' Set-AppStatus 'Ready' 'Cancelled' } if ($script:App.Login.Visible) { $script:App.LoginPassword.Clear(); $script:App.Login.Hide() } if ($script:App.State -eq 'VisualMode') { Stop-QaVisualMode } if ($script:App.State -in @('Selecting', 'RegionSelected')) { Stop-QaRegionSelection } Hide-QaAnswer Hide-QaAutoAnswer Hide-QaCodePanel if (-not (Test-QaBusy) -and $script:App.State -ne 'LoggedOut') { Set-AppStatus 'Ready' } } 'analyze' { if ($script:App.State -eq 'VisualMode') { Start-QaScreenCapture } elseif ($script:App.State -eq 'RegionSelected') { Start-QaRegionAnalysis } } 'markers' { if ($script:App.State -eq 'VisualMode') { $script:App.Markers.SetPoints($script:App.Native.RedPoint, $script:App.Native.BluePoint) } } } $signal = $script:App.Native.TakeSignal() } if ($script:App.SelectionTask -and $script:App.SelectionTask.IsCompleted) { Complete-QaSelection } if ($script:App.CaptureDue -and [DateTime]::UtcNow -ge $script:App.CaptureDue) { $script:App.CaptureDue = $null if ([QaHelper.NativeHost]::GetVirtualBounds() -ne $script:App.DesktopAtCapture) { Show-QaProtectedWindow 'Indicator' Show-QaError 'Display changed. Try again.' } else { $script:App.CaptureTask = [QaHelper.ScreenCapture]::BeginPreparedCapture( $script:App.CaptureBounds, $script:App.CaptureRed, $script:App.CaptureBlue, [long]$Config.MaxScreenshotPixels, [int](Get-QaRemoteLimit 'maxScreenshotDimension' $Config.MaxImageDimension 256 2048), [int]$Config.MaxImageBytes) } } if ($script:App.CaptureTask -and $script:App.CaptureTask.IsCompleted) { Complete-QaScreenCapture } if ($script:App.RequestTask -and $script:App.RequestTask.IsCompleted) { Complete-QaRequest } if ($script:App.ClipboardTask -and $script:App.ClipboardTask.IsCompleted) { Complete-QaClipboardOperation } if ($script:App.State -eq 'Selecting' -and $null -ne $script:App.RegionStart) { $script:App.RegionPreview.SetRegion((Get-QaNormalizedRegion $script:App.RegionStart ([Windows.Forms.Cursor]::Position))) } if ($script:App.AutoMarkerDeadline -and [DateTime]::UtcNow -ge $script:App.AutoMarkerDeadline) { Hide-QaAutoAnswer } if ($script:App.AnswerDeadline) { if ($script:App.Answer.Bounds.Contains([Windows.Forms.Cursor]::Position)) { $script:App.AnswerDeadline = [DateTime]::UtcNow.AddSeconds(2) } elseif ([DateTime]::UtcNow -ge $script:App.AnswerDeadline) { Hide-QaAnswer } } if ($script:App.State -eq 'Error' -and [DateTime]::UtcNow -ge $script:App.ErrorDeadline) { if ($Config.EnableLogin -and $script:App.Settings.Settings.ApiMode -eq 'server' -and -not $script:App.Token) { Set-AppStatus 'LoggedOut' } else { Set-AppStatus 'Ready' } } if ($script:App.StatusDeadline -and [DateTime]::UtcNow -ge $script:App.StatusDeadline) { $script:App.StatusDeadline = $null if ($script:App.State -in @('AnswerVisible', 'AutoAnswerVisible', 'CodeVisible', 'Ready')) { Set-AppStatus 'Ready' } } Update-QaInputContext if ($script:App.SmokeDeadline -and [DateTime]::UtcNow -ge $script:App.SmokeDeadline) { Stop-QaApplication } } # These functions are embedded into qa.ps1 for the single-file release. function Initialize-QaControl { $directory = $Config.SettingsDirectory if (-not $directory) { $directory = Join-Path $env:LOCALAPPDATA 'QuickAssistant' } $script:App.Settings = [QaHelper.SettingsManager]::new($directory) if ($SmokeTestSeconds -eq 0) { $script:App.Settings.Load() } $script:App.Session = [QaHelper.SessionManager]::new() $script:App.History = [QaHelper.HistoryManager]::new() $script:App.Usage = if ($SmokeTestSeconds -gt 0) { [QaHelper.UsageTracker]::new() } else { [QaHelper.UsageTracker]::new($directory) } $script:App.Windows = [QaHelper.WindowManager]::new([bool]$Config.HideOverlaysFromCapture) $script:App.ControlApi = [QaHelper.ControlApiClient]::new($Config.ApiBaseUrl, [int]$Config.AnswerTimeoutSeconds, [int]$Config.MaxResponseBytes, $Config.ClientVersion) $script:App.Telemetry = $null; $script:App.LastTelemetryReply = $null; $script:App.ServerConfig = $null; $script:App.LastAnnouncement = '' if ($Config.EnableDeviceTelemetry -and $SmokeTestSeconds -eq 0) { $script:App.Telemetry = [QaHelper.TelemetryClient]::new($Config.ApiBaseUrl, 5, $Config.ClientVersion, (Join-Path $directory 'telemetry')) } $script:App.Control = $null $script:App.AccountTask = $null; $script:App.AccountKind = '' $script:App.LogoutTask = $null; $script:App.ExitAfterLogout = $false; $script:App.LogoutDeadline = $null; $script:App.LockAfterLogout = $false $script:App.LastDashboard = [DateTime]::MinValue; $script:App.LastControlTouch = [DateTime]::MinValue $script:App.PendingQuestion = ''; $script:App.RequestStarted = [DateTime]::MinValue $script:App.AccountName = ''; $script:App.RequestGeneration = 0; $script:App.RequestOwnerGeneration = 0 $script:App.TestedKey = ''; $script:App.AccountId = ''; $script:App.PendingLogin = '' $script:App.HistoryBefore = ''; $script:App.HistoryComplete = $false $script:App.ProtectionTask = $null; $script:App.RunHistoryDue = $null if ($Config.RemoveOwnRunHistory -and $SmokeTestSeconds -eq 0 -and -not $DebugMode) { $script:App.RunHistoryDue = [DateTime]::UtcNow.AddMilliseconds(700) } if ($ConfigPath -or $SmokeTestSeconds -gt 0) { $script:App.Settings.Settings.PopupSeconds = [int]$Config.AnswerPopupSeconds $script:App.Settings.Settings.ClearClipboard = [bool]$Config.ClearCurrentClipboardAfterLaunch $script:App.Settings.Settings.AnalyzeOnRelease = [bool]$Config.AnalyzeRegionOnRelease $script:App.Settings.Settings.AutoHotkey = $Config.AutoAnswerHotkey $script:App.Settings.Settings.RegionKey = $Config.RegionSelectHotkey } Apply-QaUserSettings $script:App.Settings.Settings } function Apply-QaUserSettings($Settings) { $Config.AnswerPopupSeconds = $Settings.PopupSeconds $Config.ClearCurrentClipboardAfterLaunch = $Settings.ClearClipboard $Config.AnalyzeRegionOnRelease = $Settings.AnalyzeOnRelease $Config.AutoAnswerHotkey = $Settings.AutoHotkey $Config.RegionSelectHotkey = $Settings.RegionKey if ($script:App.ControlApi) { $script:App.ControlApi.EstimatedInputPricePerMillion = $Settings.InputUsdPerMillion $script:App.ControlApi.EstimatedOutputPricePerMillion = $Settings.OutputUsdPerMillion $script:App.ControlApi.EstimatedCachedPricePerMillion = $Settings.CachedUsdPerMillion } foreach ($name in @('Indicator', 'Answer', 'CodePanel', 'Login', 'Hint')) { if ($script:App[$name]) { $script:App[$name].Opacity = $Settings.Opacity } } if ($script:App.Indicator) { Set-QaUiFont ([single]$Settings.FontSize) if ($script:App.Control) { $script:App.Control.ApplyAppearance($Settings.Opacity, [single]$Settings.FontSize) } $script:App.CodeCopyButton.Text = $Settings.CopyHotkey + ' Copy code' } Apply-QaRemoteState ([pscustomobject]@{}) } function Set-QaUiFont([single] $FontSize) { $replacement = [Drawing.Font]::new('Courier New', $FontSize) $previous = $script:App.UiFont if ($previous -and $previous.Equals($replacement)) { $replacement.Dispose(); return } if (-not $script:App.ContainsKey('RetiredFonts')) { $script:App.RetiredFonts = [Collections.Generic.List[Drawing.Font]]::new() } for ($index = 0; $index -lt $script:App.RetiredFonts.Count; $index++) { if (-not $script:App.RetiredFonts[$index].Equals($replacement)) { continue } $replacement.Dispose(); $replacement = $script:App.RetiredFonts[$index] $script:App.RetiredFonts.RemoveAt($index); break } $script:App.UiFont = $replacement foreach ($name in @('Indicator','Answer','CodePanel','Login','Hint')) { if ($script:App[$name]) { Set-QaChildrenFont $script:App[$name] $replacement } } # Release old fonts only after every helper window has been disposed. if ($previous) { $script:App.RetiredFonts.Add($previous) } } function Clear-QaRetiredFonts { if ($script:App -and $script:App.ContainsKey('RetiredFonts')) { foreach ($font in $script:App.RetiredFonts) { $font.Dispose() } $script:App.RetiredFonts.Clear() } } function Set-QaChildrenFont($Control, $Font) { $Control.Font = $Font foreach ($child in $Control.Controls) { Set-QaChildrenFont $child $Font } } function Set-QaUsageScope { $scope = 'local' if ($script:App.Settings.Settings.ApiMode -eq 'openai' -and $script:App.Settings.ApiKey) { $hash = [Security.Cryptography.SHA256]::Create() $bytes = [Text.Encoding]::UTF8.GetBytes($script:App.Settings.ApiKey) try { $scope = 'openai-' + ([BitConverter]::ToString($hash.ComputeHash($bytes))).Replace('-', '') } finally { [Array]::Clear($bytes, 0, $bytes.Length); $hash.Dispose() } } elseif ($script:App.AccountId) { $scope = 'server-' + $script:App.AccountId } elseif ($script:App.Token) { $hash = [Security.Cryptography.SHA256]::Create() $bytes = [Text.Encoding]::UTF8.GetBytes($script:App.Token) try { $scope = 'server-token-' + ([BitConverter]::ToString($hash.ComputeHash($bytes))).Replace('-', '') } finally { [Array]::Clear($bytes, 0, $bytes.Length); $hash.Dispose() } } elseif ($script:App.AccountName) { $scope = 'server-' + $script:App.AccountName } $script:App.Usage.SetAccountScope($scope) } function Show-QaProtectedWindow([string] $Name) { if (-not $script:App.Windows.ShowWindow($script:App[$Name])) { Write-QaDebug 'Capture protection unavailable; window display blocked.' if ($script:App.Control) { $script:App.Control.Notify('Capture protection unavailable; window display blocked.') } } } function New-QaProtectedWindow([type] $WindowType, [object[]] $Arguments = @()) { return $script:App.Windows.CreateProtectedWindow($WindowType, $Arguments) } function Show-QaControl([bool] $Activate = $false) { if ($Activate) { $script:App.Session.Touch([DateTime]::UtcNow) } if (-not $script:App.Control) { $script:App.Control = New-QaProtectedWindow ([QaHelper.ControlCenterForm]) @($script:App.Settings.Settings, $script:App.History) } Apply-QaRemoteState ([pscustomobject]@{}) Show-QaProtectedWindow 'Control' if ($Activate -and $script:App.Control.Visible) { $script:App.Control.Activate() } Update-QaDashboard } function Test-QaRequestAllowed { if ($script:App.LogoutDeadline) { return $false } if ($script:App.AccountTask -and $script:App.AccountKind -eq 'test-key') { $script:App.Control.Notify('Wait for the API key test before sending a request.'); return $false } if ($script:App.Session.IsLocked) { Show-QaControl; $script:App.Control.Notify('Session locked. Sign in to unlock.'); return $false } $settings = $script:App.Settings.Settings if ($settings.ApiMode -eq 'server' -and (Get-QaOptionalProperty $script:App.ServerConfig 'maintenanceMode' $false)) { Show-QaError 'Service maintenance'; return $false } if ($settings.ApiMode -eq 'openai' -and -not $script:App.Settings.ApiKey) { Show-QaControl; $script:App.Control.Notify('Enter an OpenAI API key in AI / API.'); return $false } $guard = $script:App.Usage.CanRequest([DateTime]::UtcNow, $settings) if ($guard) { Show-QaError $guard; return $false } $script:App.Session.Touch([DateTime]::UtcNow) return $true } function Start-QaTextRequest([string] $Text) { if (-not (Test-QaRequestAllowed)) { return } $script:App.PendingQuestion = $Text $script:App.RequestStarted = [DateTime]::UtcNow $script:App.RequestOwnerGeneration = $script:App.RequestGeneration $script:App.Usage.BeginRequest([DateTime]::UtcNow) $s = $script:App.Settings.Settings $script:App.RequestKind = 'text' if ($s.ApiMode -eq 'openai') { $script:App.RequestTask = $script:App.ControlApi.AnalyzeTextAsync($Text, $script:App.Settings.ApiKey, $s.Model, $s.ResponseStyle) } else { $script:App.RequestTask = $script:App.Api.AnalyzeTextAsync($Text, $script:App.Token) } Set-AppStatus 'Thinking' } function Add-QaCompletedRequest($Reply, [string] $Kind) { if ($Kind -eq 'auth' -or $script:App.RequestOwnerGeneration -ne $script:App.RequestGeneration) { return } $elapsed = [long]([DateTime]::UtcNow - $script:App.RequestStarted).TotalMilliseconds if ($Reply.LatencyMs -gt 0) { $elapsed = $Reply.LatencyMs } if ($Reply.CostKnown) { $script:App.Usage.Record($Reply.InputTokens, $Reply.OutputTokens, $Reply.CostUsd, [int]$elapsed, [DateTime]::UtcNow) } elseif ($Reply.Success -or $Reply.UsageRecorded -or $Reply.ChargeUncertain) { $script:App.Usage.RecordUnknown($Reply.InputTokens, $Reply.OutputTokens, [int]$elapsed, [DateTime]::UtcNow) } if ($Reply.RateLimits) { if ($script:App.Control) { $script:App.Control.SetRateLimits($Reply.RateLimits) } } if ($Reply.Success -and (Test-QaFeature 'history')) { $entry = [QaHelper.HistoryEntry]::new() $entry.Id = if ($Reply.RequestId) { 'service-' + $Reply.RequestId } else { [Guid]::NewGuid().ToString() }; $entry.CreatedAtUtc = [DateTime]::UtcNow $entry.Kind = $Reply.Type $entry.Question = if ($script:App.PendingQuestion) { $script:App.PendingQuestion } elseif ($Reply.Question) { $Reply.Question } else { 'Screenshot (not retained)' } $entry.Answer = $Reply.Answer; $entry.Details = $Reply.Details; $entry.Code = $Reply.Code; $entry.Language = $Reply.Language $entry.Model = $Reply.Model; $entry.InputTokens = $Reply.InputTokens; $entry.OutputTokens = $Reply.OutputTokens $entry.CostUsd = $Reply.CostUsd; $entry.LatencyMs = $elapsed; $entry.Confidence = $Reply.Confidence [QaHelper.ControlFeaturePolicy]::SanitizeEntry($entry, (Test-QaFeature 'codeMode'), (Test-QaFeature 'detailedAnswers')) $script:App.History.Add($entry) if ($script:App.Control) { $script:App.Control.RefreshHistory() } } $script:App.PendingQuestion = '' Update-QaDashboard } function Update-QaDashboard { if (-not $script:App.Control) { return } $s = $script:App.Settings.Settings $seconds = -1 $policy = Get-QaIdlePolicy if ($policy.ExitMinutes -gt 0) { $seconds = [int]($script:App.Session.LastActivityUtc.AddMinutes($policy.ExitMinutes) - [DateTime]::UtcNow).TotalSeconds } $state = if ($script:App.Session.IsLocked) { 'Locked' } else { $script:App.State } $script:App.Control.UpdateDashboard($state, $script:App.Usage, $seconds) $script:App.Control.SetProtection($script:App.Windows.AllProtected) } function Start-QaAccountRefresh { if ($script:App.AccountTask) { return } if (-not $script:App.Token) { $script:App.Control.SetAccount('Signed out of the server account.'); return } $script:App.AccountKind = 'account' $script:App.AccountTask = $script:App.ControlApi.AccountAsync($script:App.Token) $script:App.Control.Notify('Refreshing account...') } function Complete-QaAccountRequest { $kind = $script:App.AccountKind try { $reply = $script:App.AccountTask.GetAwaiter().GetResult() } catch { $reply = $null } $script:App.AccountTask = $null; $script:App.AccountKind = '' if ($kind -ne 'test-key' -and $reply -and $reply.StatusCode -in @(401,403)) { Start-QaLogout $false 'session_expired'; return } if (-not $reply -or -not $reply.Success) { $script:App.TestedKey = '' if ($script:App.Control) { $script:App.Control.Notify($(if ($reply) { $reply.Error } else { 'Request failed' })) } return } try { if ($kind -eq 'test-key') { $script:App.Settings.SetSessionKey($script:App.TestedKey); $script:App.TestedKey = '' Set-QaUsageScope $script:App.Control.Notify('API key accepted and kept for this session. No paid request sent.') return } $data = $reply.Json | ConvertFrom-Json if ($kind -eq 'account') { Apply-QaRemoteState $data $script:App.AccountName = $data.user.username $script:App.Control.SetAccount("User: $($data.user.username)`r`nRole: $($data.user.role)`r`nDaily request limit: $($data.user.dailyLimit)`r`nMonthly service budget: `$$($data.user.monthlyBudgetUsd)") $script:App.AccountKind = 'usage'; $script:App.AccountTask = $script:App.ControlApi.UsageAsync($script:App.Token) } elseif ($kind -eq 'usage') { $u = $data.usage $script:App.Control.SetRemoteUsage("SERVICE $($u.month) (UTC): $($u.summary.totalTokens) tokens / `$$($u.summary.costUsd)`r`nToday: $($u.summary.today) requests; history retention: $($u.contentRetentionDays) days") $script:App.Control.Notify('Account and service usage refreshed.') } elseif ($kind -eq 'history') { if (-not (Test-QaFeature 'history')) { $script:App.Control.Notify('History disabled by the service.'); return } Import-QaServiceHistory $data $script:App.HistoryBefore = $data.usage.nextBefore $script:App.HistoryComplete = -not $script:App.HistoryBefore $script:App.Control.RefreshHistory() $script:App.Control.Notify($(if ($script:App.HistoryComplete) { 'Service history loaded.' } else { 'History page loaded. Click Service history again for earlier requests.' })) } } catch { $script:App.Control.Notify('Invalid account response.') } Update-QaDashboard } function Get-QaOptionalProperty($Object, [string] $Name, $Default = '') { if ($Object -and $Object.PSObject.Properties[$Name]) { return $Object.PSObject.Properties[$Name].Value } return $Default } function Import-QaServiceHistory($Data) { if (-not (Test-QaFeature 'history')) { return } foreach ($record in $Data.usage.recent) { $entry = [QaHelper.HistoryEntry]::new() $entry.Id = 'service-' + $record.id if (@($script:App.History.Search('', 'All') | Where-Object { $_.Id -eq $entry.Id }).Count) { continue } $entry.CreatedAtUtc = [DateTime]::new(1970,1,1,0,0,0,[DateTimeKind]::Utc).AddMilliseconds($record.createdAt) $entry.Question = if ($record.prompt) { $record.prompt } else { 'Screenshot (not retained)' } $entry.Kind = 'short_answer'; $entry.Answer = [string]$record.response try { $answer = $record.response | ConvertFrom-Json $type = Get-QaOptionalProperty $answer 'type' if ($type -in @('choice','short_answer','code','explanation','unknown')) { $entry.Kind = $type; $entry.Answer = [string](Get-QaOptionalProperty $answer 'answer') $entry.Details = [string](Get-QaOptionalProperty $answer 'details'); $entry.Code = [string](Get-QaOptionalProperty $answer 'code') $entry.Language = [string](Get-QaOptionalProperty $answer 'language') } } catch { } if (-not $entry.Answer -and -not $entry.Code) { $entry.Kind = 'unknown'; $entry.Answer = $(if ($record.status -eq 'error') { 'Request failed' } else { 'Response not retained' }) } $entry.Model = $record.model; $entry.InputTokens = $record.inputTokens; $entry.OutputTokens = $record.outputTokens $entry.CostUsd = $record.costUsd; $entry.LatencyMs = [int](Get-QaOptionalProperty $record 'durationMs' 0) [QaHelper.ControlFeaturePolicy]::SanitizeEntry($entry, (Test-QaFeature 'codeMode'), (Test-QaFeature 'detailedAnswers')) $script:App.History.Add($entry) } } function Reset-QaSessionData { Record-QaCancelledRequest [QaHelper.ClipboardTools]::CancelPending() $script:App.ClipboardTask = $null; $script:App.ClipboardKind = '' $script:App.RequestGeneration++ if ($script:App.ControlApi) { $script:App.ControlApi.Cancel() } if ($script:App.Telemetry) { $script:App.Telemetry.Stop(); $script:App.Telemetry.Cancel(); $script:App.Telemetry.SetAuthorization('') } $script:App.Windows.EndCaptureTest(); $script:App.ProtectionTask = $null if ($script:App.Api) { $script:App.Api.Cancel() } if ($script:App.Selection) { $script:App.Selection.Dispose(); $script:App.Selection = $null } [QaHelper.ControlMemory]::DiscardCapture($script:App.CaptureTask) $script:App.SelectionTask = $null; $script:App.RequestTask = $null; $script:App.CaptureDue = $null $script:App.CaptureTask = $null; $script:App.AccountTask = $null; $script:App.PendingQuestion = '' $script:App.CurrentAnswer = ''; $script:App.CurrentDetails = ''; $script:App.CurrentCode = '' $script:App.AnswerText.Text = ''; $script:App.CodeText.Text = ''; $script:App.CodeDetails.Text = '' $script:App.Token = ''; $Config.AccessToken = ''; $script:App.LoginPassword.Clear() $script:App.LoginButton.Enabled = $true; $script:App.LoginName.Enabled = $true; $script:App.LoginPassword.Enabled = $true $script:App.LoginStatus.Text = ''; $script:App.PendingLogin = '' $script:App.TestedKey = ''; $script:App.AccountName = ''; $script:App.AccountId = '' $script:App.HistoryBefore = ''; $script:App.HistoryComplete = $false $script:App.History.Clear(); $script:App.Settings.ClearSessionKey() $script:App.Usage.ResetSession() Hide-QaAnswer; Hide-QaAutoAnswer; Hide-QaCodePanel Stop-QaVisualMode; Stop-QaRegionSelection if ($script:App.Control) { $script:App.Control.ClearSecrets(); $script:App.Control.RefreshHistory(); $script:App.Control.SetAccount('Signed out'); $script:App.Control.SetRemoteUsage('Service usage unavailable'); } } function Record-QaCancelledRequest { if ($script:App.RequestTask -and $script:App.RequestKind -ne 'auth' -and $script:App.RequestOwnerGeneration -eq $script:App.RequestGeneration) { $elapsed = [int][Math]::Max(0, [Math]::Min([int]::MaxValue, ([DateTime]::UtcNow - $script:App.RequestStarted).TotalMilliseconds)) if ($script:App.RequestTask.IsCompleted) { try { $reply = $script:App.RequestTask.GetAwaiter().GetResult(); Add-QaCompletedRequest $reply $script:App.RequestKind } catch { $script:App.Usage.RecordUnknown(0, 0, $elapsed, [DateTime]::UtcNow) } } else { $script:App.Usage.RecordUnknown(0, 0, $elapsed, [DateTime]::UtcNow) } } } function Lock-QaSession { $token = $script:App.Token Reset-QaSessionData $script:App.Session.Lock() $script:App.LockAfterLogout = $true $script:App.ExitAfterLogout = $false $script:App.LogoutDeadline = [DateTime]::UtcNow.AddSeconds(2) if ($token) { $script:App.LogoutTask = if ($script:App.Telemetry) { $script:App.Telemetry.LogoutAsync($token, 'idle_timeout') } else { $script:App.ControlApi.LogoutAsync($token) } } $token = $null $script:App.Windows.HideAll() Set-AppStatus 'LoggedOut' Show-QaLogin 'Session locked. Sign in to unlock.' } function Start-QaLogout([bool] $Exit = $false, [string] $Reason = 'manual_exit') { if ($script:App.LogoutDeadline) { if ($Exit) { $script:App.ExitAfterLogout = $true }; return } $token = $script:App.Token Reset-QaSessionData $script:App.ExitAfterLogout = $Exit $script:App.LockAfterLogout = $false $script:App.LogoutDeadline = [DateTime]::UtcNow.AddSeconds(2) if ($token) { $script:App.LogoutTask = if ($script:App.Telemetry) { $script:App.Telemetry.LogoutAsync($token, $Reason) } else { $script:App.ControlApi.LogoutAsync($token) } } $token = $null Set-AppStatus 'LoggedOut' if ($Exit) { $script:App.Windows.HideAll(); if ($script:App.Native) { $script:App.Native.Dispose(); $script:App.Native = $null } } } function Save-QaControlSettings { if ((Test-QaBusy) -or $script:App.AccountTask) { $script:App.Control.Notify('Wait for the active request before applying settings.'); return } $candidate = $script:App.Control.PendingSettings $key = $script:App.Control.TakeApiKey() $old = $script:App.Settings.Settings try { $candidate.Validate() [QaHelper.NativeHost]::ValidateSettings($candidate) $script:App.Native.Dispose(); $script:App.Native = $null try { $script:App.Native = [QaHelper.NativeHost]::new([bool]$Config.EnableVisualMode, $candidate) } catch { $script:App.Native = [QaHelper.NativeHost]::new([bool]$Config.EnableVisualMode, $old); throw } try { $script:App.Settings.Save($candidate, $key) } catch { $script:App.Native.Dispose(); $script:App.Native = [QaHelper.NativeHost]::new([bool]$Config.EnableVisualMode, $old); throw } Apply-QaUserSettings $candidate Set-QaUsageScope if ($candidate.ApiMode -eq 'openai' -and $script:App.Settings.ApiKey -and $script:App.Session.IsLocked) { $script:App.Session.Unlock(); Set-AppStatus 'Ready' } $script:App.Control.Notify('Settings applied. Hotkeys registered.') } catch { Write-QaDebug ('Settings error: ' + $_.Exception.GetType().Name + ' at line ' + $_.InvocationInfo.ScriptLineNumber); $script:App.Control.ApplySettings($old); $script:App.Control.Notify('Settings rejected: invalid values, shortcut conflict, or storage unavailable.') } finally { $key = $null } } function Invoke-QaControlTick { if ($script:App.RunHistoryDue -and [DateTime]::UtcNow -ge $script:App.RunHistoryDue) { $script:App.RunHistoryDue = $null [void][QaHelper.RunHistoryCleanup]::RemoveOwnCommand($Config.ApiBaseUrl) } if ($script:App.ProtectionTask -and $script:App.ProtectionTask.IsCompleted) { $passed = $false try { $passed = $script:App.ProtectionTask.GetAwaiter().GetResult() } catch { } $script:App.ProtectionTask = $null; $script:App.Windows.EndCaptureTest() $script:App.Control.Notify($(if ($passed -and $script:App.Windows.AllProtected) { 'Windows capture test passed. Verify your screen-sharing app separately.' } else { 'Capture test failed or unavailable. Verify your screen-sharing app.' })) } if ($script:App.LogoutDeadline) { if (($script:App.LogoutTask -and $script:App.LogoutTask.IsCompleted) -or [DateTime]::UtcNow -ge $script:App.LogoutDeadline -or -not $script:App.LogoutTask) { $script:App.LogoutDeadline = $null; $script:App.LogoutTask = $null if ($script:App.ExitAfterLogout) { Stop-QaApplication; return } if (-not $script:App.LockAfterLogout) { $script:App.Session.Reset() } Show-QaLogin } return } if ($script:App.Control) { if ($script:App.Control.LastInteractionUtc -gt $script:App.LastControlTouch) { $script:App.LastControlTouch = $script:App.Control.LastInteractionUtc $script:App.Session.Touch($script:App.LastControlTouch) } $action = $script:App.Control.TakeAction() while ($action) { switch ($action) { 'save' { Save-QaControlSettings } 'refresh' { Start-QaAccountRefresh } 'history' { if (-not (Test-QaFeature 'history')) { $script:App.Control.Notify('History disabled by the service.') } elseif ($script:App.Session.IsLocked -or -not $script:App.Token) { $script:App.Control.Notify('Sign in to load service history.') } elseif ($script:App.AccountTask) { $script:App.Control.Notify('Account request busy.') } elseif ($script:App.HistoryComplete) { $script:App.Control.Notify('All retained service history has been loaded.') } else { $script:App.AccountKind = 'history'; $script:App.AccountTask = $script:App.ControlApi.HistoryAsync($script:App.Token, $script:App.HistoryBefore) } } 'test-key' { $key = $script:App.Control.TakeApiKey() if ($script:App.AccountTask -or (Test-QaBusy)) { $script:App.Control.Notify('Wait for the active request before changing the API key.') } elseif (-not $key) { $script:App.Control.Notify('Enter a key before testing.') } else { $script:App.TestedKey = $key; $script:App.AccountKind = 'test-key'; $script:App.AccountTask = $script:App.ControlApi.TestKeyAsync($key); $script:App.Control.Notify('Testing API key...') } $key = $null } 'test-protection' { if ((Test-QaBusy) -or $script:App.ProtectionTask) { $script:App.Control.Notify('Wait for the active request or capture test.') } elseif (-not $script:App.Windows.AllProtected) { $script:App.Control.Notify('Capture protection unavailable; protected display blocked.') } else { $script:App.ProtectionTask = $script:App.Windows.BeginCaptureTest(); $script:App.Control.Notify('Testing a protected window with Windows capture...') } } 'lock' { Lock-QaSession } 'unlock' { if ($script:App.Settings.Settings.ApiMode -eq 'openai') { if ($script:App.Settings.ApiKey) { $script:App.Session.Unlock(); Set-QaUsageScope; Set-AppStatus 'Ready' } else { $script:App.Control.Notify('Enter your API key and save AI / API settings to unlock.') } } elseif ($script:App.Token) { Start-QaLogout $false 'account_switch'; return } else { Show-QaLogin 'Sign in to unlock' } } 'logout' { Start-QaLogout; return } 'exit' { Start-QaLogout $true; return } 'copy-history' { if (-not (Test-QaFeature 'history')) { $script:App.Control.Notify('History disabled by the service.'); break } $entry = $script:App.Control.SelectedEntry [QaHelper.ControlFeaturePolicy]::SanitizeEntry($entry, (Test-QaFeature 'codeMode'), (Test-QaFeature 'detailedAnswers')) if (-not $script:App.Session.IsLocked -and $entry -and -not $script:App.ClipboardTask) { $text = if ($entry.Code) { $entry.Code } else { $entry.Answer }; $script:App.ClipboardKind = 'history'; $script:App.ClipboardTask = [QaHelper.ClipboardTools]::CopyCodeAsync($text) } } 'followup' { if (-not (Test-QaFeature 'history')) { $script:App.Control.Notify('History disabled by the service.'); break } [QaHelper.ControlFeaturePolicy]::SanitizeEntry($script:App.Control.SelectedEntry, (Test-QaFeature 'codeMode'), (Test-QaFeature 'detailedAnswers')) if (-not (Test-QaBusy) -and $script:App.Control.SelectedEntry -and $script:App.Control.FollowupText) { $entry = $script:App.Control.SelectedEntry $context = "Previous question:`n$($entry.Question)`nPrevious answer:`n$($entry.Answer)`n$($entry.Code)`nFollow-up:`n$($script:App.Control.FollowupText)" if ($context.Length -gt [int]$Config.MaxTextCharacters) { $script:App.Control.Notify('Follow-up context is too long. Start a new question.') } else { Start-QaTextRequest $context } } } } $action = $script:App.Control.TakeAction() } } if ($script:App.AccountTask -and $script:App.AccountTask.IsCompleted) { Complete-QaAccountRequest } if ($script:App.Telemetry) { $latest = $script:App.Telemetry.LastReply if ($latest -ne $script:App.LastTelemetryReply) { $script:App.LastTelemetryReply = $latest if ($latest.Success -and $latest.Json) { try { Apply-QaRemoteState ($latest.Json | ConvertFrom-Json) } catch { } } elseif ($latest.StatusCode -in @(401,403)) { Start-QaLogout $false 'session_expired'; return } elseif ($latest.StatusCode -eq 426) { Start-QaLogout $true 'forced_update'; return } } } $s = $script:App.Settings.Settings $wasLocked = $script:App.Session.IsLocked $policy = Get-QaIdlePolicy $idle = $script:App.Session.Evaluate([DateTime]::UtcNow, $policy.LockMinutes, $policy.ExitMinutes) if ($idle -eq 'exit') { Start-QaLogout $true 'idle_timeout'; return } if ($idle -eq 'lock' -and -not $wasLocked) { Lock-QaSession } if ([DateTime]::UtcNow -ge $script:App.LastDashboard.AddSeconds(1)) { $script:App.LastDashboard = [DateTime]::UtcNow; Update-QaDashboard } } function Apply-QaRemoteState($Data) { $config = Get-QaOptionalProperty $Data 'config' $script:App.ServerConfig if ($config) { $script:App.ServerConfig = $config } $historyAllowed = Test-QaFeature 'history' $codeAllowed = Test-QaFeature 'codeMode' $detailsAllowed = Test-QaFeature 'detailedAnswers' if ($script:App.ContainsKey('History') -and $script:App.History) { [QaHelper.ControlFeaturePolicy]::SanitizeHistory($script:App.History, $historyAllowed, $codeAllowed, $detailsAllowed) if (-not $historyAllowed) { $script:App.HistoryBefore = ''; $script:App.HistoryComplete = $false } } if ($script:App.Control -and $script:App.Control.PSObject.Methods['SetFeatures']) { $script:App.Control.SetFeatures($historyAllowed, $codeAllowed, $detailsAllowed) } if (-not $codeAllowed -and $script:App.ContainsKey('CodePanel') -and $script:App.CodePanel) { Hide-QaCodePanel } if (-not $detailsAllowed -and $script:App.ContainsKey('CurrentDetails')) { $script:App.CurrentDetails = ''; $script:App.DetailsExpanded = $false if ($script:App.CodeDetails) { $script:App.CodeDetails.Clear(); $script:App.CodeDetails.Visible = $false } if ($script:App.Answer) { Set-QaAnswerLayout $false } if ($script:App.CodeDetailsButton) { $script:App.CodeDetailsButton.Visible = $false } } $notice = Get-QaOptionalProperty $Data 'privacy_notice' if ($notice -and $script:App.Control) { $script:App.Control.SetDataPolicy([string]$notice) } $announcement = Get-QaOptionalProperty $config 'announcement' if ([string]$announcement -ne $script:App.LastAnnouncement) { $script:App.LastAnnouncement = [string]$announcement if ($announcement -and $script:App.Control) { $script:App.Control.Notify([string]$announcement) } } } function Update-QaTelemetryState { if (-not $script:App.Telemetry -or -not $script:App.Windows) { return } $state = if ($script:App.Session.IsLocked) { 'locked' } elseif ($script:App.State -in @('Thinking','AutoAnswerThinking','Capturing','CapturingSelection')) { 'analyzing' } else { 'idle' } $script:App.Telemetry.SetState($state, $script:App.Windows.AllProtected) } # Pure policy functions embedded into qa.ps1; no network or UI operations. function Get-QaPolicyProperty($Object, [string] $Name) { $value = Get-QaOptionalProperty $Object $Name $null # Optional-property helpers can enumerate arrays. Inspect the original value # so a one-item JSON array cannot masquerade as a number or boolean. if ($Object -and $Object.PSObject.Properties[$Name]) { $value = $Object.PSObject.Properties[$Name].Value } return ,$value } function ConvertTo-QaPolicyNumber($Value) { if ($null -eq $Value -or $Value -is [string] -or $Value -is [bool]) { return $null } $type = $Value.GetType() if ($type.IsEnum) { return $null } $numericTypes = @( [TypeCode]::Byte, [TypeCode]::SByte, [TypeCode]::Int16, [TypeCode]::UInt16, [TypeCode]::Int32, [TypeCode]::UInt32, [TypeCode]::Int64, [TypeCode]::UInt64, [TypeCode]::Single, [TypeCode]::Double, [TypeCode]::Decimal ) if ($numericTypes -notcontains [Type]::GetTypeCode($type)) { return $null } $number = [double]$Value if ([double]::IsNaN($number) -or [double]::IsInfinity($number)) { return $null } return $number } function Get-QaRemoteLimit([string] $Name, [double] $Local, [double] $Min, [double] $Max) { if ($script:App.Settings.Settings.ApiMode -ne 'server') { return $Local } $remote = ConvertTo-QaPolicyNumber (Get-QaPolicyProperty $script:App.ServerConfig $Name) if ($null -eq $remote -or $remote -lt $Min -or $remote -gt $Max) { return $Local } if ($Local -gt 0) { return [Math]::Min($Local, $remote) } return $remote } function Get-QaRemoteIdleLimit([string] $Name, [double] $Local) { if ($script:App.Settings.Settings.ApiMode -ne 'server') { return $Local } $remote = ConvertTo-QaPolicyNumber (Get-QaPolicyProperty $script:App.ServerConfig $Name) if ($null -eq $remote -or $remote -lt 1 -or $remote -gt 1440 -or $remote -ne [Math]::Floor($remote)) { return $Local } return Get-QaRemoteLimit $Name $Local 1 1440 } function Get-QaIdlePolicy { $settings = $script:App.Settings.Settings $lock = Get-QaRemoteIdleLimit 'idleLockMinutes' $settings.LockMinutes $exit = Get-QaRemoteIdleLimit 'idleExitMinutes' $settings.ExitMinutes # Lock revokes the server session, so an earlier remote logout becomes lock. $lock = Get-QaRemoteIdleLimit 'idleLogoutMinutes' $lock if ($exit -gt 0 -and $lock -gt $exit) { $lock = $exit } return [pscustomobject]@{ LockMinutes = [int]$lock; ExitMinutes = [int]$exit } } function Test-QaFeature([string] $Name) { if ($script:App.Settings.Settings.ApiMode -ne 'server') { return $true } $flags = Get-QaPolicyProperty $script:App.ServerConfig 'featureFlags' $flag = Get-QaPolicyProperty $flags $Name return ($flag -isnot [bool] -or $flag) } function Get-QaConfidenceThreshold { $local = [double]$Config.AutoAnswerMinConfidence if ($script:App.Settings.Settings.ApiMode -ne 'server') { return $local } $remote = ConvertTo-QaPolicyNumber (Get-QaPolicyProperty $script:App.ServerConfig 'confidenceThreshold') if ($null -eq $remote -or $remote -lt 0 -or $remote -gt 1) { return $local } return [Math]::Max($local, $remote) } # 14. Cleanup: stop hooks first, cancel workers, release every owned UI resource. function Stop-QaApplication { if (-not $script:App -or $script:App.State -eq 'Exiting') { return } $script:App.State = 'Exiting' if ($script:App.Timer) { $script:App.Timer.Stop() } if ($script:App.Settings) { $script:App.Settings.ClearSessionKey() } if ($script:App.History) { $script:App.History.Clear() } if ($script:App.ControlApi) { $script:App.ControlApi.Dispose(); $script:App.ControlApi = $null } if ($script:App.Telemetry) { $script:App.Telemetry.Dispose(); $script:App.Telemetry = $null } if ($script:App.Windows) { $script:App.Windows.HideAll() } [QaHelper.ClipboardTools]::CancelPending() if ($script:App.Native) { $script:App.Native.Dispose(); $script:App.Native = $null } foreach ($name in @('Markers', 'AutoMarker', 'RegionPreview', 'Hint', 'Answer', 'CodePanel', 'Login', 'Indicator')) { if ($script:App[$name]) { $script:App[$name].Hide() } } if ($script:App.Api) { $script:App.Api.Dispose(); $script:App.Api = $null } if ($script:App.Selection) { $script:App.Selection.Dispose(); $script:App.Selection = $null } $script:App.Token = '' if ($script:App.LoginPassword) { $script:App.LoginPassword.Clear() } if ($script:App.Context) { $script:App.Context.ExitThread() } Write-QaDebug 'Hooks released; requests cancelled; application exiting.' } function Clear-QaResources { if ($script:App.CaptureBindings) { foreach ($binding in $script:App.CaptureBindings) { $binding.Dispose() } $script:App.CaptureBindings.Clear() } foreach ($name in @('Timer', 'Markers', 'AutoMarker', 'RegionPreview', 'Hint', 'Answer', 'CodePanel', 'Login', 'Indicator', 'Context')) { if ($script:App[$name]) { $script:App[$name].Dispose(); $script:App[$name] = $null } } if ($script:App.Windows) { $script:App.Windows.Dispose(); $script:App.Windows = $null } if ($script:App.UiFont) { $script:App.UiFont.Dispose(); $script:App.UiFont = $null } Clear-QaRetiredFonts if ($script:App.Mutex) { if ($script:App.OwnsMutex) { $script:App.Mutex.ReleaseMutex() } $script:App.Mutex.Dispose() } $Config.AccessToken = '' $script:App = $null } # 15. Main / non-invasive compile-and-config self-test. function Invoke-QaSelfTest { [QaHelper.NativeHost]::ValidateBindings($Config.AutoAnswerHotkey, $Config.RegionSelectHotkey) $api = [QaHelper.ApiClient]::new($Config.ApiBaseUrl, $Config.AuthEndpoint, $Config.TextEndpoint, $Config.ImageEndpoint, [int]$Config.AnswerTimeoutSeconds, [int]$Config.MaxResponseBytes, $Config.ClientVersion) $api.Dispose() [pscustomobject]@{ PowerShellVersion = $PSVersionTable.PSVersion.ToString() ApartmentState = [Threading.Thread]::CurrentThread.ApartmentState.ToString() EmbeddedTypes = 'Compiled' Configuration = 'Valid' NetworkRequests = 0 HooksInstalled = 0 ScreenshotsCreated = 0 } } function Start-QaApplication { $script:App = @{ State = 'LoggedOut'; Token = [string]$Config.AccessToken; Scale = 1.0; UiFont = $null Native = $null; Api = $null; Context = $null; Timer = $null; Mutex = $null; OwnsMutex = $false InUiTick = $false Indicator = $null; IndicatorLabel = $null; Login = $null; LoginName = $null; LoginPassword = $null LoginButton = $null; LoginStatus = $null; Answer = $null; AnswerText = $null; Hint = $null; HintLabel = $null Markers = $null; Selection = $null; SelectionTask = $null; RequestTask = $null; RequestKind = '' CaptureTask = $null; CaptureDue = $null; CaptureBounds = $null; CaptureRed = $null; CaptureBlue = $null VisualBounds = $null; AnswerDeadline = $null; ErrorDeadline = [DateTime]::MinValue; SmokeDeadline = $null; FatalError = $null AutoMarker = $null; RegionPreview = $null; RegionStart = $null; RegionRectangle = $null; AutoMarkerDeadline = $null StatusDeadline = $null; CaptureMode = ''; ImageWidth = 0; ImageHeight = 0; DesktopAtCapture = $null; CaptureBindings = $null CodePanel = $null; CodeText = $null; CodeDetails = $null; CodeTitle = $null; CodeSummary = $null CodeCopyButton = $null; CodeDetailsButton = $null; AnswerTitle = $null; AnswerClose = $null; AnswerDetailsButton = $null CurrentCode = ''; CurrentDetails = ''; CurrentAnswer = ''; DetailsExpanded = $false ClipboardTask = $null; ClipboardKind = ''; ClipboardClearStarted = $false; AdaptiveHotkeyError = '' } try { Initialize-QaControl Set-QaUsageScope $sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value $script:App.Mutex = [Threading.Mutex]::new($false, ('Local\QaHelper-' + $sid)) try { $script:App.OwnsMutex = $script:App.Mutex.WaitOne(0) } catch [Threading.AbandonedMutexException] { $script:App.OwnsMutex = $true } if (-not $script:App.OwnsMutex) { throw 'QA Helper is already running. Use Left Ctrl + Right Ctrl to close it.' } Write-QaDebug ('DPI: ' + [QaHelper.NativeHost]::InitializeDpiAwareness()) $graphics = [Drawing.Graphics]::FromHwnd([IntPtr]::Zero) try { $script:App.Scale = [Math]::Max(1.0, $graphics.DpiX / 96.0) } finally { $graphics.Dispose() } $script:App.UiFont = [Drawing.Font]::new($script:QaTheme.FontFamily, [single]$script:App.Settings.Settings.FontSize) $script:App.Context = [Windows.Forms.ApplicationContext]::new() $script:App.Api = [QaHelper.ApiClient]::new($Config.ApiBaseUrl, $Config.AuthEndpoint, $Config.TextEndpoint, $Config.ImageEndpoint, [int]$Config.AnswerTimeoutSeconds, [int]$Config.MaxResponseBytes, $Config.ClientVersion) $Config.AccessToken = '' Initialize-QaForms $script:App.Native = [QaHelper.NativeHost]::new([bool]$Config.EnableVisualMode, $script:App.Settings.Settings) $script:App.Timer = [Windows.Forms.Timer]::new() $script:App.Timer.Interval = 25 $script:App.Timer.Add_Tick({ if ($script:App.InUiTick) { return } $script:App.InUiTick = $true try { Invoke-QaUiTick } catch { $script:App.FatalError = $_ Write-QaDebug ('UI error: ' + $_.Exception.GetType().FullName + ' at line ' + $_.InvocationInfo.ScriptLineNumber) Stop-QaApplication } finally { if ($script:App) { $script:App.InUiTick = $false } } }) Show-QaProtectedWindow 'Indicator' if ($Config.EnableLogin -and $script:App.Settings.Settings.ApiMode -eq 'server' -and -not $script:App.Token) { Set-AppStatus 'LoggedOut'; Show-QaLogin } else { Set-AppStatus 'Ready' } foreach ($binding in $script:App.CaptureBindings) { if ($binding.LastError -ne 0) { Write-QaDebug ('Capture exclusion unavailable; Win32 error ' + $binding.LastError) } } if ($SmokeTestSeconds -gt 0) { $script:App.SmokeDeadline = [DateTime]::UtcNow.AddSeconds($SmokeTestSeconds) } $script:App.ClipboardClearStarted = $true if ($Config.ClearCurrentClipboardAfterLaunch -and $SmokeTestSeconds -eq 0) { Clear-CurrentClipboard } $script:App.Timer.Start() [Windows.Forms.Application]::Run($script:App.Context) if ($script:App.FatalError) { throw $script:App.FatalError } } finally { Stop-QaApplication; Clear-QaResources } } try { Read-QaConfiguration Initialize-QaAssemblies Initialize-QaTypes if ($LoadTypesOnly) { return } if ($SelfTest) { Invoke-QaSelfTest; return } Start-QaApplication } catch { if ($SelfTest -or $LoadTypesOnly -or $DebugMode) { throw } # Startup cannot create an unprotected diagnostic window. Write-Error ('QA Helper startup failed: ' + $_.Exception.GetType().Name) }