Skip to content

Add deterministic explicit UI invoke actions - #846

Merged
Nikola Metulev (nmetulev) merged 27 commits into
mainfrom
nmetulev-deterministic-explicit-ui-actions
Sep 18, 2026
Merged

Nikola Metulev (nmetulev) merged 27 commits into
mainfrom
nmetulev-deterministic-explicit-ui-actions

Conversation

@nmetulev

Copy link
Copy Markdown
Member

Description

Adds --action invoke|select|toggle|toggle-on|toggle-off|expand|collapse to winapp ui invoke. Explicit actions use only their corresponding UIA pattern on the selected element, with no pattern or ancestor fallback. Explicit resolution also refuses to rebind a removed element to a same-name sibling.

Toggle-on/off read first, leave an already-correct state untouched, and verify each transition. Initially indeterminate controls allow at most two transitions; other starting states allow one. Omitted --action preserves automatic behavior.

JSON includes requestedAction and performedAction, including none for an idempotent no-op. The package API, npm forwarding surface, canonical docs, shipped skill, and generated schema are updated together.

Usage Example

winapp ui invoke SettingsCategory -a myapp --action select
winapp ui invoke AgreeCheckbox -a myapp --action toggle-on --json
winapp ui invoke SizeComboBox -a myapp --action collapse

Related Issue

Fixes #821. Independent of #841, #842, and #844; no batching or persistent handles.

Type of Change

  • New feature
  • Documentation
  • Test update

Checklist

  • Unit and real-app regression coverage added
  • Tested locally on Windows
  • Canonical UI automation documentation and shipped skill updated
  • CLI schema and npm surface regenerated through the normal build

Validation

  • 64 explicit-action unit/live cases passed, including strict identity and same-name surviving siblings.
  • 49 CLI/API/cancellation targeted cases passed.
  • scripts\build-cli.ps1 -SkipTests passed: NativeAOT x64/arm64, npm, NuGet, MSIX, and generated docs.
  • Published CLI argument/error checks and validate-llm-docs.ps1 -FailOnDrift passed.
  • Full scripts\build-cli.ps1 was run using a session-local corporate NuGet mirror. It reached 5,440 CLI and 453 UI successes plus 63 analyzer successes. A missing new-type API allowlist entry was fixed and retested. Remaining unrelated failures: two recording DirectoryNotFoundException cases, x64 dump analysis under the ARM64 host, and a process-cancellation timing case (the latter passed in isolation).

Additional Notes

Automated review and required CI gates are being monitored. This PR must not be merged by the agent.

Implement exact-pattern actions, bounded idempotent toggles, strict element identity, structured results, and unit/live coverage. Fixes #821.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings September 15, 2026 03:48
Comment thread src/winapp-CLI/WinApp.UIAutomation.Tests/ExplicitUiInvokerTests.cs
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

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.

🟡 Changes recommended

Partial UIA searches can still select the wrong duplicate, and an empty npm action silently enables automatic fallback.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds deterministic explicit actions to winapp ui invoke, including strict UIA pattern routing, toggle-state verification, API/npm exposure, tests, and documentation.

Changes:

  • Adds seven explicit invoke actions with strict element identity.
  • Reports requested and performed actions in JSON.
  • Adds extensive CLI, API, UIA, and live-control coverage.
