Skip to content

Fix ui send-keys --via post-message silently dropping input#666

Merged
nmetulev merged 9 commits into
mainfrom
nmetulev-fix-ui-send-keys-post-message
Jul 17, 2026
Merged

Fix ui send-keys --via post-message silently dropping input#666
nmetulev merged 9 commits into
mainfrom
nmetulev-fix-ui-send-keys-post-message

Conversation

@nmetulev

@nmetulev nmetulev commented Jul 16, 2026

Copy link
Copy Markdown
Member

Description

winapp ui send-keys --via post-message reported ✅ 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-message posted WM_CHAR/WM_KEYDOWN to 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):

  • Better delivery — for post-message, resolve the target thread's focused child HWND via GetGUIThreadInfo (after foregrounding) and post there, so classic Win32/WinForms edit controls now receive the input.
  • Honest reporting — broaden the delivery warning to fire for all keys (not just literal text) when the effective target looks like a windowless WinUI 3/UWP/XAML host, surface it in JSON warnings[], and reword post-message success to "Posted" (fire-and-forget). Exit code stays 0 by design since PostMessage can't confirm delivery.

Guidance is now explicit: use --via send-input for WinUI 3 / UWP / WPF apps.

Usage Example

# Classic Win32/WinForms: post-message now reaches the focused edit control
winapp ui send-keys "hello" -a notepad --via post-message

# WinUI 3 / UWP / XAML: post-message can't deliver — command now warns (still exit 0)
winapp ui send-keys "enter" -a mywinui3app --via post-message
#   ⚠️  target looks like a windowless XAML host; posted WM_CHAR/WM_KEYDOWN may not be delivered — use --via send-input
# Correct transport for XAML apps:
winapp ui send-keys "enter" -a mywinui3app --via send-input

Related Issue

Fixes #655

Type of Change

  • 🐛 Bug fix
  • 📝 Documentation
  • 🧪 Test update

Checklist

  • New tests added for new functionality (if applicable)
  • Tested locally on Windows
  • Main README.md updated (if applicable) — n/a (no README references to this transport)
  • docs/usage.md updated (if CLI commands changed) — reviewed; only the auto-generated --via reference text changed (via schema regen), plus docs/ui-automation.md
  • Language-specific guides updated (if applicable)
  • Sample projects updated to reflect changes (if applicable) — n/a
  • Agent skill templates updated in docs/fragments/skills/ (if CLI commands/workflows changed)

Additional Notes

  • New CsWin32 P/Invoke GetGUIThreadInfo + GUITHREADINFO; new ISystemUiQuery.GetFocusedWindow (with a native seam + fake for tests). KeyboardInput transport is unchanged — HWND selection happens in the command that owns ISystemUiQuery.
  • Auto-generated files (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.
  • Verification: x64 NativeAOT publish succeeded; Debug build clean (0 warnings, warnings-as-errors); full C# suite 3317 passed / 24 skipped / 0 real failures (the 5 E2E_Node* cases pass once the Node CLI is built).
  • Environment caveat (CI unaffected): the local build-cli.ps1 run could not complete the arm64 NativeAOT publish because the ARM64 C++ build tools VS component isn't installed on this machine (vswhere returns empty for VC.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-message failed 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-input when dealing with WinUI 3, UWP, or XAML apps.

# Improved command usage for classic Win32/WinForms
winapp ui send-keys "hello" -a notepad --via post-message

# Warning issued when using post-message on windowless controls
winapp ui send-keys "enter" -a mywinui3app --via post-message
#   ⚠️ target looks like a windowless XAML host... use --via send-input
# Correct transport for XAML apps:
winapp ui send-keys "enter" -a mywinui3app --via send-input

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>
Copilot AI review requested due to automatic review settings July 16, 2026 21:31
@github-actions github-actions Bot added the bug Something isn't working label Jul 16, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/winapp-CLI/WinApp.Cli/Commands/UiSendKeysCommand.cs Outdated
nmetulev and others added 4 commits July 16, 2026 14:58
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>
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Build Metrics Report

Binary Sizes

Artifact Baseline Current Delta
CLI (ARM64) 32.41 MB 32.42 MB 📈 +3.5 KB (+0.01%)
CLI (x64) 32.72 MB 32.72 MB 📈 +4.0 KB (+0.01%)
MSIX (ARM64) 13.57 MB 13.57 MB 📈 +3.5 KB (+0.03%)
MSIX (x64) 14.43 MB 14.43 MB 📉 -1.4 KB (-0.01%)
NPM Package 28.33 MB 28.33 MB 📈 +0.3 KB (+0.00%)
NuGet Package 28.35 MB 28.35 MB 📈 +3.0 KB (+0.01%)

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 Time

45ms median (x64, winapp --version) · ✅ no change vs. baseline


Updated 2026-07-17 17:05:51 UTC · commit 35ae970 · workflow run

@zateutsch zateutsch left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 hwnd field now returns effectiveHwnd (the retargeted focused child), not the resolved app/window/selector target. Scripts that read hwnd to correlate the window they requested will silently get a different (child) HWND.
  • Evidence: targetHwnd resolved at 162/176; effectiveHwnd reassigned to the focused child at ~194-212; JSON result sets Hwnd = effectiveHwnd at line 337. Prior behavior returned the passed target.
  • Recommendation: Keep hwnd as 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 == 0 branch 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.

zateutsch and others added 4 commits July 17, 2026 10:52
…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
- 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>
@nmetulev
nmetulev merged commit d93aedc into main Jul 17, 2026
22 checks passed
@nmetulev
nmetulev deleted the nmetulev-fix-ui-send-keys-post-message branch July 17, 2026 17:32
nmetulev added a commit that referenced this pull request Jul 17, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: winapp ui send-keys --via post-message reports success but delivers no input to target controls

3 participants