Add ui watch command for UI event streaming#602
Conversation
Adds a new `winapp ui watch` command that listens for UIA / WinEvent notifications from a running app and streams them as they occur, with JSON/NDJSON output for tooling and bounded capture via event filters, duration, and max-event limits. Also updates the generated CLI schema and UI automation skill/docs to surface the new command. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a new winapp ui watch subcommand that streams live UI events (focus, window open/close, invoke, selection, property/text/structure changes, notifications, live regions) from a running app, instead of forcing callers to poll for state transitions. A dedicated STA thread creates its own CUIAutomation8, registers UIA automation-event/property/structure/focus handlers plus a process-scoped SetWinEventHook for window lifecycle, and pumps messages until a duration/--max-events limit or Ctrl+C. Output is human-readable by default or NDJSON (one compact object per line) plus a trailing summary line with --json, with an optional -o tee to file. The change is wired into DI/command registration and the auto-generated + hand-written docs surfaces are (partly) updated.
Changes:
- New
UiWatchCommand+IUiEventWatcher/UiEventWatcher(COM CCW sinks + WinEvent hook) and aWindowLifecycleCoalescerto collapse paired CREATE+SHOW / DESTROY+HIDE events. - Event/property validation, element-scope-requires-window fail-fast, selector re-resolution, NDJSON JSON context, and unit tests for the command and coalescer.
- Regenerated
cli-schema.json/SKILL.md and updateddocs/usage.md+ skill fragments (but notdocs/ui-automation.md/winapp.agent.md).
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/winapp-CLI/WinApp.Cli/Commands/UiWatchCommand.cs |
New command: options, validation, streaming output, summary. |
src/winapp-CLI/WinApp.Cli/Services/UiEventWatcher.cs |
Real watcher: STA pump, UIA handlers, WinEvent hook, teardown. |
src/winapp-CLI/WinApp.Cli/Services/IUiEventWatcher.cs |
Contract, event-name constants, request/outcome models. |
src/winapp-CLI/WinApp.Cli/Services/WindowLifecycleCoalescer.cs |
De-dupes paired window lifecycle WinEvents. |
src/winapp-CLI/WinApp.Cli/Helpers/UiWatchJsonContext.cs |
Compact source-gen JSON context + event/summary DTOs. |
src/winapp-CLI/WinApp.Cli/NativeMethods.txt |
CsWin32 declarations for event handlers/WinEvent/message pump. |
src/winapp-CLI/WinApp.Cli/Helpers/HostBuilderExtensions.cs |
Registers watcher service + command handler. |
src/winapp-CLI/WinApp.Cli/Commands/UiCommand.cs |
Adds watch as a ui subcommand. |
src/winapp-CLI/WinApp.Cli.Tests/* |
New command + coalescer tests and FakeUiEventWatcher. |
scripts/generate-llm-docs.ps1 |
Adds ui watch to the skill command map. |
docs/usage.md, docs/fragments/.../ui-automation.md, .github/plugin/.../SKILL.md, .claude/.../SKILL.md, docs/cli-schema.json |
Documentation and generated-schema updates for the new command. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #### `winapp ui watch` | ||
|
|
||
| Stream live UIA / WinEvent notifications from a running app as they occur. With `--json`, emits NDJSON (one compact JSON object per event line) followed by a summary line. |
| PostQuitMessage | ||
| GetCurrentThreadId | ||
| MSG | ||
| GetAncestor |
|
|
||
| if (!string.IsNullOrWhiteSpace(outputPath)) | ||
| { | ||
| logFile = new StreamWriter(outputPath, append: false, Encoding.UTF8); |
Build Metrics ReportBinary Sizes
Test Results✅ 1562 passed, 1 skipped out of 1563 tests in 427.6s (+17 tests, -50.4s vs. baseline) Test Coverage❌ 18.2% line coverage, 37.1% branch coverage · ✅ no change vs. baseline CLI Startup Time40ms median (x64, Updated 2026-07-06 03:16:58 UTC · commit |
nmetulev
left a comment
There was a problem hiding this comment.
🤖 AI-generated review — produced by GitHub Copilot CLI at a maintainer's request. Every finding comes from an automated, multi-dimensional review plus a hands-on build-and-test of this branch in an isolated worktree. Treat
file:linereferences as pointers to verify, not gospel. Nothing was pushed. Posting as a comment (no approve / request-changes).
Verdict: 🟡 SHIP-WITH-CHANGES — Good architecture (dedicated STA pump, correct COM handler teardown), but a buffering bug currently defeats the streaming use case this command exists for.
What I actually ran
Built (~33s, 0 warnings); 17 unit tests pass. Watched Notepad while driving it from a second shell:
--duration-secand--max-eventsbounding stop cleanly; NDJSON is one event per line + a summary object. ✅- Reading the
-ofile mid-run: empty until the process exits (C01) — real-time streaming doesn't work. - Opening one WinUI3 Notepad emitted 25+
window-openevents for internal panes (DesktopWindowXamlSource,Default IME,CicMarshalWnd, …); only 1 was the real window (H01). - Focus-event latency <100ms; timestamps ISO-8601 UTC.
Must-fix (Critical / High)
- C01 — no flush after each event (
UiWatchCommand.cs:173).StreamWriterdefaults toAutoFlush = false, so neither stdout nor the-ofile streams until the process exits. This is the headline "agents read events live" scenario, and it's currently broken. One-line fix (Flush()after each write). - H01 — window-open/close filter too broad (
UiEventWatcher.cs:225-230): 20–30 internal-pane events per window open. Add a top-level-window filter (parent == desktop, orWS_CAPTION/WS_OVERLAPPEDWINDOW). - H02 — event
selectorisn't a winapp slug. It's the raw AutomationId (UiEventWatcher.cs:314-319) and is frequently null, soevent.element.selectorcan't be piped intoui invoke/set-value. The unit-test fakes use slug-format strings, which hides this. Route throughSelectorService.
Medium / Low
- M01
RequestStop()is a non-atomic check-then-set. M02 thes_activestatic is unguarded against a concurrentWatchAsync. M03--outputdescription is copy-pasted fromscreenshot. M04 npmwinapp-commands.tsis missinguiWatch(). - L: no integration test for the real watcher (all 17 use
FakeUiEventWatcher); the summary line lacks atypediscriminant;ui-json-envelope.mdisn't updated; thedetailfield format varies across event types.
Fit & recommendation
Push notifications are genuinely more efficient than polling. But this is a long-running streaming command inside an otherwise request/response CLI, and it overlaps the existing wait-for verb. The agent workflow (fork a subprocess, drive the app from another, parse after exit) is awkward, and the flush bug amplifies it. I'd gate this on fixing streaming (C01), then decide whether the streaming model earns its complexity versus investing in wait-for.
Summary
winapp ui watchto listen for UIA/WinEvent notifications from a running app--duration-sec,--max-events), and JSON/NDJSON outputWhy
This adds a first-class way to observe focus, selection, window, and related UI events instead of polling for every state transition.