Skip to content

fix(menubar): exclude parked items from the display-spread gate - #930

Merged
diazdesandi merged 2 commits into
developmentfrom
fix/exclude-parked-items-from-display-spread-gate
Aug 10, 2026
Merged

fix(menubar): exclude parked items from the display-spread gate#930
diazdesandi merged 2 commits into
developmentfrom
fix/exclude-parked-items-from-display-spread-gate

Conversation

@nightah

@nightah nightah commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

LayoutSolver.itemsSpanMultipleDisplays assumed parked hidden items land on no display, which is false whenever a screen sits to the left of the main one. The predicate then reported a spread on a settled layout, permanently, and the persist gate it feeds stopped saveSectionOrder from ever running. Callers now exclude parked items before consulting it.

Scope: This PR changes one focused thing (bug fix or feature) plus minimal plumbing. Larger refactors need prior agreement in the issue.

Linked issue (required)

Closes: N/A

Reported directly against 2.0.0-rc.2.1 with diagnostic logs rather than through the tracker. Not a duplicate of #927, which reports a similar symptom on a single monitor, where this predicate short-circuits at guard screenFrames.count > 1.

PR Type

  • Bug fix
  • CI/CD
  • Documentation
  • Feature
  • Enhancement
  • Performance improvement
  • Refactor
  • Test addition or update
  • Other (please describe)

Area

  • menubar
  • icebar
  • layout
  • appearance
  • settings
  • onboarding
  • permissions
  • profiles
  • hotkeys
  • updates
  • ops

Does this PR introduce a breaking change?

  • Yes - if yes, please describe the impact and migration path
  • No

What is the new behavior?

itemsSpanMultipleDisplays was documented as ignoring parked hidden items because the control item shoves them "thousands of points to the left", onto no display. That assumption only holds when no screen occupies negative x. On the reporter's arrangement (main at x=0 3440 wide, a second display at x=3440, and a third at x=-2560) the hidden section parks at x ≈ -2400 to -2570, which is inside the third display's frame. The predicate matched two distinct screens and returned true on a completely settled layout, for as long as that display stayed connected.

Both consumers then fired continuously. The persist gate skipped saveSectionOrder on every cache cycle: one field log covering a single day recorded 1088 skips and zero successful writes, and MenuBarItemManager.savedSectionOrder on disk was three empty arrays. Everything the user saw came from the active display profile's frozen snapshot, because armProfileState overwrites the in-memory copy at startup while ProfileManager.captureCurrentLayout reads the empty persisted one.

With the saved order unable to advance, any item moved into always-hidden stayed absent from desiredAH, so partitionUnmanagedUIDs kept it and planUnmanagedPlacement classified it as a new item bound for the new-items section. Quitting any app changed the window-ID set, which dispatched a bulk apply, which dragged the item back to visible. From the reporter's log:

:42  cacheItemsIfNeeded: window IDs changed (30 cached vs 29 current)
:59  applySavedLayout: dispatching bulk apply (windowID change)
:73  current always-hidden section: [OneDrive, cisco, cortex, wdav, beyondtrust]
:81  desiredAH = [beyondtrust, cisco, wdav]
:74  planUnmanagedPlacement com.paloaltonetworks.cortex.agent:Item-0 -> newItemDefault(section=visible section)
:442 Skipping saveSectionOrder; menu bar items span multiple displays (relocation in progress)

The log corroborates the geometry independently: the hidden section captured cleanly at those coordinates while the always-hidden items at x ≈ -7660 produced captureWindowsImageSCK: no display intersects. Hidden items were on a real display; always-hidden ones were not.

What changed

Callers exclude parked items rather than relying on them falling outside every screen frame. The persist path passes visible-section centers only; applySavedLayout and rebalanceNotchOverflowIfNeeded filter to items at or right of the hidden divider's minX. A real relocation still splits those items across screens, so deferring on mid-migration geometry is unchanged.

Two related repairs travel with it:

  • applySavedLayout regains the display-spread guard that d2824fa4 dropped while leaving the persist-side guard in place. Both halves landed together in 4c04159e; the asymmetry is what made this visible rather than merely inert, because applies kept running against an order that could no longer be saved.
  • rebalanceNotchOverflowIfNeeded derives its screen frames from CGDisplayBounds instead of NSScreen.frame, so the rectangles share the top-left origin space of the item bounds they are tested against rather than mixing it with AppKit's bottom-left origin.

The persist-side skip is logged at warning rather than debug, matching the sibling branches around it. A run of these means the saved layout has stopped tracking the user's arrangement, and the symptom that surfaces gives no hint that persistence is the cause.