File summaries
File Description
src/winapp-npm/src/winapp-commands.ts Forwards the action option.
src/winapp-CLI/WinApp.UIAutomation/Services/UiAutomationService.cs Implements strict action resolution.
src/winapp-CLI/WinApp.UIAutomation/Services/IUiAutomation.cs Exposes the explicit-action API.
src/winapp-CLI/WinApp.UIAutomation/Services/ExplicitUiInvoker.cs Routes patterns and verifies toggles.
src/winapp-CLI/WinApp.UIAutomation/Models/UiInvokeAction.cs Defines action and result models.
src/winapp-CLI/WinApp.UIAutomation.TestSupport/FakeUiServices.cs Extends the UIA test fake.
src/winapp-CLI/WinApp.UIAutomation.Tests/RealUiAutomationTests.ExplicitActions.cs Adds live UIA regressions.
src/winapp-CLI/WinApp.UIAutomation.Tests/GestureTargetingTests.cs Updates the test implementation contract.
src/winapp-CLI/WinApp.UIAutomation.Tests/ExplicitUiInvokerTests.cs Tests action routing and transitions.
src/winapp-CLI/WinApp.Cli/Helpers/UiJsonContext.cs Extends JSON output.
src/winapp-CLI/WinApp.Cli/Commands/UiInvokeCommand.cs Adds parsing, execution, and reporting.
src/winapp-CLI/WinApp.Cli.Tests/UiCommandTests.Invoke.cs Tests CLI behavior and output.
src/winapp-CLI/WinApp.Cli.Tests/RealRecordingTests.Helpers.cs Forwards the new API overload.
src/winapp-CLI/WinApp.Cli.Tests/PublicApiSurfaceTests.cs Allows the new public types.
plugins/winapp/skills/winapp-ui-automation/SKILL.md Documents explicit actions.
plugins/winapp/skills/winapp-ui-automation/references/ui-json-envelope.md Documents invoke JSON.
docs/ui-automation.md Adds canonical action guidance.
docs/npm-usage.md Documents npm support.
docs/cli-schema.json Adds generated option metadata.
Review details
  • Files reviewed: 19/19 changed files
  • Comments generated: 2
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/winapp-CLI/WinApp.UIAutomation/Services/UiAutomationService.cs Outdated
Comment thread src/winapp-npm/src/winapp-commands.ts Outdated

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.

🟡 Changes recommended

Explicit mode can still rebind after waiting or trust partial uniqueness results, while the npm wrapper can convert an empty action into automatic invocation.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (3)

Previously missed (1) — in code that hasn't changed since the last review.

src/winapp-npm/src/winapp-commands.ts:1256

  • What is wrong: The npm API exposes a closed seven-value option as unrestricted string. Show me: uiInvoke({ action: 'selet' }) type-checks and only fails at runtime, while existing closed options such as IfExists, SdkInstallMode, and ManifestTemplates are generated as string-literal unions. Why it matters: TypeScript callers lose compile-time validation on the new package surface. Smallest fix: emit a named union for the seven CLI spellings from the command schema, then regenerate this file and npm docs.

src/winapp-CLI/WinApp.UIAutomation/Services/UiAutomationService.cs:1435

  • What is wrong: This uniqueness check assumes FindAll(Descendants) returns every match, but this service already has to compensate for UIA providers returning nonzero partial results, and PR #842 documents that behavior. Show me: a provider has two matching AutomationIds but returns one bulk match -> count == 1 -> the action is performed instead of rejected as ambiguous. Why it matters: explicit mode can invoke the wrong duplicate while reporting deterministic identity. Smallest fix: use a complete Control View traversal (or the completion helper from #842) capped at two matches before deciding the AutomationId is unique.
                var matches = root.FindAll(TreeScope.TreeScope_Descendants, condition);
                var count = matches?.get_Length() ?? 0;

src/winapp-npm/src/winapp-commands.ts:1271

  • What is wrong: An empty npm action is silently omitted instead of forwarded for CLI validation. Show me: uiInvoke({ selector: 'Save', app: 'myapp', action: '' }) -> no --action argument -> automatic fallback may invoke an element; the native CLI rejects --action '' as invalid_arguments. Why it matters: a malformed deterministic request can become a successful nondeterministic UI action. Smallest fix: distinguish omission from an explicitly supplied empty value.
  if (options.action) args.push('--action', options.action);
  • Files reviewed: 19/19 changed files
  • Comments generated: 1
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

Comment thread src/winapp-CLI/WinApp.Cli/Commands/UiInvokeCommand.cs
Verify explicit AutomationId uniqueness using a complete ControlView walk and reject unreadable trees. Preserve defined scalar options in generated npm wrappers so empty actions reach native validation. Add partial/deep/fault UIA and argv regressions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

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.

🔵 Needs a closer look

