param( [string]$HelperExe = "runs/win-helper/ISphereWinHelper.exe", [int]$WaitSeconds = 10, [int]$KeepOpenSeconds = 0, [int]$X = 160, [int]$Y = 120, [int]$Width = 760, [int]$Height = 560, [ValidateSet("Visible", "Offscreen", "Minimized")] [string]$WindowMode = "Visible", [switch]$ProbeClick, [switch]$NoScreenshot, [switch]$NoActivate, [switch]$TopMost ) $ErrorActionPreference = "Stop" $repo = Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..") $helperPath = Join-Path $repo $HelperExe $runOut = Join-Path $repo "runs\offline-chat-window" New-Item -ItemType Directory -Force -Path $runOut | Out-Null function Resolve-RepoPath([string]$PathValue) { if ([System.IO.Path]::IsPathRooted($PathValue)) { return $PathValue } return (Join-Path $repo $PathValue) } function Invoke-HelperJson([string]$Op, [hashtable]$OpArgs, [string]$RequestId, [int]$TimeoutMs = 5000) { $request = @{ protocol = "isphere.helper.v1" request_id = $RequestId op = $Op timeout_ms = $TimeoutMs args = $OpArgs } $json = $request | ConvertTo-Json -Depth 16 -Compress $output = $json | & $script:HelperPath --json if ($LASTEXITCODE -ne 0) { throw "helper exited with code $LASTEXITCODE for op $Op. Output: $output" } try { return $output | ConvertFrom-Json } catch { throw "helper output was not JSON for op $Op`: $output" } } function Add-NativeWindowApi { Add-Type @" using System; using System.Runtime.InteropServices; public static class ISphereOfflineChatProbeWindowApi { [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); } "@ -ErrorAction SilentlyContinue } function Set-ProbeWindowPlacement([string]$HwndText) { $hwndValue = [Convert]::ToInt64(($HwndText -replace "^0x", ""), 16) $hwnd = [IntPtr]::new($hwndValue) $insertAfter = if ($TopMost) { [IntPtr]::new(-1) } else { [IntPtr]::Zero } $flags = 0x0040 if ($NoActivate) { $flags = $flags -bor 0x0010 } if ($WindowMode -eq "Minimized") { [ISphereOfflineChatProbeWindowApi]::ShowWindow($hwnd, 6) | Out-Null return } $effectiveX = $X $effectiveY = $Y if ($WindowMode -eq "Offscreen") { $effectiveX = -32000 $effectiveY = -32000 } [ISphereOfflineChatProbeWindowApi]::ShowWindow($hwnd, 9) | Out-Null [ISphereOfflineChatProbeWindowApi]::SetWindowPos($hwnd, $insertAfter, $effectiveX, $effectiveY, $Width, $Height, [uint32]$flags) | Out-Null if (-not $NoActivate) { [ISphereOfflineChatProbeWindowApi]::SetForegroundWindow($hwnd) | Out-Null } } function Save-WindowScreenshot([string]$OutputPath, [int]$ScreenX, [int]$ScreenY, [int]$CaptureWidth, [int]$CaptureHeight) { Add-Type -AssemblyName System.Drawing $bitmap = New-Object System.Drawing.Bitmap $CaptureWidth, $CaptureHeight $graphics = [System.Drawing.Graphics]::FromImage($bitmap) try { $graphics.CopyFromScreen(($ScreenX - 20), ($ScreenY - 20), 0, 0, $bitmap.Size) $bitmap.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Png) } finally { $graphics.Dispose() $bitmap.Dispose() } } function New-Sha256Hex([string]$Value) { $sha = [System.Security.Cryptography.SHA256]::Create() try { $bytes = [System.Text.Encoding]::UTF8.GetBytes($Value) return -join ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString("x2") }) } finally { $sha.Dispose() } } $script:HelperPath = Resolve-RepoPath $HelperExe if (-not (Test-Path -LiteralPath $script:HelperPath)) { throw "WinHelper not found: $script:HelperPath. Run scripts\build-win-helper.ps1 first." } $timestamp = (Get-Date).ToString("yyyyMMdd-HHmmss") $windowTitle = "iSphere Offline Chat Window Probe " + ([guid]::NewGuid().ToString("N").Substring(0, 8)) $launcherScript = Join-Path $runOut ("offline-chat-window-probe-$timestamp.ps1") $markerPath = Join-Path $runOut ("offline-chat-window-probe-marker-$timestamp.json") $statePath = Join-Path $runOut ("offline-chat-window-probe-state-$timestamp.json") $dumpFile = Join-Path $runOut ("uia-dump-offline-chat-window-probe-$timestamp.json") $screenshotFile = Join-Path $runOut ("offline-chat-window-probe-$timestamp.png") $escapedTitle = $windowTitle.Replace("'", "''") $escapedMarker = $markerPath.Replace("'", "''") $escapedState = $statePath.Replace("'", "''") $formScript = @" Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing `$form = New-Object System.Windows.Forms.Form `$form.Name = 'frmP2PChat' `$form.Text = '$escapedTitle' `$form.StartPosition = 'Manual' `$form.Location = New-Object System.Drawing.Point(160, 120) `$form.Size = New-Object System.Drawing.Size(760, 560) `$form.KeyPreview = `$true `$layout = New-Object System.Windows.Forms.TableLayoutPanel `$layout.Name = 'chatRootLayout' `$layout.Dock = 'Fill' `$layout.RowCount = 5 `$layout.ColumnCount = 1 `$layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 38))) | Out-Null `$layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 55))) | Out-Null `$layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 42))) | Out-Null `$layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 45))) | Out-Null `$layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 48))) | Out-Null `$form.Controls.Add(`$layout) `$search = New-Object System.Windows.Forms.TextBox `$search.Name = 'skinAlphaTxt' `$search.Text = 'search contact placeholder' `$search.Dock = 'Fill' `$layout.Controls.Add(`$search, 0, 0) `$recv = New-Object System.Windows.Forms.RichTextBox `$recv.Name = 'rtbRecvMessage' `$recv.ReadOnly = `$true `$recv.Text = 'Offline chat window probe. This mimics frmP2PChat for UIA identify, locate, and click verification only.' `$recv.Dock = 'Fill' `$layout.Controls.Add(`$recv, 0, 1) `$toolbar = New-Object System.Windows.Forms.FlowLayoutPanel `$toolbar.Name = 'chatToolbar' `$toolbar.Dock = 'Fill' `$toolbar.FlowDirection = 'LeftToRight' `$layout.Controls.Add(`$toolbar, 0, 2) `$file = New-Object System.Windows.Forms.Button `$file.Name = 'btnSendFile' `$file.Text = 'Send File' `$file.Width = 92 `$file.Height = 30 `$file.Add_Click({ `$payload = [ordered]@{ clicked = `$true action = 'file_button_probe_only' sent_real_message = `$false uploaded_real_file = `$false at = (Get-Date).ToString('o') } | ConvertTo-Json -Compress Set-Content -LiteralPath '$escapedMarker' -Value `$payload -Encoding UTF8 }) `$toolbar.Controls.Add(`$file) `$offline = New-Object System.Windows.Forms.Label `$offline.Name = 'offlineSendBlocker' `$offline.Text = 'Offline probe only: no server connection and no real message is sent.' `$offline.AutoSize = `$true `$offline.Padding = New-Object System.Windows.Forms.Padding(8) `$toolbar.Controls.Add(`$offline) `$send = New-Object System.Windows.Forms.RichTextBox `$send.Name = 'rtbSendMessage' `$send.Text = '' `$send.Dock = 'Fill' `$layout.Controls.Add(`$send, 0, 3) `$buttonPanel = New-Object System.Windows.Forms.FlowLayoutPanel `$buttonPanel.Name = 'sendButtonPanel' `$buttonPanel.Dock = 'Fill' `$buttonPanel.FlowDirection = 'RightToLeft' `$layout.Controls.Add(`$buttonPanel, 0, 4) `$sendButton = New-Object System.Windows.Forms.Button `$sendButton.Name = 'btnSend' `$sendButton.Text = 'Send(S)' `$sendButton.Width = 92 `$sendButton.Height = 32 `$sendButton.Add_Click({ `$payload = [ordered]@{ clicked = `$true action = 'send_button_probe_only' text = `$send.Text text_length = `$send.Text.Length sent_real_message = `$false uploaded_real_file = `$false at = (Get-Date).ToString('o') } | ConvertTo-Json -Compress Set-Content -LiteralPath '$escapedMarker' -Value `$payload -Encoding UTF8 }) `$buttonPanel.Controls.Add(`$sendButton) `$form.Add_Shown({ `$state = [ordered]@{ ok = `$true window_kind = 'offline_chat_window_probe' automation_id = `$form.Name title = `$form.Text process_id = [System.Diagnostics.Process]::GetCurrentProcess().Id marker_path = '$escapedMarker' at = (Get-Date).ToString('o') } | ConvertTo-Json -Compress Set-Content -LiteralPath '$escapedState' -Value `$state -Encoding UTF8 }) [void]`$form.ShowDialog() "@ Set-Content -LiteralPath $launcherScript -Value $formScript -Encoding UTF8 $process = Start-Process powershell -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $launcherScript) -PassThru $candidate = $null $scan = $null $deadline = (Get-Date).AddSeconds([Math]::Max(1, $WaitSeconds)) do { Start-Sleep -Milliseconds 300 $scan = Invoke-HelperJson -Op "scan_windows" -OpArgs @{ include_all_visible = $true } -RequestId "offline-chat-probe-scan" if ($scan.ok -and $scan.data.windows) { $candidate = $scan.data.windows | Where-Object { $_.pid -eq $process.Id -and $_.title -eq $windowTitle } | Select-Object -First 1 } } while (-not $candidate -and (Get-Date) -lt $deadline) if (-not $candidate) { if ($process -and -not $process.HasExited) { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } throw "offline chat probe window not found. Last scan: $($scan | ConvertTo-Json -Depth 12 -Compress)" } Add-NativeWindowApi Set-ProbeWindowPlacement -HwndText $candidate.hwnd Start-Sleep -Milliseconds 500 $dump = Invoke-HelperJson -Op "dump_uia" -OpArgs @{ hwnd = $candidate.hwnd max_depth = 8 max_children = 200 include_text = $true } -RequestId "offline-chat-probe-dump-uia" $dump | ConvertTo-Json -Depth 40 | Set-Content -LiteralPath $dumpFile -Encoding UTF8 $classify = Invoke-HelperJson -Op "probe_send_uia_controls" -OpArgs @{ hwnd = $candidate.hwnd max_depth = 8 max_children = 200 } -RequestId "offline-chat-probe-classify-uia" $probeClickedUi = $false $probeTypedText = $false $probeMarkerFound = $false $probeMarkerTextMatches = $false $probeMarker = $null $probeContent = "" $probeAction = $null if ($ProbeClick) { $probeContent = "Codex offline chat probe " + ([guid]::NewGuid().ToString("N").Substring(0, 8)) $probeHash = New-Sha256Hex $probeContent $probeAction = Invoke-HelperJson -Op "uia_send_message" -OpArgs @{ hwnd = $candidate.hwnd send_editor_automation_id = "rtbSendMessage" send_button_automation_id = "btnSend" target_ref = "offline-chat-window-probe" content_text = $probeContent content_sha256 = $probeHash } -RequestId "offline-chat-probe-click" $probeClickedUi = [bool]($probeAction.ok -and $probeAction.data.clicked_ui) $probeTypedText = [bool]($probeAction.ok -and $probeAction.data.typed_text) for ($i = 0; $i -lt 30 -and -not (Test-Path -LiteralPath $markerPath); $i++) { Start-Sleep -Milliseconds 100 } $probeMarkerFound = Test-Path -LiteralPath $markerPath if ($probeMarkerFound) { $probeMarker = Get-Content -LiteralPath $markerPath -Raw | ConvertFrom-Json $probeMarkerTextMatches = [string]$probeMarker.text -eq $probeContent } } $screenshotPathForOutput = $null if (-not $NoScreenshot -and $WindowMode -eq "Visible") { Save-WindowScreenshot -OutputPath $screenshotFile -ScreenX $X -ScreenY $Y -CaptureWidth ($Width + 40) -CaptureHeight ($Height + 40) $screenshotPathForOutput = $screenshotFile } if ($KeepOpenSeconds -gt 0) { Start-Sleep -Seconds $KeepOpenSeconds if ($process -and -not $process.HasExited) { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } } $rootAutomationId = "" if ($dump -and $dump.ok -and $dump.data -and $dump.data.root) { $rootAutomationId = [string]$dump.data.root.automation_id } $flags = $null if ($classify -and $classify.ok -and $classify.data -and $classify.data.flags) { $flags = $classify.data.flags } [ordered]@{ ok = [bool]($dump.ok -and $classify.ok) window_kind = "offline_chat_window_probe" process_id = $process.Id hwnd = $candidate.hwnd title = $windowTitle root_automation_id = $rootAutomationId root_control_type = if ($dump.ok) { [string]$dump.data.root.control_type } else { "" } send_editor_found = [bool]($flags -and $flags.has_send_editor) send_button_found = [bool]($flags -and $flags.has_send_button) file_button_found = [bool]($flags -and $flags.has_file_menu) receive_document_found = [bool]($flags -and $flags.has_receive_document) offline_blocker_visible = [bool]($flags -and $flags.offline_blocker_visible) route_hint = if ($flags) { [string]$flags.route_hint } else { "" } probe_click_requested = [bool]$ProbeClick probe_clicked_ui = $probeClickedUi probe_typed_text = $probeTypedText probe_click_marker_found = $probeMarkerFound probe_marker_text_matches = $probeMarkerTextMatches probe_action_ok = if ($probeAction) { [bool]$probeAction.ok } else { $false } probe_action_mode = if ($probeAction -and $probeAction.ok) { [string]$probeAction.data.action_mode } else { "" } sent_real_message = $false uploaded_real_file = $false marker_path = $markerPath state_path = $statePath launcher_script = $launcherScript uia_dump_file = $dumpFile screenshot_file = $screenshotPathForOutput alive_after_return = if ($KeepOpenSeconds -gt 0) { $false } else { $null -ne (Get-Process -Id $process.Id -ErrorAction SilentlyContinue) } } | ConvertTo-Json -Depth 16