Skip to content

fix: see an already frontmost app when focusing, and say why focus stopped - #61

Merged
iamnbutler merged 1 commit into
mainfrom
fix/focus-activation
Oct 17, 2026
Merged

iamnbutler merged 1 commit into
mainfrom
fix/focus-activation

Conversation

@iamnbutler

Copy link
Copy Markdown
Contributor

Addresses #23. This is a partial fix; live evidence is pending.

What the error was

"Timeout while waiting for condition" has only one source on the desktop_focus path: FocusManagementService.focusWindow's wait for the window's app to become active, before the raise. Both branches of that wait polled only:

  • NSRunningApplication.isActive, on a fresh instance from NSRunningApplication(processIdentifier:)
  • NSWorkspace.shared.frontmostApplication

Both are cached by AppKit and refreshed by workspace notifications on the host's main run loop. In the report, desktop_activate succeeded and the focus right after it still timed out, with nothing sent. desktop_activate's own verification requires isActive and frontmostApplication to agree, but on its own NSRunningApplication instance. So the likeliest causes are:

  • (a) a stale cached read in the focus path while the app really was frontmost;
  • (b) macOS declining a background activation request (cooperative activation, macOS 14+).

Change

New patch peekaboo-focus-activation.patch, at the end of the stack after element-depth. It is independent of #58 and #57: no shared files.

  • Fixes (a). FocusManagementService.isFrontmost accepts isActive, frontmostApplication, or the system-wide AXFocusedApplication. That last one is read directly from the Accessibility server, with a 50 ms messaging timeout. Two places use it:
    • both activation waits (accepted and not accepted);
    • focus verification's frontmost check.
  • Makes (b) actionable. A wait that ends without the app in front now throws FocusError.activationNotObserved. For example: "TextEdit (pid 4242) did not become the active app within 3 seconds of the activation request, so its window was not raised. macOS can decline activation requested from the background. Activate the app, then retry; if it still isn't frontmost, ask the user to click the window." When activate() returned false, the message says macOS did not accept the request.
  • Unchanged. Activation is still requested once, the 3 s budget and dispatch accounting are unchanged, and nothing new is dispatched.
  • Not done. desktop_activate falls back to setting AXFrontmost when activate() doesn't take. Focus could do the same, but that would be a new dispatch inside the focus accounting, so I'd rather see live evidence first. If the live check shows (b), that's the follow-up.

Tests

  • swift test --filter 'FocusActivationObservationTests|FocusDispatchAccountingTests|ForegroundModifierClickAcceptedActivationTests|FocusRaise' in Core/PeekabooAutomationKit: 34/34 pass. It ran in the foreground in its own process group with a 600 s limit, with an uncommitted isolation edit that was reverted. No helper was left behind.
  • New tests check the error text and that a nonexistent pid is never frontmost.
  • npm run check passes. The native build applies the full stack from a restored checkout and is green.

Live checks (pending: screen locked)

  • Reproduce Focus is refused on a just-restored window while another window of the same app is key #23: TextEdit with two windows, the agent's window minimized and the other key. desktop_restore it, then desktop_focus → completed, or activationNotObserved with a clear reason. Record which, and whether TextEdit was frontmost beforehand.
  • The same with TextEdit already frontmost (after desktop_activate) → completed.
  • With another app frontmost, if focus reports "did not become the active app", record the macOS version. That's the evidence for the AXFrontmost fallback.

🤖 Generated with Claude Code

…opped

Co-Authored-By: Claude <noreply@anthropic.com>
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.

1 participant