The cross-layer UIA and public API changes require final human review despite strong regression coverage.

Review details
  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

Keep the initially selected model for strict identity resolution inside the turn. Only automatic mode re-runs broad selectors after the wait. Cover same-name replacements and queued stale targets.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@nmetulev

Copy link
Copy Markdown
Member Author

Automated-review follow-up: all three actionable Copilot findings are fixed with regression coverage (partial identity searches, empty npm action forwarding, and queued text-query rebinding), and all inline threads have been answered and resolved. The suppressed TypeScript string-literal-union suggestion is not a correctness blocker for this change: the CLI preflight rejects unknown values before desktop acquisition, and the generated npm wrapper now preserves even empty values for that validation. Other string-valued UI options (for example scroll direction) use the same runtime-validation contract. A stricter generated TypeScript type is a separate API ergonomics improvement rather than necessary for #821. Final-head review and required CI remain pending; no merge will be performed.

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.

🔵 Needs a closer look

The COM identity semantics and new public API surface warrant the requested final human review despite comprehensive coverage.

Review details
  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Build Metrics Report

Binary Sizes

Artifact Baseline Current Delta
CLI (ARM64) 56.84 MB 56.89 MB 📈 +54.5 KB (+0.09%)
CLI (x64) 56.89 MB 56.94 MB 📈 +51.5 KB (+0.09%)
MSIX (ARM64) 23.64 MB 23.65 MB 📈 +14.0 KB (+0.06%)
MSIX (x64) 25.08 MB 25.10 MB 📈 +16.9 KB (+0.07%)
NPM Package 49.30 MB 49.33 MB 📈 +34.0 KB (+0.07%)
NuGet Package 49.39 MB 49.42 MB 📈 +38.2 KB (+0.08%)

Test Results

7494 passed, 37 skipped out of 7531 tests in 1147.3s (+272 tests, +66.0s vs. baseline)

Test Coverage

86% line coverage, 80.2% branch coverage · ✅ no change vs. baseline

CLI Startup Time

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

Try This Build

Installs the MSIX for your architecture, replacing any previously installed build. Needs the GitHub CLI — the command offers to install it and sign you in if it is missing.

& ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) 846
Switching between builds often?

Put the tool on your PATH once:

& ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) -AddToPath

Then this build is just:

winapp-pr 846

Run winapp-pr with no arguments to pick from a list of open PRs.


Updated 2026-09-18 04:23:33 UTC · commit 91f7775 · workflow run

@nmetulev

Copy link
Copy Markdown
Member Author

Pre-human-review gate complete for head a7656e0: fresh Copilot review 5205557992 covered 22/22 files with 0 new comments; all 5 inline review threads are answered and resolved. All 30 passing checks are green, with 0 failures and 0 pending (one neutral CodeQL summary). Required build-and-package and the final WinUI/UI-coordination E2E job passed in run 34929223926; sample matrix and Code Quality analyses passed. The branch is MERGEABLE and current with main. Local final targeted coverage: 77 explicit unit/live tests, 49 CLI/API cases, 265 npm tests; NativeAOT x64/arm64 and npm/NuGet/MSIX/schema generation completed successfully. Ready for human review; not merged.

Integrate main 8e5f737. Reuse retained provider elements without re-resolution, retain strict serialized identity behavior, regenerate combined target/action npm bindings, and validate actions before remote target preparation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

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.

🟡 Changes recommended

Duplicate AutomationIds can still cause explicit mode to invoke an arbitrary retained element instead of reporting ambiguity.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 22/22 changed files
  • Comments generated: 1
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

Comment thread src/winapp-CLI/WinApp.Cli/Commands/UiInvokeCommand.cs
winapp ui invoke --action resolved a plain-text/AutomationId selector to the
first match and stamped it with a runtime slug, so the strict uniqueness walk was
never reached and a duplicate AutomationId silently activated the first control.
For a non-slug selector that matched a control with an AutomationId, commit to
that AutomationId so the strict resolver proves it is unique and fails closed when
it is ambiguous; a name-only match keeps its runtime slug and a user-supplied slug
is used verbatim. Add CLI-level tests covering the identity handoff.

