Skip to content

Eclipse Messages behind an external window when no Beeper window is found#92

Merged
KishanBagaria merged 5 commits into
mainfrom
kb/cli-coordination
May 30, 2026
Merged

Eclipse Messages behind an external window when no Beeper window is found#92
KishanBagaria merged 5 commits into
mainfrom
kb/cli-coordination

Conversation

@KishanBagaria
Copy link
Copy Markdown
Member

Summary

When the Messages app needs to be made automatable, EclipsingWindowCoordinator briefly shows it behind the Beeper (Electron) window. Previously, if no Electron window could be found, this hard-threw and automation failed. This branch adds a fallback to an external on-screen window as the eclipse anchor, plus a round of hardening on anchor selection and coordinate handling.

Changes

  • External-window fallback. When no Electron window is available, pick an external on-screen window to eclipse behind — preferring the frontmost app's topmost window, else the topmost window overall. Best-effort: z-order isn't guaranteed for a non-Beeper anchor, so this is documented as such.
  • Resolved-value AnchorWindow. Modeled via per-source factories (.electron / .external) instead of a struct-of-optionals filled differently per path.
  • Threading. All main-thread-affined AppKit reads (NSApp.windows, NSWorkspace, NSScreen, NSWindow) are resolved eagerly inside a main-thread hop, since makeAutomatable runs on a background queue.
  • Coordinate handling. The Cocoa↔screen flip now uses the primary display height rather than the window's own screen, fixing positioning and screen lookup on mixed-height multi-monitor setups. Extracted into a shared NSRect.flippedBetweenCocoaAndScreenSpace() helper; OnboardingManager now uses it too (also fixing its NSScreen.main usage).
  • Selection policy. Drops the fixed min-size pre-filter in favor of the uniform shouldOnlyEclipseIfEncompasses guard, and skips near-transparent/fading windows.

Dependency

