---
id: standard-appbot-window
app: wkappbot
description: "Complete reference for the Standard AppBot Window concept. Covers what it is, how to detect it via ancestor walk only, IPC forwarding, hidden ConPTY handling, multi-tab terminals, iconified callers, Chrome placement, and all downstream consumers. Read this before touching any callerHwnd code."
---

> **Refresh**: `wkappbot skill read standard-appbot-window --if-newer` — v1.19 (2026-05-15)

# Standard AppBot Window: definition, detection, IPC forwarding, placement

## Steps

1. DEFINITION -- Standard AppBot Window: The window owned by the NEAREST ancestor process in the wkappbot process chain that owns ANY window with a valid non-zero GetWindowRect. This is the window FROM WHICH wkappbot was launched. It is the correct target for Chrome placement and input injection.
2. FOREGROUND IS IRRELEVANT: The focused/foreground window has nothing to do with the AppBot caller. GetForegroundWindow() is always wrong for this purpose. The user may be looking at YouTube, a PDF, anything -- that is someone else's window. Never use P1 (foreground terminal-class check) or P3 (GetForegroundWindow fallback). Foreground fallbacks removed in commit 08b0b423.
3. DETECTION -- ancestor walk only (P2): Walk GetParentProcessId chain up to 8 levels from wkappbot.exe. For each ancestor PID call EnumWindows and find windows where GetWindowThreadProcessId == ancestor PID. Filter: IsWindow + GetWindowRect returns non-zero area. NO class restriction. NO IsWindowVisible requirement. Return the FIRST match from the CLOSEST ancestor. If no ancestor has a usable window: return Zero -- do NOT fall back to foreground.
4. HIDDEN WINDOWS (ConPTY PseudoConsoleWindow): ConPTY is the nearest ancestor when wkappbot runs in a terminal tab. Class=PseudoConsoleWindow, IsWindowVisible=false, no title -- but IsWindow=true and GetWindowRect returns valid non-zero rect. P2 finds it (no visibility filter). For Chrome PLACEMENT coordinates: use GetAncestor(hwnd, GA_ROOT) to get the root visible ancestor (e.g. WindowsTerminal.exe main HWND) and use THAT rect. Pattern: hForRect = IsWindowVisible(hwnd) ? hwnd : GetAncestor(hwnd, GA_ROOT). Original hidden hwnd kept as identity for input injection. See AskCommands.CallerPlacement.cs FindRectProviderHwnd.
5. MULTI-TAB TERMINAL: Ancestor chain goes shell -> ConPTY server (for the specific AppBot tab) -> WindowsTerminal.exe. ConPTY for the AppBot tab is the nearest ancestor window. GA_ROOT gives WindowsTerminal main HWND for placement -- correct regardless of which tab is active. No need to identify the active tab.
6. ICONIFIED CALLER: If the AppBot terminal is minimized -- TryMoveChromeToCaller skips placement. Chrome stays at its last valid position. Minimized terminal = background/batch run = moving Chrome would be disruptive. Do NOT fall back to foreground. Do NOT move Chrome to screen center.
7. IPC FORWARDING via __hwnd: EyeCmdPipeClient.TryDelegate calls ResolveCallerTerminalHwnd() before sending to Eye pipe. If hwnd != Zero, prepends __hwnd:0xHHHHHHHH to JSON args. EyeCmdPipeServer.HandleClientAsync strips prefix, validates IsWindow, sets CallerHwnd AsyncLocal + ChromeLauncher.CallerHwndForPlacement static (reset to Zero when __hwnd absent). ONE __hwnd only -- no __conhwnd needed. Use the single hwnd for both placement (via GA_ROOT rect) and input injection.
8. DOWNSTREAM CONSUMERS: (1) TryMoveChromeToCallerAsync -- Chrome placement on every ask/cdp command (AskCommands.CallerPlacement.cs). (2) ChromeLauncher.CallerHwndForPlacement -- static anchor for next cdp open. (3) MCP worker via _meta.callerHwnd JSON (EyeMcpClient). (4) Admin Eye via ElevatedEyeServer.Execute.cs (fixed d557c6486). (5) WKAPPBOT_CALLER_HWND env var to Core subprocess via CreateProcessW env block.
9. BUGS FIXED (do not regress): Gap A stale CallerHwndForPlacement reset (7c19f50cb). Gap B EyeMcpClient static leak (d7bc05191). Gap G IsWindow validation on pipe parse (d7bc05191). Gap I MCP _meta validation (d7bc05191). Gap D/J Admin Eye --sudo (d557c6486). P1/P3 foreground removed (08b0b423). P2 hidden-window support (e450655a). GA_ROOT simplification (6d656296d). WithAutoReconnectAsync on CDP DOM calls (26dd30c8). Chrome placement on every ask command (2ca0500a7).
10. WHAT NOT TO DO: Never use GetForegroundWindow() for caller detection. Never require IsWindowVisible in P2. Never use two IPC fields (__hwnd + __conhwnd) -- one hwnd handles all use cases. Never fall back to foreground when Zero returned -- skip and keep Chrome in place. Never reject ConPTY by class name in P2. Never use negative x coordinate as off-screen signal -- use MonitorFromPoint.
11. DPI-AWARE COORDINATE PIPELINE: AppBot window rect (GetWindowRect) returns PHYSICAL pixel coordinates when process is PerMonitorV2 DPI-aware (set via TrySetPerMonitorV2DpiAwareness at Launcher startup). Three DPI values matter: (1) callerDpi = GetDpiForWindow(callerHwnd) -- DPI of the monitor hosting the caller window. (2) chromeDpi = GetDpiForWindow(chromeHwnd via Win32) -- DPI of the monitor hosting Chrome. (3) System DPI = GetDpiForSystem() -- baseline 96dpi reference. All placement calculations must stay in PHYSICAL pixels end-to-end. Do not mix logical and physical coordinates. MonitorFromPoint + GetMonitorInfo.rcWork always returns physical coords when process is PerMonitorV2 aware. ClampBoundsToVisible operates in physical coords. If caller and Chrome are on different DPI monitors, the raw rect coords are already correct in physical space -- no scaling needed between monitors when using physical coords throughout.
12. DPI verified 2026-05-16: wkappbot-core.exe now sets PerMonitorV2 awareness in Program.Dispatch.Run() via NativeMethods.TrySetPerMonitorV2DpiAwarenessOnce(). Caller GetWindowRect + CDP Browser.setWindowBounds + Win32 SetWindowPos all exchange PHYSICAL pixels end-to-end. No LogicalToPhysical conversion needed in TryMoveChromeToCallerAsync.
13. 2026-05-20 BUG FOUND: MyCdpContext.cs:300 LastValidatedCallerRect = TryGetWindowRectLTRB(callerHwnd) -- no GA_ROOT fallback for PseudoConsoleWindow. Standard pattern: rectHwnd = IsWindowVisible(callerHwnd) ? callerHwnd : GetAncestor(callerHwnd, GA_ROOT=2). Fix: add GetAncestor DllImport to MyCdpContext.Window.cs, apply hForRect pattern at MyCdpContext.cs:300.
14. 2026-05-20 FIX PLAN: MyCdpContext.cs callerHwnd priority must skip consoleHwnd when GetWindowRect=(0,0,0,0). Under ConPTY, GetConsoleWindow() returns PseudoConsoleWindow with zero rect -- using it as caller blocks ancestorHwnd (CASCADIA_HOSTING_WINDOW_CLASS with valid rect from P2 walk). Fix: validate consoleHwnd rect before using. Correct callerHwnd = CASCADIA found by P2.
15. 2026-05-20 ROOT CAUSE: ResolveValidCallerWindow() largest-terminal fallback non-deterministic -- EnumWindows picks largest KnownHostProcess window. When user moves/resizes windows, 'largest' changes per command -> Chrome placement flies. FIX: remove largest-terminal fallback. Zero return = placement skip (Chrome stays). P2 ancestor walk is the only correct path.
16. Verified 2026-05-21: experience/tests/a11y/find/test-a11y-find-domain-youtube-verify.sh (resolve ts=2026-05-21T01:18:05.9925140Z)
17. ACTIVE BUG 2026-05-22: CallerPlacementResolver.cs lines 309-316 has 'prefer large windows >= 200x100' heuristic that still contains non-deterministic fallback -- when multiple large KnownHostProcess windows exist, EnumWindows ordering determines which one Chrome moves to. This causes Chrome to fly to wrong window. FIX: remove all non-P2-ancestor heuristics. Only use the hwnd passed in from P2 walk. If TryGetCallerRect fails, return false (placement skip). Do NOT enumerate windows to find 'large' ones.
18. Caller-placement resolver (CallerPlacementResolver / ResolveValidCallerWindow): MUST NOT enumerate KnownHost windows and pick largest -- EnumWindows ordering is non-deterministic so wrong window gets Chrome. Only P2 ancestor walk + isBig(>=200x100) at placement-rect calculation time. hwnd=0 or tiny-rect => skip placement entirely, never substitute another window.
19. DIAGNOSTIC SCRIPT PATTERN (2026-05-22): wkp2sim.ps1 simulates P2 walk for all claude.exe processes. Uses Get-WmiObject Win32_Process for parent PID chain + Add-Type Win32 EnumWindows to find first visible window >= 200x100 per ancestor PID. Shows which HWND would be sent as __hwnd to Eye (EyeCmdPipeClient selection) vs first any-rect window (CallerPlacementResolver). Useful for diagnosing placement bugs: run after fix to verify correct terminal is selected.
20. CALLER-HWND PIPE LOGGING (2026-05-24, commit 6a4a4044e): stderr traces added at every transfer point. Launcher (EyeCmdPipeClient.WindowDetection.cs + .cs): [LAUNCHER:HWND] ancestor-pids=N WT_SESSION=... | [LAUNCHER:HWND] EnumWindows: wtMatch=0xH zMatch=0xH | [LAUNCHER:HWND] best=0xH cls=CLASSNAME onScreen=T/F | [LAUNCHER:HWND] no usable ancestor window -- returning Zero | [LAUNCHER:HWND] resolved=0xH piped=T/F. Eye (EyeCmdPipeServer.Pipe.cs): [EYE:HWND-RX] ipc_hwnd=0xH/(absent) cwd=... | [EYE:HWND-RX] __hwnd=H accepted=0xH. Diagnostic grep: Select-String 'LAUNCHER:HWND|EYE:HWND' bin\wkappbot.hq\logs\eye.pid=*.log -- shows full pipe chain in sequence.
21. FORBIDDEN-PATTERN FIX (2026-05-24, cd3e8c116, Opus): Removed dead GetForegroundWindow P/Invoke from ChromeLauncher.Win32.cs. Deleted TryRecoverCallerFromParentProcess (Process.MainWindowHandle-based, violates P2 rule) from ChromeLauncher.CallerRecovery.cs (-134 lines). Zero=placement-skip principle fully enforced. Other 5 files (LaunchPreparation/SessionRestore/CallerRecovery/ChromeLauncher/AskCommands.CallerPlacement) were already clean. Build 0 errors 0 warnings v7.3.213.
22. HIDDEN-WINDOW GA_ROOT LIMITATION (2026-05-22 + fix 946355351): GA_ROOT(ConPTY)=ConPTY itself on current Windows Terminal -- the window is top-level unowned so GetAncestor(GA_ROOT) is a no-op. The existing hForRect=GA_ROOT pattern (step 4) works for child/owned ConPTY but NOT top-level ConPTY. Fix in CallerPlacementResolver.TryGetCallerRect (CallerPlacementResolver.PidChain.cs 946355351): after GA_ROOT fails or returns tiny rect, walk the window's owning PROCESS ancestor chain via FindBigVisibleAncestorWindow(callerPid): for each ancestor PID closest-first, EnumWindows for IsWindowVisible + isBig(>=200x100) + non-Chrome window. First match wins. Telemetry prefix: [PLACEMENT:RESOLVE] hidden-window pid-chain pid=N -> 0xH rect=(...). If all fallbacks fail, TryGetCallerRect returns false -> placement skip (Chrome stays put). KEY RULE: pass the hidden ConPTY hwnd anyway via __hwnd; placement code resolves the actual visible rect downstream.