Also reject an explicitly empty or whitespace cert password in 'cert generate' so
the npm wrapper's empty-string forwarding cannot produce an unprotected PFX, and
document explicit --action ambiguity behavior in the agent guide and usage docs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread src/winapp-CLI/WinApp.Cli.Tests/CertGenerateCommandTests.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli.Tests/CertGenerateCommandTests.cs Fixed
The previous attempt committed a non-slug selector to its AutomationId so the
strict resolver would prove uniqueness, but that path is unreachable: after the
merge of main, FindSingleElementAsync stamps a live COM Context on every resolved
element and GetAutomationElement returns that retained provider before strict
resolution runs, so a duplicate AutomationId still activated the first match.

Instead, gate explicit --action at the command: for a non-slug selector, verify
the selector matches exactly one element (SearchAsync, maxResults 2) before acting
and fail closed with an ambiguous_selector error when more than one matches. A
slug already names one element and is exempt; automatic invoke is unchanged, so
legacy lenient rebinding is preserved. Add CLI tests (fake-backed) covering the
ambiguous, unique, and slug cases, and route the ambiguity JSON error through the
command's configured error stream.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Preserve collaborator ambiguity guard and require complete AutomationId uniqueness against the retained provider. Compare live identities without replacing the selected element. Add nonactivating regressions and fix empty-password logging test placement.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@nmetulev

Copy link
Copy Markdown
Member Author

Zach Teutsch (@zateutsch) Fixed the owned-BSTR leak in f3aa8b3. Both new AutomationId/Name reads now use the existing throwing GetBstr helper, which copies the value and frees the BSTR in finally; no SafeGetBstr catch or weaker uniqueness behavior was added. Audited the other string reads introduced by this PR for the same issue.

Verified ownership from the generated CsWin32 return projection and ran the same isolated long-lived public requireUnique API probe before/after (8 synthetic tree nodes, fresh 64-KiB BSTR per identity/name read, warmup then 160 calls): baseline private-memory growth 174,198,784 bytes; repeat 174,186,496. Fixed growth 4,096 bytes; repeat 0. Managed memory stayed about 1.55 MB. Every run completed the expected traversal and results, with 1,408 AutomationId and 1,408 Name getters. This is an amplified deterministic allocation probe, not a claim about typical app string sizes. Also passed 90 existing matching/provider-failure/cancellation/nonactivating tests and the full NativeAOT/package build.

Requested your reassessment and a fresh Copilot review; required CI is still running. Human approval remains separate from automated checks.

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.

🟡 Changes recommended

Strict slug resolution can select the wrong element when distinct RuntimeIds collide in the four-character slug hash.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 28/28 changed files
  • Comments generated: 1
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

Complete strict slug matching before choosing a provider, applying predicates and scope before ambiguity detection. Preserve automatic first-match behavior and cover real 16-bit RuntimeId hash collisions without changing the slug protocol.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread src/winapp-CLI/WinApp.UIAutomation.Tests/SlugGeneratorTests.cs

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.

🟡 Changes recommended

App-scoped external slug resolution is incomplete, and non-stale COM failures are reported as stale elements.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 30/30 changed files
  • Comments generated: 2
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

Comment thread src/winapp-CLI/WinApp.Cli/Commands/UiInvokeCommand.cs
Comment thread src/winapp-CLI/WinApp.UIAutomation/Services/UiAutomationService.cs
Resolve source-less app-scoped external slugs across all windows before materialization, preserving collision rejection and recorded HWND boundaries. Distinguish explicit stale-element HRESULT from other provider errors without changing automatic behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

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.

🔵 Needs a closer look