Regression range

4c04159e (2026-06-24) introduced the predicate and both gates. 2.0.0-rc.1 is dated 2026-06-16, so this regressed inside the rc.1 to rc.2 window and shipped in 2.0.0-rc.2 and 2.0.0-rc.2.1.

PR Checklist

  • I've built and run the app locally and verified that it works as expected.
  • I've run swiftformat . to keep the code style consistent.
  • I've run the smallest relevant test commands (list 1–2 below), e.g. xcodebuild test … or swift test --package-path MenuBarModel.
  • I've added tests for new behavior (if applicable).
  • I've documented new public APIs / non-obvious helpers.
  • I've updated documentation as needed.
  • This PR targets the development branch.
  • If this PR changes dependencies / lockfiles (Package.resolved, Actions pins, etc.), dependency-sca is green — or any osv-scanner.toml suppression includes both reason and ignoreUntil (see SECURITY.md § Dependency SCA policy).

Test commands run:

  • xcodebuild test -project Thaw.xcodeproj -scheme Thaw -destination 'platform=macOS' -only-testing:ThawTests/DisplaySpreadGateTests
  • xcodebuild test -project Thaw.xcodeproj -scheme Thaw -destination 'platform=macOS' (full suite: 1993 tests in 257 suites)
  • swiftformat was run against the touched files only.
  • swiftlint is clean on all three touched files.

Verified on the reporting machine

Debug build run against the three-display arrangement that reproduces the bug:

Signal Before After
span multiple displays skips 1088 0
Saved section order: writes 0 1, six seconds after launch
planUnmanagedPlacement for the affected item every window-ID change 0

MenuBarItemManager.savedSectionOrder went from three empty arrays to a populated dict with the affected item present in alwaysHidden, which is the condition that stops partitionUnmanagedUIDs from picking it up at all.

Other information

The predicate keeps its signature and its behaviour of ignoring centers that match no screen. That fallback is still useful, but its doc comment now states plainly that it is not the filter and that callers own the exclusion, so the next caller does not re-derive the same assumption.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by CodeRabbit

  • Bug Fixes

    • Improved menu bar item placement across multiple displays.
    • Prevented hidden or parked items from incorrectly triggering layout changes, cache updates, or notch-overflow rebalancing.
    • Improved handling of display coordinates to avoid false cross-display relocation detection.
  • Documentation

    • Clarified how parked and hidden items are handled during display-spread detection.
  • Tests

    • Added coverage for parked items, settled items, and genuine cross-display movement scenarios.

`LayoutSolver.itemsSpanMultipleDisplays` was documented as ignoring parked hidden items on the grounds that the control item shoves them "thousands of points to the left", onto no display at all. That only holds when no screen occupies negative x. On an arrangement with a display positioned to the left of the main one, hidden-section items park at coordinates that fall squarely inside that display's frame, so the predicate matched two distinct screens and reported a spread on a completely settled layout.

Both gates that consult the predicate then fired continuously. The persist gate in the cache-update path skipped `saveSectionOrder` on every cycle: a field log covering a single day recorded 1088 skips and zero successful writes, leaving `MenuBarItemManager.savedSectionOrder` on disk as three empty arrays. With the saved order frozen, any item the user moved into always-hidden was absent from `desiredAH`, so `partitionUnmanagedUIDs` kept it, `planUnmanagedPlacement` classified it as a new item, and every window-ID change dragged it back to the new-items section. Quitting any app was enough to trigger it.

Callers now exclude parked items before consulting the predicate rather than relying on them landing on no display. The persist path passes visible-section centers only; `applySavedLayout` and `rebalanceNotchOverflowIfNeeded` filter to items at or right of the hidden divider's `minX`. A genuine relocation still splits those items across screens, so the protection against applying or persisting mid-migration geometry is unchanged.

Two related repairs travel with it. `applySavedLayout` regains the display-spread guard that `d2824fa4` dropped while leaving the persist-side guard in place; that asymmetry is what made the stale layout visible, because applies kept running against an order that could no longer be saved. And `rebalanceNotchOverflowIfNeeded` now derives its screen frames from `CGDisplayBounds` rather than `NSScreen.frame`, so the rectangles share the top-left origin space of the item bounds they are tested against instead of mixing it with AppKit's bottom-left origin.

The persist-side skip is logged at warning rather than debug, matching the sibling branches around it. A run of these means the saved layout has stopped tracking the user's arrangement entirely, and the symptom that surfaces gives no hint that persistence is the cause.

