Fix ui send-keys --via post-message silently dropping input#666
Conversation
post-message posted WM_CHAR/WM_KEYDOWN to the top-level window, which does not route keyboard messages to the focused control, so keystrokes never reached the target even though the command reported success and exited 0. - Retarget post-message to the target thread's focused child HWND via GetGUIThreadInfo so classic Win32/WinForms edit controls receive input. - Broaden the delivery warning to fire for all keys (not just literal text) when the effective target is a windowless WinUI 3/UWP/XAML host; surface it in JSON warnings[] and reword post-message success to "Posted" (fire-and-forget). - Add GetGUIThreadInfo P/Invoke and ISystemUiQuery.GetFocusedWindow, with tests and updated docs/skills. Fixes #655 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes issue #655 by retargeting post-message input and improving unsupported-XAML reporting.
Changes:
- Posts keys to the target thread’s focused child HWND.
- Adds XAML delivery warnings and clearer success output.
- Expands tests and regenerates documentation artifacts.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/winapp-npm/src/winapp-commands.ts |
Updates generated transport documentation. |
src/winapp-CLI/WinApp.Cli/Services/SystemUiQuery.cs |
Adds focused-window native lookup. |
src/winapp-CLI/WinApp.Cli/Services/ISystemUiQuery.cs |
Exposes focused-window lookup. |
src/winapp-CLI/WinApp.Cli/NativeMethods.txt |
Adds required Win32 APIs. |
src/winapp-CLI/WinApp.Cli/Commands/UiSendKeysCommand.cs |
Retargets input and reports delivery limitations. |
src/winapp-CLI/WinApp.Cli.Tests/UiCommandTests.SendKeys.cs |
Tests retargeting and warnings. |
src/winapp-CLI/WinApp.Cli.Tests/SystemUiQueryTests.cs |
Tests the native seam. |
src/winapp-CLI/WinApp.Cli.Tests/InjectionGuardTests.cs |
Updates warning-gate tests. |
src/winapp-CLI/WinApp.Cli.Tests/FakeUiServices.cs |
Adds fake focused-window data. |
docs/ui-automation.md |
Documents transport behavior. |
docs/npm-usage.md |
Regenerates npm API documentation. |
docs/fragments/skills/winapp-cli/ui-automation.md |
Updates skill source guidance. |
docs/cli-schema.json |
Regenerates CLI schema. |
.github/plugin/skills/winapp-cli/ui-automation/SKILL.md |
Regenerates plugin skill guidance. |
.github/plugin/agents/winapp.agent.md |
Updates agent command guidance. |
.claude/skills/winapp-ui-automation/SKILL.md |
Synchronizes Claude skill guidance. |
.claude/agents/winapp.md |
Synchronizes Claude agent guidance. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Resolved the ui-automation agent-doc conflict by keeping #660's "app" -> "screen" coordinate terminology on the ui drag line and the send-keys windowless/post-message caveat from this branch. Regenerated all auto-generated docs/skills (cli-schema.json, plugin + .claude skills, npm-usage.md, winapp-commands.ts) from the merged CLI; restored plugin.json version to match main. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…review) GetGUIThreadInfo reports keyboard focus for the entire GUI thread, and one UI thread can own multiple top-level windows. If SetForegroundWindow was denied, the reported focus could belong to a different window on that thread, causing post-message to deliver keys to the wrong window despite an explicit target. Add ISystemUiQuery.GetRootWindow (GetAncestor GA_ROOT) and only retarget when the focused HWND shares the target's top-level root; otherwise keep the resolved target. Covered by a new negative test plus updated seam/retarget tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The merge regeneration used a plain 'dotnet publish' CLI binary (which defaults to version 1.0.0) instead of a versioned build, baking 1.0.0 into cli-schema.json, winapp-commands.ts, and every skill's YAML frontmatter. validate-llm-docs normalizes the fresh version to version.json's base (0.4.1) and so flagged the committed docs as drifted. Restore the version marker to 0.4.1 to match version.json and origin/main. validate-llm-docs.ps1 -FailOnDrift and sync-claude-plugin.ps1 -Check both pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Build Metrics ReportBinary Sizes
Test Results✅ 3550 passed, 4 skipped out of 3554 tests in 547.5s (+10 tests, +6.5s vs. baseline) Test Coverage✅ 95.2% line coverage, 89.3% branch coverage · ✅ no change vs. baseline CLI Startup Time45ms median (x64, Updated 2026-07-17 17:05:51 UTC · commit |
zateutsch
left a comment
There was a problem hiding this comment.
Some minor things, M1 may be worth a follow up. Approving as nothing seems to be a merge blocker.
PR Review — nmetulev-fix-ui-send-keys-post-message vs origin/main (5 commits, 17 files, +355/-60 lines)
Summary
Critical: 0 High: 0 Medium: 4 Low: 1
Coverage
security ✓ clean
correctness ✓ clean
cli-ux ⚠ 1 finding
alternative-solution ⚠ 1 finding
necessity-simplicity n/a (no new surface — bug fix; --via option unchanged)
test-coverage ⚠ 3 findings
docs-and-samples ✓ clean
packaging ✓ clean
multi-model ✓ 0 critical/high to reconcile (none raised) · models: gpt (gpt-5.4)
regression ✓ send-input path unchanged; retarget is opt-in for post-message only
validation ✓ build clean (0 warnings) + all SendKeys/SystemUiQuery/InjectionGuard tests pass
Findings
M1 src/winapp-CLI/WinApp.Cli/Commands/UiSendKeysCommand.cs:331-338 cli-ux --json hwnd now reports the retargeted focused-child HWND, not the resolved target
M2 src/winapp-CLI/WinApp.Cli/Services/SystemUiQuery.cs:91-102 alternative-solution GetWindowSize duplicates IUiAutomationService.TryGetWindowRect
M3 src/winapp-CLI/WinApp.Cli.Tests/UiCommandTests.SendKeys.cs:416-454 test-coverage Effective-child-XAML warning path (top-level non-XAML, focused child XAML) untested
M4 src/winapp-CLI/WinApp.Cli.Tests/UiCommandTests.SendKeys.cs:436-479 test-coverage targetRoot==0 retarget-suppression guard untested
L1 src/winapp-CLI/WinApp.Cli/Commands/UiSendKeysCommand.cs:352-356 test-coverage "Posted"/"Sent" plain-log wording not asserted
Details
M1 src/winapp-CLI/WinApp.Cli/Commands/UiSendKeysCommand.cs:331-338
- Severity: medium
- Confidence: medium
- Validation: validated (build + tests confirm behavior; contract impact is judgment)
- Domain: cli-ux
- Multi-model: reviewed, not escalated to high
- Finding: The --json
hwndfield now returnseffectiveHwnd(the retargeted focused child), not the resolved app/window/selector target. Scripts that readhwndto correlate the window they requested will silently get a different (child) HWND. - Evidence:
targetHwndresolved at 162/176;effectiveHwndreassigned to the focused child at ~194-212; JSON result setsHwnd = effectiveHwndat line 337. Prior behavior returned the passed target. - Recommendation: Keep
hwndas the resolved requested target and add a distinct field (e.g.effectiveHwnd/postTargetHwnd) for the retarget, or document the changed semantics in the JSON-output docs / release notes.
M2 src/winapp-CLI/WinApp.Cli/Services/SystemUiQuery.cs:91-102
- Severity: medium
- Confidence: high
- Validation: static-only (needs runtime confirmation)
- Domain: alternative-solution
- Finding: SystemUiQuery introduces its own GetWindowRect wrapper (GetWindowSize) even though window-rect querying already exists behind IUiAutomationService.TryGetWindowRect, creating a second parallel Win32 rect seam.
- Evidence: New path SystemUiQuery.cs:91-98 (s_getWindowSize/NativeGetWindowSize → PInvoke.GetWindowRect); existing IUiAutomationService.TryGetWindowRect (IUiAutomationService.cs:27-32, UiAutomationService.cs:83-104). New callers at UiSessionService.cs:100,117.
- Recommendation: Reuse IUiAutomationService.TryGetWindowRect where UiSessionService already has it injected, or back both APIs with one shared geometry helper, to avoid maintaining two rect/size seams that can drift.
M3 src/winapp-CLI/WinApp.Cli.Tests/UiCommandTests.SendKeys.cs:416-454
- Severity: medium
- Confidence: high
- Validation: static-only (needs runtime confirmation)
- Domain: test-coverage
- Finding: No test covers the new OR-condition where the top-level target is non-XAML but the retargeted focused child IS XAML — the exact case the code added.
- Evidence:
targetLooksXaml = ...GetWindowClassName(targetHwnd) || ...GetWindowClassName(effectiveHwnd)(UiSendKeysCommand.cs:258-261); existing warning test sets only the top-level class (416-421), retarget test asserts only HWND forwarding (441-454). - Recommendation: Add a --json post-message test with matching roots, non-XAML/null top-level class, XAML focused-child class; assert
warnings[]contains the delivery warning.
M4 src/winapp-CLI/WinApp.Cli.Tests/UiCommandTests.SendKeys.cs:436-479
- Severity: medium
- Confidence: high
- Validation: static-only (needs runtime confirmation)
- Domain: test-coverage
- Finding: The
targetRoot == 0branch of the PR #666 retarget guard is untested. Tests cover roots-match and roots-differ, but not the target-root-lookup-failed case. - Evidence: Retarget requires
targetRoot != 0 && GetRootWindow(focused) == targetRoot(UiSendKeysCommand.cs:206-207); tests at 441-454 (match) and 465-479 (differ) only. - Recommendation: Add a test with FocusedWindowByHwnd[target]=child and RootWindowByHwnd[target]=0; assert Send and JSON hwnd stay the original target.
L1 src/winapp-CLI/WinApp.Cli/Commands/UiSendKeysCommand.cs:352-356
- Severity: low
- Confidence: high
- Validation: static-only (needs runtime confirmation)
- Domain: test-coverage
- Finding: The plain-log wording change ("Posted" for post-message, "Sent" for send-input) is not asserted anywhere.
- Evidence:
transport == KeyTransport.PostMessage ? "Posted" : "Sent"(352-356); no output assertion in UiCommandTests.SendKeys.cs. - Recommendation: Add non-JSON tests capturing ambient AnsiConsole asserting "Posted … via post-message" and "Sent … via send-input".
Coverage notes
security: Verified the retarget is guarded by GetAncestor(GA_ROOT) before using the focused child, GetWindowText/GetClassName fixed buffers use returned lengths safely, the new debug logs contain only HWNDs (no key contents), and the send-input foreground guard still runs. Clean.
correctness: Walked all retarget-guard branches (focused==0/==target, targetRoot==0, root mismatch, both-zero) — all safe; send-input path provably unchanged (retarget gated on PostMessage). Confirmed the static native seams ARE isolated: SystemUiQueryTests marked [DoNotParallelize] with ResetNativeSeams, so no parallel-test race despite process-global Func seams. Clean.
docs-and-samples: Verified the hand-written fragment, both generated SKILL.md files, the .claude mirror, docs/ui-automation.md, docs/npm-usage.md, cli-schema.json, and both agent files all reflect the new focused-child/XAML/"Posted" behavior and are mutually consistent; the expanded --via description round-trips into cli-schema.json (no CI drift). Clean.
packaging: Confirmed the 1.0.0→0.4.1 version fix is complete and consistent across version.json, cli-schema.json, plugin.json, skill frontmatter, and winapp-commands.ts (no residual 1.0.0 in generated artifacts); generate-commands:check passed; NativeMethods.txt additions are CsWin32 source-gen only; no command/npm-wrapper drift. Clean.
multi-model (gpt-5.4): Independent pass found no critical/high; verified seam isolation and the JSON hwnd contract change; ran 57/57 focused tests green.
validation: dotnet build (Debug) 0 warnings/0 errors; SendKeys + SystemUiQuery + InjectionGuard tests all passed (0 failed, 0 skipped).
Verdict: ready to merge. No critical or high findings. Solid fix — the tricky PR #666 wrong-window guard is correct, the new service is cleanly testable, tests are isolated ( [DoNotParallelize] ), and all doc/generated surfaces (including the 0.4.1 version correction) are in sync. The 4 medium / 1 low are polish: most worth addressing before merge are M1 (decide whether --json hwnd should stay the requested target or be documented as the effective one) and M2 (reuse the existing TryGetWindowRect instead of a second rect seam); the rest are additive test cases.
…keys-post-message # Conflicts: # .claude/agents/winapp.md # .claude/skills/winapp-ui-automation/SKILL.md # .github/plugin/agents/winapp.agent.md # .github/plugin/skills/winapp-cli/ui-automation/SKILL.md # docs/fragments/skills/winapp-cli/ui-automation.md
…-fix-ui-send-keys-post-message
- Cover the XAML-warning 2nd clause: a non-XAML top-level target whose retargeted focused child is a windowless XAML control now asserts the warning fires (console + json) and json hwnd == child (M1). - Add no-retarget guard fall-through tests (targetRoot==0; focused==target) and a success-verb test (post-message 'Posted' vs send-input 'Sent') (L3/L4). - Fix imprecise docs wording: literal text via post-message posts only a WM_CHAR per character (WM_KEYDOWN/UP is reserved for named keys/combos) in docs/ui-automation.md and the ui-automation skill fragment + generated plugin/.claude skills (L1). - Document the --json behavior in docs/ui-automation.md: hwnd reports the effective (retargeted) post target and warnings[] carries the XAML delivery advisory (L2). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolves conflicts after #664/#665/#666/#668 merged to main. - KeyboardInput.cs: keep #657 chunked/throttled SendViaSendInput + per-segment foreground re-check + partial-apply messaging, and apply #664 NormalizeNewlines to the chunking path (normalize once, then chunk). - KeyboardInputTests.cs: keep both #657 segment/throttle/partial-apply tests and #664 newline tests. - UiSendKeysCommand.cs: keep #657 throttle warning; deliver via #666 effectiveHwnd (post-message focused-child retarget; equals target for send-input). - ui-automation skill fragment + generated SKILL.md (.github + .claude): keep #666 focused-child/windowless post-message note and the #657 auto-throttle / foreground-recheck / set-value-for-bulk note. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Description
winapp ui send-keys --via post-messagereported✅ Sent N key action(s)and exited 0, but the keystrokes never reached the target control (reproduced on Notepad/WinUI, Calculator/UWP, and File Explorer's Win32 edit).Root cause:
post-messagepostedWM_CHAR/WM_KEYDOWNto the resolved element's HWND, which is stamped as the top-level window handle. Top-level windows don't route keyboard messages to their focused child control, and windowless WinUI/UWP/XAML controls ignore posted messages entirely — so input was dropped while the command still reported success.Fix (combined):
post-message, resolve the target thread's focused child HWND viaGetGUIThreadInfo(after foregrounding) and post there, so classic Win32/WinForms edit controls now receive the input.warnings[], and reword post-message success to "Posted" (fire-and-forget). Exit code stays 0 by design sincePostMessagecan't confirm delivery.Guidance is now explicit: use
--via send-inputfor WinUI 3 / UWP / WPF apps.Usage Example
Related Issue
Fixes #655
Type of Change
Checklist
--viareference text changed (via schema regen), plusdocs/ui-automation.mddocs/fragments/skills/(if CLI commands/workflows changed)Additional Notes
GetGUIThreadInfo+GUITHREADINFO; newISystemUiQuery.GetFocusedWindow(with a native seam + fake for tests).KeyboardInputtransport is unchanged — HWND selection happens in the command that ownsISystemUiQuery.docs/cli-schema.json,.github/plugin/skills/**,.claude/**,docs/npm-usage.md,src/winapp-npm/src/winapp-commands.ts) were regenerated via the build/sync scripts, not hand-edited.E2E_Node*cases pass once the Node CLI is built).build-cli.ps1run could not complete the arm64 NativeAOT publish because the ARM64 C++ build tools VS component isn't installed on this machine (vswherereturns empty forVC.Tools.ARM64); this is a local prerequisite gap unrelated to the change. CI runners with the ARM64 workload build both architectures.AI Description
The pull request resolves an issue where the command
ui send-keys --via post-messagefailed to deliver keystrokes to target controls in certain applications. It improves message delivery by targeting the focused child HWND and broadens warning messaging for scenarios involving windowless controls. Users are advised to use--via send-inputwhen dealing with WinUI 3, UWP, or XAML apps.