Strict HWND resolution can still misreport non-stale provider failures as stale-element errors.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/winapp-CLI/WinApp.UIAutomation/Services/UiAutomationService.cs:1603

  • What is wrong: Strict source-window binding still calls GetRootElementForHwnd with requireCurrentIdentity == false, whose catch-all at line 1830 converts every provider error into null. Show me: a serialized element with a recorded HWND reaches this branch and ElementFromHandle throws E_ACCESSDENIED -> the explicit overload reports the element as stale; expected: preserve the access/provider failure (only UIA_E_ELEMENTNOTAVAILABLE is stale). Why it matters: package and JSON callers receive the wrong recovery advice and lose the real failure. Smallest fix: make strict HWND binding suppress only the stale HRESULT, or let COM failures propagate so the command’s HRESULT filter can classify them.
  • Files reviewed: 30/30 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

Use throwing HWND resolution for explicit identity checks and suppress only the element-unavailable HRESULT. Cover access-denied and stale source/target bindings without fallback.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@nmetulev

Copy link
Copy Markdown
Member Author

Addressed the suppressed provider-error finding from review 5242333274 in 3c48167. Strict source/target HWND binding now uses the throwing root helper; only UIA_E_ELEMENTNOTAVAILABLE keeps the prior stale outcome. E_ACCESSDENIED and other provider failures retain the original COM exception, with no fallback or invocation. Four access-denied cases failed before the fix; all 24 targeted source-window/error tests now pass. Independent fix review found no concrete blocker, and full NativeAOT/package build passed. Requesting a fresh latest-head assessment; this is not a claim of reviewer approval.

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.

🔵 Needs a closer look

Cross-window UIA identity and COM-provider behavior warrants final human review despite successful builds and targeted test suites.

Review details
  • Files reviewed: 30/30 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

@zateutsch Zach Teutsch (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.

Clean on my end. Approving.

Preserve the incoming run --aot option alongside explicit UI action and defined string argument forwarding. Regenerate wrappers and npm docs from the merged schema without changing UI identity or ownership behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

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.

🔵 Needs a closer look

The broad UIA identity and cross-window behavior warrants final human approval despite comprehensive coverage and no remaining critical or high findings.

Review details
  • Files reviewed: 30/30 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

@nmetulev

Copy link
Copy Markdown
Member Author

Updated #846 to current main 19212c0 in 91f7775. Resolved the sole conflict by regenerating npm bindings, preserving both run --aot and defined-string forwarding. All prior UI identity, BSTR, and error fixes are unchanged. Local validation: 311 npm tests, 149 CLI/AOT tests, 118 focused UIA cases, full NativeAOT/package build, and generated-doc validation passed. Exact-head CI run 35304228665 completed successfully, including WinUI/coordination E2E; all 30 passing checks are green with none pending/failing, and GitHub reports MERGEABLE.

The superseded 3c build had remained in progress for over four hours; it was cancelled after this new head was pushed, and GitHub did not provide its job logs. The last completed historical failure was a signing diagnostic assertion that received a different manifest error; its root cause cannot be proven from the available runner records. No speculative test weakening was applied.

All 24 inline threads are resolved. Latest Copilot review 5244074262 reports no remaining critical/high findings but explicitly defers to human approval; that is not affirmative approval. Zach Teutsch (@zateutsch) your approval was on 3c48167; please reassess the current main-merge head when available. pr-review-done remains off pending the requested review gate. Not merged.

@nmetulev
Nikola Metulev (nmetulev) merged commit 09a401b into main Sep 18, 2026
32 checks passed
@nmetulev
Nikola Metulev (nmetulev) deleted the nmetulev-deterministic-explicit-ui-actions branch September 18, 2026 05:49
Zach Teutsch (zateutsch) added a commit that referenced this pull request Sep 18, 2026
main added deterministic explicit UI invoke actions (#846), which also
changed the npm command generator (generate-commands.mjs now emits
`!== undefined` guards). That regenerated winapp-commands.ts conflicted with
this branch's project-mode packageApp options. Regenerated winapp-commands.ts,
cli-schema.json, and npm-usage.md from the merged CLI so the wrapper combines
main's generator style and new UI actions with the pack project-mode options.
All other main changes auto-merged. Verified with tsc --noEmit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dcabfb3c-9ea1-44c5-b91b-3b04f86a4289
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Add deterministic explicit UI actions

3 participants