Signed-off-by: Amir Zarrinkafsh <3339418+nightah@users.noreply.github.com>
@github-actions github-actions Bot added fix PR that fixes a defect (issue reports use bug) layout Saved layouts, LayoutBar, reorder, spacing menubar Hide/show, sections, control items, backends, capacity test Test additions or updates labels Aug 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Labels: test, fix, menubar, layout

All convention checks passed. ✅

cc @nightah

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a538ac06-4090-4470-b7ff-2c0cfcf8544d

📥 Commits

Reviewing files that changed from the base of the PR and between 959b049 and 430ab7c.

📒 Files selected for processing (1)
  • Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift

📝 Walkthrough

Walkthrough

Display-spread detection now excludes parked hidden and always-hidden items from layout persistence, saved-layout application, and notch-overflow rebalancing. Tests cover parked coordinates on left displays and unparked cross-display placement.

Changes

Display spread filtering

Layer / File(s) Summary
Display spread contract and coverage
Thaw/MenuBar/MenuBarItems/LayoutSolver.swift, ThawTests/MenuBar/Items/DisplaySpreadGateTests.swift
The contract requires unparked item centers. Tests cover parked items inside and outside displays, plus unparked items spanning displays.
Menu bar spread decision paths
Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift
Section-order persistence, saved-layout application, and notch-overflow rebalancing exclude parked items and use CGDisplayBounds for display checks. The rebalancing path receives the resolved ControlItemPair.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: diazdesandi, stonerl

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.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 identifies the main bug fix: excluding parked items from the menubar display-spread gate.
Description check ✅ Passed The description follows the template, includes the required Closes line, explains the fix, documents testing, and identifies scope and behavior.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/exclude-parked-items-from-display-spread-gate

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.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 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 `@Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift`:
- Around line 9024-9030: Update the caller and the method containing the
`unparkedItems` calculation to receive the hidden divider position from the
retained control-item data, such as `controlItems.hidden.bounds.minX` or the
`ControlItemPair` itself. Use that passed value to always filter items before
calling `LayoutSolver.itemsSpanMultipleDisplays`, including the
`cacheItemsRegardless` path where `.hiddenControlItem` has already been removed
from `items`.
🪄 Autofix

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: ASSERTIVE

Plan: Pro Plus

Run ID: 9b407a6e-dbfc-4927-90a6-dc279adc6741

📥 Commits

Reviewing files that changed from the base of the PR and between 2344eb3 and bc11378.

📒 Files selected for processing (3)
  • Thaw/MenuBar/MenuBarItems/LayoutSolver.swift
  • Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift
  • ThawTests/MenuBar/Items/DisplaySpreadGateTests.swift

Comment thread Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift Outdated
@nightah
nightah force-pushed the fix/exclude-parked-items-from-display-spread-gate branch 2 times, most recently from 959b049 to 8673dd7 Compare August 10, 2026 23:35
`rebalanceNotchOverflowIfNeeded` looked its hidden divider up by searching the item array it was handed for `.hiddenControlItem`. Its only caller is `cacheItemsRegardless`, which builds its `ControlItemPair` with an inout array, and that initializer removes the hidden and always-hidden control items from the array it is given. The caller then passes that same stripped array down, so the lookup always found nothing, the optional binding never fired, and the display-spread gate fell back to its unfiltered branch and kept receiving parked items. The pair is now passed in from the caller and the filter runs unconditionally against `controlItems.hidden.bounds.minX`.

The function no longer re-derives its own `ControlItemPair` from that stripped array either. The copy it built for the purpose was used nowhere else, and the `guard` around it could not succeed once the control items had been removed upstream.

The doc comment on `lastNotchRebalanceTimestamp` drops its DocC backticks while being updated for the new signature, per the project convention that backticks in Swift comments are reserved for GitHub-rendered surfaces.

Signed-off-by: Amir Zarrinkafsh <3339418+nightah@users.noreply.github.com>
@nightah
nightah force-pushed the fix/exclude-parked-items-from-display-spread-gate branch from 8673dd7 to 430ab7c Compare August 10, 2026 23:36
@sonarqubecloud

Copy link
Copy Markdown

@diazdesandi
diazdesandi merged commit b02df3a into development Aug 10, 2026
11 checks passed
@diazdesandi
diazdesandi deleted the fix/exclude-parked-items-from-display-spread-gate branch August 10, 2026 23:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix PR that fixes a defect (issue reports use bug) layout Saved layouts, LayoutBar, reorder, spacing menubar Hide/show, sections, control items, backends, capacity test Test additions or updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants