cua-driver: NSMenu key equivalents on backgrounded apps, overlay z-order, focus fixes - #1437
Conversation
…d hermes form-fill tests
## Overlay z-ordering (NSWindowLevel.normal)
Changed the agent-cursor overlay from NSWindowLevel.floating (above all
normal windows) to NSWindowLevel.normal so it is sandwiched just above
the target window: [target, overlay, fg-windows]. Foreground windows
above the target now correctly occlude the cursor, making it visually
clear which window the agent is interacting with.
## Continuous repin loop (30 fps)
Replaced the sparse one-shot defensive-repin schedule [60, 180, 360,
600, 900, 1200ms] with a continuous ~30 fps loop. macOS sometimes
raises a background window's z-level asynchronously after an AX action;
the old schedule left a gap of up to 300ms where the overlay sat below
the target. At 33ms the overlay snaps back within one frame.
## Background Safari form-fill fixes
- ClickTool: 800ms post-AXPress delay for AXTextField/AXTextArea so
WebKit establishes DOM focus before type_text_chars fires (email/text
field race condition).
- ClickTool: detect AXPopUpButton clicks, list available options and
hint to use set_value instead of click (native popup closes on bg).
- SetValueTool: two-strategy popup selection — AX child AXPress (native
AppKit) with JavaScript injection fallback for Safari/WebKit selects
that expose no AX children when the popup is closed.
- AXInput: added screenBoundingRect(), children(), stringAttribute()
helpers used by ClickTool and SetValueTool.
## Agent cursor focus rect
AgentCursorRenderer/View/Cursor: draw a cyan glowing rounded rect around
the targeted AX element's screen bbox after each click so the user sees
which element the agent targeted.
## FocusMonitorApp enhancements
Added TrackingTextField (NSTextField subclass) that holds keyboard focus
throughout the test run and tracks field-level resignFirstResponder
events. Three-level focus-loss counters (app, window key, text field)
written to /tmp/focus_monitor_{losses,key_losses,field_losses}.txt.
## Structured content for list_windows / launch_app
Both tools now return structuredContent alongside their human-readable
text so integration tests can access window/app data without parsing.
## New integration tests
- test_hermes_form_fill.py: 9 tests driving individual HTML input types
(text, password, email, number, textarea, checkbox, select, radio,
submit) in a backgrounded Safari window via the hermes CLI, asserting
no focus is stolen from FocusMonitorApp.
- test_overlay_z_order.py: asserts overlay appears at layer 0 (not
floating/layer 3) and is sandwiched between the target and any
foreground windows above it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AgentCursor: call clearFocusRect() in hide() so the focus rect does not linger after the cursor auto-hides (the helper had a docstring explicitly saying it should be called there but was never wired up) - SetValueTool: fix value escaping in JS/AppleScript injection path. Previously only single-quotes were escaped, leaving double-quotes and backslashes able to break the outer AppleScript string or inject arbitrary commands. Switch to percent-encoding with unreserved chars only and decode via decodeURIComponent() in the JS, eliminating the double-escape problem entirely. Also add a 10-second polling timeout around proc.waitUntilExit() so a stuck Safari/osascript does not block the MCP tool handler indefinitely. - FocusMonitorApp: update doc-comment from "two kinds" to "three kinds" of focus loss to match the implementation (app-level, window key status, and text-field first-responder, the third being the newest addition tracked via TrackingTextField). - test_hermes_form_fill: change module-level os.environ["ANTHROPIC_API_KEY"] to os.environ.get(..., "") so test discovery / lint / collect-only passes do not raise KeyError. setUpClass now skips the suite cleanly when the key is absent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t SPIs Fix hotkey/press_key silently swallowing NSMenu key equivalents (Cmd+N, Cmd+S, Cmd+W, …) when the target app is backgrounded. Root cause: SLEventPostToPid with the SkyLight auth-message envelope forks onto a direct-mach delivery path that bypasses IOHIDPostEvent. NSMenu key equivalents are dispatched in NSApplication.sendEvent: which only processes the IOHIDPostEvent stream — so auth-enveloped events are silently ignored. Fix (when window_id is supplied to hotkey/press_key): 1. FocusWithoutRaise.activateForMenuShortcut — calls SLPSSetFrontProcessWithOptions(kCPSNoWindows = 0x400) to make the target WindowServer-frontmost without raising its window or triggering Space follow. 2. Post via SLEventPostToPid WITHOUT the auth-message envelope (attachAuthMessage: false) — routes through IOHIDPostEvent so NSApplication.sendEvent: dispatches NSMenu key equivalents. The Chromium/renderer path (no window_id) is unchanged: auth-message envelope stays on since Chromium requires it for its keyboard pipeline. Also replaces the deprecated GetProcessForPID Carbon API (removed in macOS 26) with SLSGetWindowOwner + SLSGetConnectionPSN for window-ID → PSN lookup. Test: test_background_menu_shortcut.py — verifies via FocusMonitorApp: - hotkey WITHOUT window_id → 0 focus losses (no activation, Chromium path) - hotkey WITH window_id → 1 focus loss (activateForMenuShortcut) + new TextEdit document opens (NSMenu Cmd+N fires correctly) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR enhances menu key equivalent handling for backgrounded applications by adding window-id-based focus activation (without raising or stealing focus), introducing an ChangesBackground Menu Shortcut Support
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Conflicts resolved by taking main's additions for all hunks: - .bumpversion.cfg / CuaDriverCore.swift: version 0.1.1 → 0.1.2 - ScreenshotTool.swift: window_id is now required - CuaDriverCommand.swift: add --claude-code-computer-use-compat flag - CLIDocExtractor.swift: document the new compat flag - install.sh: add claude-code-computer-use-compat install note - uninstall.sh: add cache dir + Claude MCP registration cleanup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
libs/cua-driver/Sources/CuaDriverCore/Input/FocusWithoutRaise.swift (1)
50-69:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPreserve the PID fallback for pre-macOS 15.
Both activation paths now depend entirely on
getProcessPSN(forWindowId:), but that helper returnsfalsewhenSLSGetWindowOwner/SLSGetConnectionPSNare unavailable. SincetargetPidis still available here, older systems lose background focus/menu-shortcut support unnecessarily.Suggested fix
let targetOk = targetPSN.withUnsafeMutableBytes { raw in - SkyLightEventPost.getProcessPSN(forWindowId: targetWid, into: raw.baseAddress!) + SkyLightEventPost.getProcessPSN(forWindowId: targetWid, into: raw.baseAddress!) + || SkyLightEventPost.getProcessPSN(forPid: targetPid, into: raw.baseAddress!) } guard targetOk else { return false }Apply the same fallback in
activateForMenuShortcut(...)as well.Also applies to: 127-136
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/cua-driver/Sources/CuaDriverCore/Input/FocusWithoutRaise.swift` around lines 50 - 69, In activateWithoutRaise, retain the pre-macOS15 PID fallback: if SkyLightEventPost.getProcessPSN(forWindowId:into:) returns false, fall back to constructing a PSN (process serial number) or using the PID path used previously (same approach as activateForMenuShortcut) so the call path still works on older systems where SLSGetWindowOwner/SLSGetConnectionPSN are unavailable; update both activateWithoutRaise and activateForMenuShortcut to attempt getProcessPSN(forWindowId:into:) first and if it fails use the targetPid-based PSN/pid-fallback code path (refer to prevPSN, targetPSN, getFrontProcess, and getProcessPSN symbols to locate where to insert the fallback).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@libs/cua-driver/Sources/CuaDriverServer/Tools/HotkeyTool.swift`:
- Around line 102-117: The branch silently proceeds when rawWindowId is nil/out
of UInt32 range or when
FocusWithoutRaise.activateForMenuShortcut(targetPid:targetWid:) fails; change
the logic in the block that checks rawWindowId and calls
FocusWithoutRaise.activateForMenuShortcut so it fails fast: if rawWindowId is
nil or UInt32(exactly: rawWindowId) is nil, throw or return an error immediately
instead of falling back to the non-windowed hotkey path, and after calling
FocusWithoutRaise.activateForMenuShortcut, check its boolean result and
throw/return on false before calling KeyboardInput.hotkey(keys, toPid: pid,
attachAuthMessage: false) so callers see the real failure rather than a silent
no-op.
In `@libs/cua-driver/Sources/CuaDriverServer/Tools/PressKeyTool.swift`:
- Around line 160-171: FocusWithoutRaise.activateForMenuShortcut can fail
silently yet you proceed to call KeyboardInput.press with
attachAuthMessage:false; update the PressKeyTool logic to check the boolean
result of FocusWithoutRaise.activateForMenuShortcut (when rawWindowId -> UInt32
path is taken) and handle failure explicitly: if activateForMenuShortcut returns
false, do not call KeyboardInput.press(..., attachAuthMessage: false) — instead
either call the fallback KeyboardInput.press(key, modifiers: modifiers, toPid:
pid) or propagate/throw an error so the caller sees the failure; ensure the
change is applied around the same conditional that converts rawWindowId to
UInt32 and references FocusWithoutRaise.activateForMenuShortcut and
KeyboardInput.press.
In `@libs/cua-driver/Tests/integration/test_background_menu_shortcut.py`:
- Around line 107-121: The current _launch_focus_app() blocks on
proc.stdout.readline() and can hang; modify the loop to poll the subprocess
stdout with a timeout (e.g., using select.select on proc.stdout.fileno() or
os.poll) and only call proc.stdout.readline() when data is available, handle EOF
(empty read) by terminating the process and raising, and preserve the existing
4s total timeout/loop count and the returned (proc, pid) behavior; update
_launch_focus_app to use select/select-like readiness checks before reading, and
ensure proc.terminate() is still called on timeout or EOF.
---
Outside diff comments:
In `@libs/cua-driver/Sources/CuaDriverCore/Input/FocusWithoutRaise.swift`:
- Around line 50-69: In activateWithoutRaise, retain the pre-macOS15 PID
fallback: if SkyLightEventPost.getProcessPSN(forWindowId:into:) returns false,
fall back to constructing a PSN (process serial number) or using the PID path
used previously (same approach as activateForMenuShortcut) so the call path
still works on older systems where SLSGetWindowOwner/SLSGetConnectionPSN are
unavailable; update both activateWithoutRaise and activateForMenuShortcut to
attempt getProcessPSN(forWindowId:into:) first and if it fails use the
targetPid-based PSN/pid-fallback code path (refer to prevPSN, targetPSN,
getFrontProcess, and getProcessPSN symbols to locate where to insert the
fallback).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a67516df-5acd-4230-832a-baf0934ab8f3
📒 Files selected for processing (7)
libs/cua-driver/Sources/CuaDriverCore/Input/FocusWithoutRaise.swiftlibs/cua-driver/Sources/CuaDriverCore/Input/KeyboardInput.swiftlibs/cua-driver/Sources/CuaDriverCore/Input/SkyLightEventPost.swiftlibs/cua-driver/Sources/CuaDriverServer/Tools/HotkeyTool.swiftlibs/cua-driver/Sources/CuaDriverServer/Tools/PressKeyTool.swiftlibs/cua-driver/Sources/CuaDriverServer/Tools/ScreenshotTool.swiftlibs/cua-driver/Tests/integration/test_background_menu_shortcut.py
| if let rawWid = rawWindowId, rawWid != 0, | ||
| let wid = UInt32(exactly: rawWid) | ||
| { | ||
| FocusWithoutRaise.activateForMenuShortcut( | ||
| targetPid: pid, targetWid: CGWindowID(wid)) | ||
| usleep(50_000) | ||
| // Post WITHOUT the SkyLight auth-message envelope. | ||
| // With the envelope, SLEventPostToPid forks onto a direct-mach | ||
| // path that bypasses IOHIDPostEvent — NSMenu never sees those | ||
| // events. Without the envelope the path goes through IOHIDPostEvent | ||
| // so NSApplication.sendEvent: dispatches NSMenu key equivalents. | ||
| // SLPSSetFrontProcessWithOptions (called inside activateForMenuShortcut) | ||
| // already made the target WindowServer-frontmost. | ||
| try KeyboardInput.hotkey(keys, toPid: pid, attachAuthMessage: false) | ||
| } else { | ||
| try KeyboardInput.hotkey(keys, toPid: pid) |
There was a problem hiding this comment.
Fail fast when window_id is invalid or activation fails.
This branch silently degrades if window_id is out of UInt32 range or activateForMenuShortcut(...) returns false, but it still reports success. For the NSMenu path that turns a real targeting failure into a silent no-op.
Suggested fix
- if let rawWid = rawWindowId, rawWid != 0,
- let wid = UInt32(exactly: rawWid)
- {
- FocusWithoutRaise.activateForMenuShortcut(
- targetPid: pid, targetWid: CGWindowID(wid))
+ if let rawWid = rawWindowId {
+ guard rawWid != 0, let wid = UInt32(exactly: rawWid) else {
+ return errorResult(
+ "window_id \(rawWid) is outside the supported UInt32 range.")
+ }
+ guard FocusWithoutRaise.activateForMenuShortcut(
+ targetPid: pid, targetWid: CGWindowID(wid))
+ else {
+ return errorResult(
+ "Failed to activate window_id \(rawWid) for NSMenu shortcut delivery.")
+ }
usleep(50_000)
// Post WITHOUT the SkyLight auth-message envelope.
// With the envelope, SLEventPostToPid forks onto a direct-mach
@@
- } else {
+ } else {
try KeyboardInput.hotkey(keys, toPid: pid)
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@libs/cua-driver/Sources/CuaDriverServer/Tools/HotkeyTool.swift` around lines
102 - 117, The branch silently proceeds when rawWindowId is nil/out of UInt32
range or when FocusWithoutRaise.activateForMenuShortcut(targetPid:targetWid:)
fails; change the logic in the block that checks rawWindowId and calls
FocusWithoutRaise.activateForMenuShortcut so it fails fast: if rawWindowId is
nil or UInt32(exactly: rawWindowId) is nil, throw or return an error immediately
instead of falling back to the non-windowed hotkey path, and after calling
FocusWithoutRaise.activateForMenuShortcut, check its boolean result and
throw/return on false before calling KeyboardInput.hotkey(keys, toPid: pid,
attachAuthMessage: false) so callers see the real failure rather than a silent
no-op.
| if let rawWid = rawWindowId, rawWid != 0, | ||
| let wid = UInt32(exactly: rawWid) | ||
| { | ||
| FocusWithoutRaise.activateForMenuShortcut( | ||
| targetPid: pid, targetWid: CGWindowID(wid)) | ||
| usleep(50_000) | ||
| // Same recipe as HotkeyTool — see its inline comment. | ||
| try KeyboardInput.press( | ||
| key, modifiers: modifiers, toPid: pid, attachAuthMessage: false) | ||
| } else { | ||
| try KeyboardInput.press( | ||
| key, modifiers: modifiers, toPid: pid) |
There was a problem hiding this comment.
Handle activation failure before posting the key.
This has the same silent-failure mode as HotkeyTool: an invalid/stale window_id, or a false return from activateForMenuShortcut(...), still leads to a success result even though the NSMenu-targeted path was not established.
Suggested fix
- if let rawWid = rawWindowId, rawWid != 0,
- let wid = UInt32(exactly: rawWid)
- {
- FocusWithoutRaise.activateForMenuShortcut(
- targetPid: pid, targetWid: CGWindowID(wid))
+ if let rawWid = rawWindowId {
+ guard rawWid != 0, let wid = UInt32(exactly: rawWid) else {
+ return errorResult(
+ "window_id \(rawWid) is outside the supported UInt32 range.")
+ }
+ guard FocusWithoutRaise.activateForMenuShortcut(
+ targetPid: pid, targetWid: CGWindowID(wid))
+ else {
+ return errorResult(
+ "Failed to activate window_id \(rawWid) for NSMenu key delivery.")
+ }
usleep(50_000)
// Same recipe as HotkeyTool — see its inline comment.
try KeyboardInput.press(
key, modifiers: modifiers, toPid: pid, attachAuthMessage: false)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@libs/cua-driver/Sources/CuaDriverServer/Tools/PressKeyTool.swift` around
lines 160 - 171, FocusWithoutRaise.activateForMenuShortcut can fail silently yet
you proceed to call KeyboardInput.press with attachAuthMessage:false; update the
PressKeyTool logic to check the boolean result of
FocusWithoutRaise.activateForMenuShortcut (when rawWindowId -> UInt32 path is
taken) and handle failure explicitly: if activateForMenuShortcut returns false,
do not call KeyboardInput.press(..., attachAuthMessage: false) — instead either
call the fallback KeyboardInput.press(key, modifiers: modifiers, toPid: pid) or
propagate/throw an error so the caller sees the failure; ensure the change is
applied around the same conditional that converts rawWindowId to UInt32 and
references FocusWithoutRaise.activateForMenuShortcut and KeyboardInput.press.
| def _launch_focus_app() -> tuple[subprocess.Popen, int]: | ||
| proc = subprocess.Popen( | ||
| [_FOCUS_APP_EXE], | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.DEVNULL, | ||
| text=True, | ||
| ) | ||
| for _ in range(40): | ||
| line = proc.stdout.readline().strip() | ||
| if line.startswith("FOCUS_PID="): | ||
| pid = int(line.split("=", 1)[1]) | ||
| return proc, pid | ||
| time.sleep(0.1) | ||
| proc.terminate() | ||
| raise RuntimeError("FocusMonitorApp did not print FOCUS_PID in time") |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Python's subprocess module, does Popen.stdout.readline() block until it receives a newline or EOF, and what non-blocking pattern is recommended for implementing a timeout while waiting for child process output?
💡 Result:
Yes—Popen.stdout.readline blocks until it can return a complete “line” (a trailing newline in text mode, or a newline byte in binary mode) or until it reaches EOF. How to implement a timeout for waiting on child output (non-blocking pattern): don’t try to make readline non-blocking directly. Instead, use a readiness wait (select/poll) or an asynchronous/threaded reader that you can time out. Recommended approach (works with line-delimited output): 1) Use select.select with a timeout to wait until the pipe is readable, then call readline (which will not block because data is ready). Example: import subprocess, select p = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True, bufsize=1) deadline = 10 # seconds end = time.time + deadline buf = '' while True: remaining = end - time.time if remaining <= 0: raise TimeoutError('Timed out waiting for child output') r, _, _ = select.select([p.stdout], [], [], remaining) if not r: raise TimeoutError('Timed out waiting for child output') line = p.stdout.readline # now safe: pipe is readable if line == '': # EOF break print(line, end='') 2) If you already use asyncio, the subprocess-aware timeout pattern is to await readline with asyncio.wait_for(process.stdout.readline, timeout). Example (asyncio): line = await asyncio.wait_for(process.stdout.readline, timeout) Evidence notes: - subprocess pipes should be read/handled carefully; the official docs recommend communicate to avoid deadlocks when using stdout/stderr pipes, and they describe that communicate reads until EOF. - For timeouts while reading lines from a PIPE, the documented non-blocking pattern is to use select/poll to wait for readability before calling readline, rather than calling readline directly (which otherwise blocks waiting for newline/EOF).
Citations:
- 1: https://docs.python.org/3.15/library/subprocess.html
- 2: https://docs.python.org/3/library/subprocess.html?highlight=subprocess.popen
- 3: https://stackoverflow.com/questions/10756383/timeout-on-subprocess-readline-in-python
- 4: https://stackoverflow.com/questions/1388753/how-to-get-output-from-subprocess-popen-proc-stdout-readline-blocks-no-dat
- 5: https://stackoverflow.com/questions/375427/a-non-blocking-read-on-a-subprocess-pipe-in-python
This timeout loop can hang forever on the first readline().
proc.stdout.readline() blocks until a newline is received or EOF is reached. If FocusMonitorApp fails before printing FOCUS_PID=..., the loop never reaches its intended 4-second timeout and wedges the integration suite instead of failing cleanly.
Suggested fix
+import select
@@
def _launch_focus_app() -> tuple[subprocess.Popen, int]:
proc = subprocess.Popen(
[_FOCUS_APP_EXE],
stdout=subprocess.PIPE,
@@
- for _ in range(40):
- line = proc.stdout.readline().strip()
- if line.startswith("FOCUS_PID="):
- pid = int(line.split("=", 1)[1])
- return proc, pid
- time.sleep(0.1)
+ deadline = time.time() + 4.0
+ while time.time() < deadline:
+ ready, _, _ = select.select([proc.stdout], [], [], 0.1)
+ if not ready:
+ if proc.poll() is not None:
+ break
+ continue
+ line = proc.stdout.readline().strip()
+ if line.startswith("FOCUS_PID="):
+ pid = int(line.split("=", 1)[1])
+ return proc, pid
proc.terminate()
raise RuntimeError("FocusMonitorApp did not print FOCUS_PID in time")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@libs/cua-driver/Tests/integration/test_background_menu_shortcut.py` around
lines 107 - 121, The current _launch_focus_app() blocks on
proc.stdout.readline() and can hang; modify the loop to poll the subprocess
stdout with a timeout (e.g., using select.select on proc.stdout.fileno() or
os.poll) and only call proc.stdout.readline() when data is available, handle EOF
(empty read) by terminating the process and raising, and preserve the existing
4s total timeout/loop count and the returned (proc, pid) behavior; update
_launch_focus_app to use select/select-like readiness checks before reading, and
ensure proc.terminate() is still called on timeout or EOF.
- Call unhide() after launch so windows appear on screen in the background - If app already running with no on-screen windows (e.g. Finder windows on a different Space), re-send an oapp AppleEvent to create a new window on the current Space — same behavior as clicking the Dock icon - Update tool description: "Launches in the background" (was "Launches hidden") Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ri to about:blank - Finder fallback: open ~/ via application(_:open:) when no on-screen windows - Safari fallback: open about:blank instead of ~/ to avoid file:// URL side effects - Scope fallback to com.apple.finder and com.apple.Safari only (avoids unintended side effects on document apps, Calculator, etc.) - Add integration test: test_launch_app_visible.py covering TextEdit cold-launch, Calculator focus-steal suppression, and Finder no-URL window creation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
hotkey/press_keywithwindow_idnow fires NSMenu key equivalents (Cmd+N, Cmd+S, Cmd+W, …) on backgrounded native AppKit apps. Previously, SkyLight's auth-message envelope routed events onto a direct-mach path that bypassed IOHIDPostEvent, so NSMenu never saw them. Fix: callFocusWithoutRaise.activateForMenuShortcut(SLPSSetFrontProcessWithOptions with kCPSNoWindows = 0x400) then post without the auth envelope — events route through IOHIDPostEvent and NSApplication.sendEvent dispatches NSMenu key equivalents normally.GetProcessForPIDCarbon API (removed in macOS 26 / Darwin 25.x) withSLSGetWindowOwner + SLSGetConnectionPSNfor window-ID → PSN lookup.activateWithoutRaisenow uses window-ID for PSN lookup (compatible with macOS 26).drag,page,set_agent_cursor_style; browser JS execution via CDP/WebKit/WebInspector.Test plan
scripts/test.sh test_background_menu_shortcut— both cases pass (Cmd+Z no-window_id → 0 focus losses; Cmd+N with window_id → 1 focus loss + TextEdit window count +1)scripts/test.sh test_overlay_z_order— overlay stays on topscripts/test.sh test_hermes_form_fill— all 9 Hermes tests pass🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
window_idparameter to hotkey and key press tools for menu shortcut supportImprovements
Tests