Hope Agent Mac Control
mac_control operates the user's macOS desktop from the authorized Hope Agent app process. macOS UI state is volatile: apps steal focus, AX IDs expire, sheets attach to windows, and multiple windows often share similar titles. Use a fresh observation before every meaningful action.
Standard Loop
Use this loop unless the user explicitly asks for a single read-only query:
1. mac_control(action="status")
2. mac_control(action="apps", op="frontmost" | "search" | "installed")
3. observe: snapshot / visual.observe / elements.find / windows.list / dock.list / spaces.list / menu.list / menu.popover / dialog.inspect
4. act: apps.activate/launch, dock.launch, spaces.switch, windows.*, act.*, menu.click, dialog.*
5. verify: wait, snapshot, windows.list, or dialog.inspect
For a concrete app workflow:
apps.launch bundleId=...
apps.frontmost # verify focus if the next step depends on menus/input
snapshot, elements.find, or windows.list # get fresh window/element ids
act/menu/windows/clipboard/dialog # one action burst
wait or snapshot # verify the expected change
Targeting Rules
- Prefer
bundleIdoverappNamefor mutations. Useapps.search/apps.installedwhen the app name is uncertain, then retry withbundleId. appNameMatchdefaults toexact. Usecontainsonly for read-only discovery or when the user clearly gave a partial name.- Prefer
windowIdfrom the latestwindows.listorsnapshotfor window mutations. target.windowTitleMatchdefaults toexact. Usecontainsonly after listing windows and confirming a partial title is intentional.- Prefer
elementIdfrom the latestsnapshot/visual.observe/elements.findfor precise clicks and set-value actions, and pass the matchingtarget.snapshotIdwith it.snapshotId + elementIdlets the runtime verify the original AX fingerprint and re-resolve staleel_Nids instead of blindly trusting a new traversal. - Use
elements.findwhen a full snapshot is too noisy or when an action target is ambiguous. It is read-only and returns scored candidates with reasons; retry mutations withtarget.elementIdfrom the chosen candidate plus the resultsnapshotId. - If two windows, dialogs, text fields, or buttons match, do not guess. Use a more specific target or ask the user.
- Element mutations reject equally ranked AX candidates instead of choosing the first match. When this happens, take a fresh
snapshotand retry withelementId,target.windowTitle,target.role, or more specifictarget.text.
Actions
Apps
- Use
apps.frontmostto know what macOS will receive menu and keyboard actions. - Use
apps.activate bundleId=...before operating an app that is not frontmost. - Use
apps.searchorapps.installedwhen launch/activate by name fails. apps.quitis destructive. Verify the target app and preferbundleId.
Dock and Spaces
- Use
dock.listbeforedock.launch; preferdockItemIdorbundleIdover a loose app name. - Use
dock.menuto open a Dock item's context menu and inspectmenuItems; usedock.select_menuwithmenuItemwhen possible, ormenuIndexonly when titles are unavailable. If both are present,menuItemis treated as the intended target. dock.hideanddock.showchange the user's Dock autohide setting and restart Dock, so be explicit before approval.- Use
spaces.listbeforespaces.switchwhen targeting a numbered Space.spaceIndexis 1-based. spaces.switch direction="left"|"right"/spaceIndex/spaceIdpass exactly one selector. Direction and adjacent targets use Mission Control Control+Left/Right first; non-adjacent exact targets may fall back to Control+number or SkyLight/CGS. Verify withspaces.listor a fresh screenshot after switching.spaces.move_windowmoves one explicit window tospaceIndex/spaceIdthrough SkyLight/CGS. Resolve the window first withwindows.list windowScope="all"and preferwindowId; if post-move verification warns, usespaces.listor a fresh screenshot to confirm.
Windows
- Use
windows.listbeforewindows.close,move,resize, orminimizeunless the user supplied an exactwindowId. windowScopedefaults tofrontmost. Usewindows.list windowScope="all"to discover background app windows before activating or focusing them.- Prefer all-scope ids like
win_<pid>_<index>for cross-app window mutations; they are safer than generic titles. - For
windows.close, avoid generic titles likeUntitled/未命名when multiple similar windows exist. UsewindowId. - Hope Agent's own window cannot be mutated through the Accessibility worker; if the target is Hope Agent itself, explain the limitation.
Screenshots
- Use
snapshot includeScreenshot=truewhen visual context matters. - Default screenshots capture the primary display. Use
displayIdfromsnapshot.displayswhen the user points at a specific monitor. - For a focused-window image, use
snapshot includeScreenshot=true screenshotTarget="window". PasswindowIdfrom the latest snapshot/list when several windows are possible. - Window screenshot matching uses the current AX window state; if it fails, take a fresh snapshot and retry with a precise
windowId.
Elements and Text
- Use
elements.find op="find"before clicking or typing into ambiguous UI. Useful examples:target.role="AXButton",target.text="Save",target.windowTitle="Untitled". elements.findreturnstotalMatchesplus candidatescore,reasons,element, andwindow. Prefer high-score candidates whose reasons include the user's intended text/role/window.- Browser/WebView snapshots may focus the dominant
AXWebAreaand re-traverse when no text input is exposed. If a result warning mentions this fallback, use the refreshed candidates first; if it still exposes only web/canvas content, switch tovisual.observe annotate=true, OCR, orvisual.point. - Use
act.dry_runwhen the next mutation should use the exact same target resolver, but you want to verify the resolved element first. PassdryRunOpfor the intended real op, such asclick,type, orset_value; the result returns resolvedtargetpluspreview.executionPlan,fallbackPlan,verificationPlan, andwarningswithout changing the UI. - Read mutation
verificationwhen present.verifiedmeans the low-level expected state was observed,failedmeans the action returned but the observed state did not match, andunverifiedmeans the tool could not prove the result. For ordinary clicks without a clear state change, still verify withwait,snapshot,elements.find, ordialog.inspect. - Use
explain=trueonly when you need the same preview attached to an executed action result; for pre-approval review, preferact.dry_run. - Use
act.perform_actionfor a named AX action when a higher-level op is not enough. It requirestargetandaxAction; common aliases such aspressandshow_menunormalize to AX names, while other valid AX action strings are attempted directly even if the target did not advertise them inactions[]. act.clickis for AX targets only. It requirestargetand should not consume rawx/y.act.clickfirst attemptsAXPress; if that fails and the target has bounds, the runtime may click the target center and report anAXPressFailed+CGEventFallback(...)execution marker.- Use
act.click_pointonly when the user explicitly wants a coordinate click or AX cannot represent the target. This includes valid coordinates like(0, 0). - Use
act.move_cursorwhen the user wants the pointer moved without clicking. It accepts eitherx/yor a target, and can smooth the path withdurationMs/steps/motionProfile. - Use
act.pressfor single-key or repeated key presses. Usehotkeyfor one chord such as Cmd+N; usepresswhen you need sequential keys, repeat, holdMs, intervalMs, or shared modifiers. - Use
act.swipefor smooth pointer drag gestures fromx/y,fromX/fromY, or a target todeltaX/deltaY,toX/toY, ortoTarget; useact.dragfor deliberate drag/drop between coordinate or AX element endpoints. PassmotionProfile="human"only when the gesture benefits from eased, less mechanical pointer motion. act.typeandact.set_valueshould target text input roles (AXTextArea,AXTextField,AXSearchField, etc.).act.typedefaults to AXSetValue. Only passtypingProfile/typingDelayMswhen the app needs real character-by-character keyboard input.- For replacement-style text entry, failed
AXSetValuecan fall back to focus + Cmd+A + protected pasteboard replace; still inspect the returnedverificationbefore assuming the text changed. - Use
act.pastefor long text or apps that do not acceptAXValuereliably. It stages text on the pasteboard, invokes paste, and reports only clipboard restore status. act,wait, anddialogresults are compact by default and do not return a full AX snapshot. SetincludeSnapshot=trueonly when full AX tree debugging is needed; otherwise verify withwait,elements.find,windows.list, ordialog.inspect.- Do not type passwords, OTPs, or private credentials unless the user explicitly supplied them in the current flow.
Visual Positioning
Use visual positioning when AX labels are missing, the UI is canvas-like, or the user refers to something visible on screen rather than a stable element.
Standard visual loop:
visual.observe screenshotTarget="window" | "display" annotate=true
act.click target.elementId="el_..." target.snapshotId="macsnap_..." # when the annotated id is clear
visual.ocr or visual.find_text text="..." # when the target is visible text
read the returned image and choose an image pixel point # when OCR is not enough
visual.point snapshotId=... coordinateSpace="image_pixels" x=... y=...
act.click target=<suggestedAction.target> # if suggestedAction.op is click
act.click_point x=<suggestedAction.x> y=<suggestedAction.y> # if suggestedAction.op is click_point
verify with snapshot, visual.observe, wait, or elements.find
Rules:
visual.observeis read-only. It returns an image file marker for model vision plus a compact JSON payload withsnapshotId, screenshot metadata, displays, and windows.- Prefer
visual.observe annotate=truefor ambiguous visual UI. The returned image is labeled with AX element ids and includesuiMap; when an id clearly identifies the target, useact.click target.elementId=... target.snapshotId=<observe snapshotId>instead of a coordinate click. - If the annotated id is unclear or the target is not in
uiMap, use OCR or image-pixel positioning. visual.ocris read-only. Use it when visible text matters but you do not need to filter for one phrase yet.visual.find_textis read-only. Use it before coordinate clicking visible words or text-only buttons; passtextMatch="contains"only for intentional partial text.visual.find_textreturns OCRtextMatcheswith center points, AXhitElements/nearestElements, a top-levelsuggestedAction, andsuggestedActions[]ordered from stable AX target to coordinate fallback.- Image pixel coordinates use the screenshot top-left as origin.
(0, 0)is valid. Never pass image pixels directly toact.click_point. - Always call
visual.pointbefore coordinate clicks chosen from a screenshot. It converts image pixels to macOS screen points and returns AXhitElements/nearestElements. - Prefer
suggestedActionfromvisual.pointorvisual.find_text; follow itsop. If it includestarget, callact.clickwith that target. If it isclick_point, use itsx/y. IfinsideFrame=false, do not click; adjust the point or observe again. - If
suggestedActions[]has multiple entries, use the first clear AX target first and keepclick_pointas a fallback after re-observing uncertainty. - If OCR returns no match, do not click blindly. Retry with
textMatch="contains", OCRlanguages, a fresh window screenshot, or use image-pixel visual positioning. - If the snapshot expired or lacks screenshot metadata, call
visual.observeagain instead of reusing old points.
Menus
- Prefer
menu.clickover hotkeys for app commands. menu.scopedefaults toapp, which targets the current frontmost app menu bar.- Use
menu.list scope="system"before operating macOS menu bar extras/status items. System menu entries include 0-basedindex, optionalboundsPoints, and may expose usefuldescription,value, andactionseven whentitleis empty. - For status items, prefer
menu.click scope="system" menuIndex=<index> verify=trueafter listing when the title is empty or localized. Verification returns likely popovers and OCR screenshot metadata when available. - Menu clicks use a native chain before giving up:
AXShowMenu, thenAXPress, then center-point click when bounds are available. - After opening a status item or menu bar extra popover, use
menu.popover appHint="..."to identify the floating panel. It ranks all-app AX windows with menu-bar geometry, host app hints, and optional OCR text; it does not click anything. - If a menu path fails, call
menu.listwith the samescopeand check the localized titles/descriptions of the current menu surface. - If the user says "do not use shortcuts", never call
act.hotkey. Use menus or AX actions.
Clipboard
clipboard.getreads user clipboard text and may expose secrets. Use it only when the user asked for clipboard content or it is clearly necessary, and keepmaxCharstight.clipboard.setis useful before a deliberate paste workflow. It does not echo the written text in the result; verify by pasting into the intended target, not by reading the clipboard back unless needed.- Prefer
act.pasteover separateclipboard.set+act.hotkeyfor text insertion; it backs up and restores the previous pasteboard items. - Use
clipboard.clearonly when the user asked to clear the clipboard or after a sensitive paste workflow.
Dialogs and Sheets
- Use
dialog.list/dialog.inspectbefore mutating dialogs when the button or field label is not already known. - macOS sheets and lightweight prompts may appear as
AXSheetorAXPopoverelements attached to normalAXWindows; inspect with highermaxElementswhen needed. - When several dialogs are present, target by dialog text/window or use the button id from the inspected result.
dialog.clickrequiresbuttonText; use the visible label. Examples:取消,保存,删除,Cancel,Save,Don't Save.dialog.inputrequirestext; usefield,fieldIndex, ortarget.elementIdwhen more than one dialog field exists. Setclear=trueto replace the value.- Dialog button presses and
clear=truetext input share the same fallbacks asact.click/act.set_value. dialog.filecan enterfilePath, setfileName, then clickselectButton(or the default accept button). It returnsfileDialog.nameField,requestedButton, and the actualselectedButtonwhen clicked. UseselectButton="none"when you only want to fill path/name.dialog.dismissmeans a cancel/close-style action. If the user wants to discard changes, choose the explicit discard button such as删除orDon't Save, not a generic dismiss guess.
Verification and Recovery
- After
apps.launch/apps.activate, verifyfrontmostbefore menu or input actions. - After any action that changes UI, re-snapshot or call the relevant list/inspect command before using old ids.
- Tool approval restores the previously frontmost app and focused window before the approved
mac_controlmutation runs, but treat it as best-effort. After an approval, verifyfrontmostor take a fresh observation before chaining another focus-sensitive action. - If an element becomes stale, take a fresh snapshot and reselect by role + label/text + window.
- If
act.perform_actionreturns an AX unsupported/action error, do not retry the same call blindly. Use freshelements.find/snapshotand choose a supported action, or switch toact.click/act.click_pointfallback. - If
dialog.inspectreturns empty but the UI visibly has a sheet, retry withmaxElements: 300or500and confirm the frontmost app. - If
menu.clicksays a path component was not found, check frontmost app andmenu.list; do not retry the same path blindly. - If a mutation succeeds but the expected state did not change, use
waitor a fresh snapshot to verify before deciding the next action. - If a failure is hard to reproduce, call
diagnostics.summaryto inspect readiness, recent errors, cached snapshot summaries, and the focus anchor. Usediagnostics.exportwhen the user/developer needs a managed JSON bundle under~/.hope-agent/mac-control/diagnostics/for replay analysis.
Approval Awareness
Treat these as higher risk and be extra explicit about the target:
windows.closeapps.quitdock.hide/dock.showspaces.switchmenu.clickon destructive menu itemsclipboard.get/clipboard.set/clipboard.cleardialog.accept/ explicit discard buttons- raw coordinate clicks, cursor moves, swipes, and drags
The approval system will enforce policy, but the model should still choose precise targets and explain uncertainty before asking the user to approve.