Repository navigation
feat: wait until a window, dialog, element, text or app condition holds - #70
Merged
Merged
Conversation
This was referenced Oct 17, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
desktop_wait, the 42nd tool. It is a bounded, read-only wait that returns as soon as one condition holds (matched, with what matched), ortimed_outwith the last observation.Conditions (
until, one per call)window_appears: an app's window, bybundle_idorpid, optionally with an exacttitleor atitle_containssubstring. Only windows that are on screen, minimized or on a Space count, so Finder's off-screen menu-bar strips don't match.window_gone: windowpid+windowis no longer listed, or its app quit.dialog_appears/dialog_gone, in apppidor its windowwindow, asdesktop_dialogreads it.element_appears/element_gonein windowpid+window.elementis an exact role (AX role or the listed role) plus at least one of an exact title (label), value or identifier. An optionalscoperestricts the search to one container.text_changes/text_equals/text_contains. The text is the window's or ascope's text (asdesktop_readreads it), or the value of the one elementelementmatches.app_running/app_frontmost/app_quit, bybundle_idorpid.How it works
src/wait.tsand polls existing reads with backoff: 200 ms, ×1.5, up to 2 s between polls. Each poll is a separate client process, so no lane lock is held between polls. MCP runs it like the other reads, outside the action queue.timeout_msdefaults to 10 s, with a cap of 45 s. The final inspection after an element appears gets up to 10 s more, so the worst case (about 55 s) stays under the 60 s default request timeout of the MCP TypeScript SDK. The wait doesn't use the client's 30 s per-request deadline; each poll still has its own.desktop_read, then found that it captures a screenshot every time: it fails with "Capture failed" while the session is locked. Polling it would put ScreenCaptureKit traffic on every other agent (the fix: wait out a quarantined ScreenCaptureKit gate and explain the refusal #63 problem).elements(native/sources/client/elements.swift) uses upstream's existinginspectAccessibilityTreeinstead. That is a detached, read-only AX traversal that needs only Accessibility, stores no snapshot and takes a traversal budget; the poll usesdesktop_read's large budget..inspectAccessibilityTree. It is an existing upstream operation, so there is no new Bridge operation or patch.scopeSource/locateScopeas scoped inspect and read.desktop_windows/desktop_appsrows with their targets. A dialog is thedesktop_dialoglisting.desktop_inspect(with the scope, if given) and returnsinspection.snapshot_id, the matching elements with their IDs from that snapshot, and the screenshot, ready fordesktop_clickand the other actions.snapshot_idis null, and the note says to inspect, with a scope, before acting.The docs table and a section are in
docs/tools.md; the tool count is now 42.Checks
npm run checkpasses.app_runningmatched on the first poll, in 170 ms.app_frontmostandapp_quittime out with the last observation.window_appearsfor Finder returns the real windows with targets, and no longer the off-screen strips.dialog_appearsfor TextEdit times out withdialog: null.dialog_gonestays unmatched while Accessibility enumeration is incomplete.element_*andtext_*polls take no screenshot. While locked they readax_incomplete_readwith 0 elements, andelement_gonecorrectly doesn't match.window_appearswithtitle_contains, started beforedesktop_openof a file in TextEdit, matches the new window.window_gonematches afterdesktop_closeof that window.element_appearsfor TextEdit's close button (AXButton, identifier_XCUI:CloseWindow) returns a snapshot, anddesktop_click's argument check accepts that element ID and snapshot (no click needed).text_containson the window text and on anelementmatcher (AXTextArea, …) afterdesktop_typeadds text.text_changesreportsbefore.dialog_appearsafter Command-S on an untitled TextEdit document (Save sheet), thendialog_goneafter answering it with Cancel.app_quitafterdesktop_quitof TextEdit.scopesearch on a long web page in Safari, for an element deeper than 200 elements.lsofon~/.peekaboolocks that none is held between polls.