Bumps BetterSwiftAX 0.1.2 → 0.1.3 (beeper/BetterSwiftAX#2), where Window.listDescriptions now skips individual malformed window descriptors instead of throwing the entire list.

Testing

swift build --target IMessage passes. Multi-monitor and no-Electron-window paths have not been exercised at runtime.

🤖 Generated with Claude Code

KishanBagaria and others added 3 commits May 31, 2026 00:42
Reworks EclipsingWindowCoordinator's anchor resolution following review:

- Model the anchor as a resolved-value AnchorWindow built via per-source
  factories, replacing the struct-of-optionals that filled fields
  differently per path.
- Resolve all main-thread-affined AppKit reads (NSApp.windows,
  NSWorkspace, NSScreen, NSWindow) eagerly inside a main-thread hop, since
  makeAutomatable runs on a background queue.
- Fix the Cocoa<->screen coordinate flip to use the primary display height
  rather than the window's own screen, correcting positioning and screen
  lookup on mixed-height multi-monitor setups. Extract the flip into a
  shared NSRect.flippedBetweenCocoaAndScreenSpace() helper and route
  OnboardingManager through it (also fixing its NSScreen.main usage).
- For the external-window fallback: select the topmost (frontmost) window
  instead of largest-by-area, drop the fixed min-size pre-filter in favor
  of the uniform encompass guard, and skip near-transparent windows.

Relies on BetterSwiftAX 0.1.3, where Window.listDescriptions now skips
individual malformed descriptors instead of throwing the whole list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 30, 2026 20:06
@indent
Copy link
Copy Markdown
Contributor

indent Bot commented May 30, 2026

PR Summary

Hardens EclipsingWindowCoordinator's anchor selection so it works reliably from a background queue, on mixed-height multi-monitor setups, and when no Beeper/Electron window is present. Centralises the Cocoa↔screen coordinate flip into a shared NSRect.flippedBetweenCocoaAndScreenSpace() helper (also adopted by OnboardingManager, fixing its NSScreen.main usage). Adopts BetterSwiftAX 0.1.3 so Window.listDescriptions skips individual malformed window descriptors.

  • Replace the struct-of-optionals anchor with a fully-resolved AnchorWindow value built via per-source factories (Electron NSWindow or external Window.Description), exposing a screenFrame already in AX/screen space.
  • Eagerly resolve all main-thread-affined AppKit reads (NSApp.windows, NSWorkspace, NSScreen, NSWindow.frame/screen) inside an onMain { DispatchQueue.main.sync } hop, since makeAutomatable is called on a background automation queue.
  • Add an external-window fallback when NSApp.largestElectronWindow is missing: filter on-screen candidates by layer == 0, alpha >= 0.5, and excluded PIDs (self + Messages), and prefer the frontmost app's topmost window; drop the fixed pre-filter min-size in favour of the existing uniform shouldOnlyEclipseIfEncompasses guard.
  • Extract the Cocoa↔screen flip to NSRect.flippedBetweenCocoaAndScreenSpace() anchored on the primary display height, and route both EclipsingWindowCoordinator.screenFrame(for:) and OnboardingManager.createOrUpdateWindow through it.
  • Bump BetterSwiftAX 0.1.2 → 0.1.3 (and canonicalise the package URL to github.com/beeper/BetterSwiftAX.git) for the resilient Window.listDescriptions parsing.

Issues

2 potential issues found:

  • log.notice("falling back to external frontmost window for eclipsing: …") fires on every makeAutomatable call while Beeper is closed; this can spam logs since makeAutomatable runs on each prepareForAutomation. Consider logging at .debug after the first occurrence, or only when the chosen anchor changes. → Autofix
  • flippedBetweenCocoaAndScreenSpace() silently returns the unflipped rect when NSScreen.screens.first is nil, so window positions end up in the wrong coordinate space without any warning. A log.warning (matching the style of screenFrame(for:)) would surface the misconfiguration. → Autofix
2 issues already resolved
  • External-window fallback can pick Messages itself as its own anchor when app is nil (so messagesPID is nil); the eclipse then no-ops visually. Trigger: makeAutomatable runs without app set — narrow today, but nothing on the coordinator side enforces it. (fixed by commit afb7649)
  • AnchorWindow.screen stores an NSScreen?, and makeAutomatable reads screen.frame / screen.visibleFrame (lines 69-70) from the background automation queue — violating the doc comment's invariant that consumers "only read plain values." Capture the screen's frames as NSRect on the main thread when building AnchorWindow, and drop the NSScreen reference. (fixed by commit 19fdad7)

CI Checks

Waiting for CI checks...


⚡ Autofix All Issues

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 95186f62-08b4-4dca-be24-bd6290b6a4de

📥 Commits

Reviewing files that changed from the base of the PR and between afb7649 and 19fdad7.

📒 Files selected for processing (1)
  • src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/EclipsingWindowCoordinator.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/EclipsingWindowCoordinator.swift

📝 Walkthrough

Summary by CodeRabbit

  • Improvements

    • More accurate conversion between Cocoa and screen/Accessibility coordinate spaces for window geometry.
    • Anchor-based eclipse positioning for more reliable alignment across multi-screen setups.
    • Improved window frame validation and expanded diagnostics for eclipse overlays.
  • Dependencies

    • Updated a pinned accessibility-support library to a newer version.

Walkthrough

Refactors eclipse placement to compute geometry from a resolved AnchorWindow in screen/AX space, adds an NSRect flip helper for Cocoa↔screen coordinates, and updates a dependency. EclipsingWindowCoordinator now resolves an Electron-first anchor with an external-window fallback and uses its screen-frame for positioning.

Changes

Window Coordinate Space and Anchor Positioning

Layer / File(s) Summary
Coordinate space flip infrastructure
src/IMessage/Sources/IMessage/Extensions.swift, Package.resolved, src/IMessage/Sources/IMessage/OnboardingManager.swift
Adds NSRect.flippedBetweenCocoaAndScreenSpace(), updates betterswiftax pin in Package.resolved, and applies the flip in OnboardingManager.createOrUpdateWindow(_:).
Eclipsing anchor window positioning
src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/EclipsingWindowCoordinator.swift
Imports WindowControl; makeAutomatable resolves an AnchorWindow via eclipsingAnchorWindow(messagesPID:) (Electron-first, external fallback) and derives target sizing/origin from anchorWindow.screenFrame. Adds AnchorWindow type and helpers for main-thread-safe resolution, Cocoa↔screen-frame conversion, external-window selection (layer/alpha/excluded-PID filtering), and screen mapping. Debug logging updated to use anchor diagnostics.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding a fallback to eclipse Messages behind an external window when Beeper is unavailable.
Description check ✅ Passed The description is directly related to the changeset, providing clear context about the external-window fallback, coordinate handling improvements, and threading considerations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch kb/cli-coordination

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens EclipsingWindowCoordinator.makeAutomatable so automation no longer hard-throws when there is no Electron/Beeper window: it now falls back to an external on-screen window as the eclipse anchor. Along the way, it consolidates anchor data into a resolved AnchorWindow value, eagerly hops AppKit reads to the main thread (since makeAutomatable runs off-main), and centralizes the Cocoa↔screen-space flip into a shared NSRect helper that OnboardingManager also adopts. Also bumps BetterSwiftAX 0.1.2 → 0.1.3 so Window.listDescriptions is resilient to malformed entries.

Changes:

  • Add external-window fallback for the eclipse anchor with AnchorWindow.electron / .external factories and a main-thread-synchronous resolver.
  • Extract NSRect.flippedBetweenCocoaAndScreenSpace() (using primary-display height) and use it from both eclipsing and onboarding paths.
  • Replace the fixed min-size pre-filter with the uniform shouldOnlyEclipseIfEncompasses guard, skip near-transparent/fading windows, and pin BetterSwiftAX to 0.1.3 in Package.resolved.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated no comments.

File Description
src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/EclipsingWindowCoordinator.swift Introduce AnchorWindow, external-window fallback, main-thread hop, and shared screen/coordinate helpers.
src/IMessage/Sources/IMessage/OnboardingManager.swift Use shared flippedBetweenCocoaAndScreenSpace() instead of ad-hoc NSScreen.main-based flip.
src/IMessage/Sources/IMessage/Extensions.swift Add reusable NSRect.flippedBetweenCocoaAndScreenSpace() helper based on primary display height.
Package.resolved Update BetterSwiftAX pin from 0.1.2 to 0.1.3 and normalize repo URL casing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

/// CGWindow bounds). The flip is about the primary display's height, so it is
/// its own inverse and is correct regardless of which display the rect is on.
func flippedBetweenCocoaAndScreenSpace() -> NSRect {
guard let primaryHeight = NSScreen.screens.first?.frame.height else { return self }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent no-op when no displays are attached: returning self here means callers (OnboardingManager, EclipsingWindowCoordinator.screenFrame(for:)) silently use an unflipped rect. The same guard appears in EclipsingWindowCoordinator.screen(containing:). In practice macOS without displays is rare, but if it ever happens (headless session, screen sleep race) the misplacement will be hard to diagnose. Consider a log.warning(...) before falling through, matching the warning style already used in screenFrame(for:).


if let description = externalEclipsingAnchorWindow(messagesPID: messagesPID) {
let anchor = AnchorWindow.external(description)
log.notice("falling back to external frontmost window for eclipsing: \(anchor.description)")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repeat log.notice on every automation: makeAutomatable is invoked on each prepareForAutomation call (see MessagesController.swift), so this .notice fires every time Messages automation runs while Beeper is closed. Existing logging style in this file uses .debug for repeated state and .notice for transitions. Suggest demoting to .debug or stashing the last-chosen anchor on the coordinator and only emitting .notice when it changes.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/EclipsingWindowCoordinator.swift (1)

75-99: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Include the configured offsets in the “encompasses” check.

Lines 75-77 only validate targetSize, but Lines 94-95 can still move the final rect outside the anchor. With non-zero eclipsingOffsetX/Y (or a tight right-aligned anchor), shouldOnlyEclipseIfEncompasses can pass even though the target window still protrudes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/EclipsingWindowCoordinator.swift`
around lines 75 - 99, The current encompasses guard only checks targetSize
against anchorWindow.screenFrame but ignores Self.eclipsingOffsetX/Y and
right-alignment, so the final targetRect created in the targetOrigin closure can
protrude; compute the same targetOrigin logic (respecting
Self.eclipsingAlignment and adding Self.eclipsingOffsetX and
Self.eclipsingOffsetY) before the guard and then validate that
anchorWindow.screenFrame fully contains the resulting NSRect(origin:
targetOrigin, size: targetSize) (or use the existing encompasses helper) when
Self.shouldOnlyEclipseIfEncompasses is true; update the guard and error log to
use that adjusted rect check so the decision matches how targetRect is actually
constructed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/EclipsingWindowCoordinator.swift`:
- Around line 69-70: makeAutomatable is dereferencing NSScreen (accessing
screen.frame/visibleFrame) on the automation thread via AnchorWindow.screen;
change AnchorWindow to store plain NSRect/strings for screenFrame and
screenVisibleFrame (populate these on the main thread where AnchorWindow is
created—see where AnchorWindow.screen is set around the population at ~154-160,
169-181) and update makeAutomatable to read those stored rects/formatted values
instead of accessing NSScreen. Also fix the shouldOnlyEclipseIfEncompasses
logic: when computing whether the eclipsing window fits, include
eclipsingOffsetX, eclipsingOffsetY and alignment adjustments so the final
targetRect (not just anchorWindow.screenFrame.size vs targetSize) is tested for
containment/fit before early-return; update any checks that reference
anchorWindow.screenFrame.size and targetSize to use the computed targetRect
bounds.

---

Outside diff comments:
In
`@src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/EclipsingWindowCoordinator.swift`:
- Around line 75-99: The current encompasses guard only checks targetSize
against anchorWindow.screenFrame but ignores Self.eclipsingOffsetX/Y and
right-alignment, so the final targetRect created in the targetOrigin closure can
protrude; compute the same targetOrigin logic (respecting
Self.eclipsingAlignment and adding Self.eclipsingOffsetX and
Self.eclipsingOffsetY) before the guard and then validate that
anchorWindow.screenFrame fully contains the resulting NSRect(origin:
targetOrigin, size: targetSize) (or use the existing encompasses helper) when
Self.shouldOnlyEclipseIfEncompasses is true; update the guard and error log to
use that adjusted rect check so the decision matches how targetRect is actually
constructed.
🪄 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: 21ce808c-76a3-43b6-946a-024bed4e423f

📥 Commits

Reviewing files that changed from the base of the PR and between 214b335 and cf5fabd.

📒 Files selected for processing (4)
  • Package.resolved
  • src/IMessage/Sources/IMessage/Extensions.swift
  • src/IMessage/Sources/IMessage/OnboardingManager.swift
  • src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/EclipsingWindowCoordinator.swift
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-05-03T17:00:19.662Z
Learnt from: KishanBagaria
Repo: beeper/platform-imessage PR: 69
File: src/IMessage/Sources/IMessage/EventWatcher/EventWatcher+Updates.swift:89-93
Timestamp: 2026-05-03T17:00:19.662Z
Learning: In the beeper/platform-imessage Swift codebase, keep message IDs (`PlatformSDK.MessageID`) as raw GUIDs. When mapping from DB/event rows to `message.id`, set the ID directly from `msgRow.guid` (no GUID→public-ID hashing or transformation). For multi-part messages, append the part index as `_<part.index>` to the GUID-derived ID. During code review, if changes touch message ID creation/mapping, ensure this raw GUID + optional `_<part.index>` suffix behavior is preserved.

Applied to files:

  • src/IMessage/Sources/IMessage/OnboardingManager.swift
  • src/IMessage/Sources/IMessage/Extensions.swift
  • src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/EclipsingWindowCoordinator.swift
🪛 SwiftLint (0.63.2)
src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/EclipsingWindowCoordinator.swift

[Warning] 270-270: Magic numbers should be replaced by named constants

(no_magic_numbers)

indent Bot and others added 2 commits May 30, 2026 20:44
Without it, the external-window fallback could pick the Messages window itself, no-opping the eclipse. The coordinator already assumes an `app` is being coordinated, so refuse to proceed when it isn't set.

Generated with [Indent](https://indent.com)
Co-Authored-By: KishanBagaria <KishanBagaria@users.noreply.github.com>
AnchorWindow.screen was an NSScreen?, and makeAutomatable read screen.frame/visibleFrame from the background automation queue. Capture both frames as NSRect? inside the onMain hop so consumers only touch plain values.

Generated with [Indent](https://indent.com)
Co-Authored-By: KishanBagaria <KishanBagaria@users.noreply.github.com>
@KishanBagaria KishanBagaria merged commit 3826e8f into main May 30, 2026
4 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request May 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants