Skip to content

fix(menubar): restore launch layout and stop move/storm loops - #889

Open
diazdesandi wants to merge 90 commits into
developmentfrom
fix/menubar-reliability
Open

fix(menubar): restore launch layout and stop move/storm loops#889
diazdesandi wants to merge 90 commits into
developmentfrom
fix/menubar-reliability

Conversation

@diazdesandi

@diazdesandi diazdesandi commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

Menu bar layout reliability: cold-start restore that actually runs, early apply for already-resolved identities, and hard bounds on automatic re-applies so a failed or parked-divider move cannot storm the cursor or rewrite the saved order. The same branch removes the full-sort rearrange, fixes divider seeding/pairing for show-hide (including collapsed sections), and adds field-repair tools (--reset-layout, clearer move-refusal diagnostics, profile layout preview).

Scope: One reliability track across launch restore, move planning, control-item pairing, and the related layout/settings surfaces that the field logs required. Not a drive-by refactor of unrelated areas.

We are on the process of migrating from XCTest to Swift Test. If you are adding new tests, please use Swift Test.
External contributors: before opening a PR for a bug fix or new feature, please make sure there's a corresponding issue in the issue tracker. PRs that fix or change things that haven't been reported/agreed on may be closed without review.

Linked issue (required)

PR Metadata fails without a Closes: line in this exact form (keep it on its own line):

Closes: N/A

Replace N/A with #<issue_number> (e.g. Closes: #123) when this PR fixes/implements a specific issue.

Closes: #885
Closes: #881
Closes: #900
Closes: #815
Closes: #890
Closes: #895
Closes: #899
Closes: #794
Closes: #886
Closes: #897
Closes: #248
Closes: #903
Closes: #751
Closes: #769
Closes: #904
Closes: #905
Closes: #887

Related / needs field verification: #788, #634, #791

PR Type

Describe what this change does (not the linked issue’s request kind). Bug reports use the Bug Issue type; bug fixes use Fix on PRs.

If you tick Feature or Refactor and touch more than ~20 files, please mention why this can’t be split.

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

Larger than a single-issue fix because each storm fix exposed the next failure mode in the same launch/move pipeline (cooldown → early apply → adjacency → unfinished batch → parked divider → late-arrival flap). Splitting would leave half-fixed storm loops on development.

Area

Product surfaces (optional when the change is not about the app UI). Path-based labeling also applies.

  • Use PR Type for what changed (CI/CD, Documentation, Other / chore, etc.).

  • Use ops for where when it is repo operations: CI, release, GitHub hygiene, scripts, lint/sonar config — not a product surface.

  • 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?

Cold start / settling (#881, #900)

  • Launch restore actually runs: the saved-layout / move cooldowns that launch itself stamped are bypassed for that path, including the early-apply path after relocating the Thaw icon.
  • Early apply moves already-resolved identities during settling instead of waiting for every sourcePID.
  • The Thaw icon relocates immediately when macOS parks it left of the hidden divider.
  • Move success is checked by adjacency in one snapshot, not exact coordinate equality against a target that reflows mid-drag. Stale destinations abort instead of burning retries.
  • An unfinished bulk apply no longer writes its half-done order into savedSectionOrder.
  • Automatic re-applies are bounded: one automatic retry after an unfinished batch, then a 60 s cooldown; batches abandon after three consecutive failures; notch-overflow ejections go through the failure ledger. The late-arrival profile re-sort uses the same unfinished-batch gate.
  • Parked (off-screen) items are excluded from the H_ctrl drag anchor; a parked H_ctrl divider skips the boundary move and records ledger backoff so the move cannot storm ([Bug] Mouse disappearing and also moving to top right of display #881, [Bug] RC2.1 destroys my cursor moves #899).
  • Late-arrival detection ignores unresolved identities so sourcePID flaps do not look like a bar full of new items.

Order at any time (#885, #881)

  • The full-sort planner that reordered the entire bar is gone. Bulk apply keeps the per-item / control-item path.
  • Failed move attempts no longer starve the operation timeout.
  • Diagnostics log a section-order digest (not just counts).
  • Display-sized overlay windows (e.g. Droppy drag-catchers) are never treated as an open menu; the completed-drop bypass of the menu probe was tried and reverted (Control Center interactions still need the wait).

Show / hide and control items (#890, #895, #899, #897)

Profiles / repair (#904, #887, #788, #815)

  • "Update All" marks that profile active when the capture matches running state.
  • Profiles prune Control-Center-hosted empty-title identifiers that can never match a live item (same idea as saved-order pruning).
  • Profile list rows can preview the saved layout and key behavior settings.
  • CLI: Thaw --reset-layout clears persisted order / pinning / relocation bookkeeping and re-seeds divider positions without starting the app (for repairing a wrecked bar before launch).

Also on this branch

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:

  • LayoutStormReplayTests
  • MoveOperationTimeoutTests / SectionOrderDigestTests
  • ControlItemDefaultsSeedingTests
  • StaleDestinationGateTests / UnfinishedMoveBatchGateTests / EarlySavedLayoutRestrictionTests / AutomaticBulkApplyGateTests
  • ParkedDividerTests
  • UnresolvedPlaceholderAliasTests
  • ThawBarSectionRoutingTests
  • Profile prune / late-arrival identifier filter suites added on this branch

Known limitations / follow-ups

Other information

~50 commits on fix/menubar-reliability. Started as full-sort removal + timeout hardening; grew through launch restore, parked-divider / unfinished-batch storm bounds, control-item pairing/seeding, capture topology, profile prune/preview, and --reset-layout.

Commit groups (headlines):

  • Planner / storms: delete full-sort; timeout budget; adjacency + stale destination; withhold partial saved order; bound automatic re-applies; gate late-arrival re-sort; ignore unresolved late arrivals; exclude parked H_ctrl anchors; skip/backoff parked H_ctrl divider
  • Launch: saved-layout cooldown bypass; early apply; Thaw icon during settling
  • Control items / sourcePID: divider seeding write-through; restore on hide/removal; stop preflight hidden reset; resolve disabled Thaw dividers; promote unresolved CC placeholders via AX
  • Diagnostics / repair: section-order digest; diagnose silent refusals; --reset-layout
  • Profiles: mark active after Update All; prune unmatchable identifiers; layout preview from list row
  • Capture / identity / settings features: display topology refresh; LyricsX stable identity; named layout display; badge-only drops; appearance borders; always-hidden Thaw Bar routing; profile spacing offset; search pointer warp
  • Cleanup: retire gesture/AX experimental toggles; AlphaChannelView; swift-subprocess 1.0.0

Signed-off-by: René Jiménez <diazdesandi@proton.me>
Cherry-pick of a1bb846 from feat/macos-27-experimental, so the 26/27
shared codebase does not diverge on image handling.

Introduce `AlphaChannelView` to encapsulate alpha channel access, buffer
validation, and pixel/row transparency checks. This centralizes bounds
validation, preventing potential out-of-bounds reads during image
processing, and simplifies the `TransparencyContext` and
`isTransparent(fastPath:)` implementations.

Signed-off-by: René Jiménez <diazdesandi@proton.me>
Signed-off-by: René Jiménez <diazdesandi@proton.me>
Signed-off-by: René Jiménez <diazdesandi@proton.me>
…ult off)

Signed-off-by: René Jiménez <diazdesandi@proton.me>
Signed-off-by: René Jiménez <diazdesandi@proton.me>
@github-actions github-actions Bot added enhancement Iteration, improvement, or optimization of an existing feature fix PR that fixes a defect (issue reports use bug) layout Saved layouts, LayoutBar, reorder, spacing menubar Hide/show, sections, control items, backends, capacity profiles Profiles and layout snapshots refactor Code restructuring without behavior change settings Settings UI not covered by a more specific area test Test additions or updates labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Labels: feature, enhancement, test, refactor, fix, menubar, icebar, layout, appearance, settings, onboarding, permissions, profiles, updates, ops

Warning

This PR looks large (130 files, 17945 lines changed). Consider splitting if possible (see CONTRIBUTING: aim ≤20 files / ≤500 LOC).

cc @diazdesandi

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR refactors menu-bar layout restoration and move execution, adds saved-order safeguards, introduces Thaw Bar and cursor settings, separates appearance borders, hardens capture utilities, and adds regression coverage.

Changes

Layout reliability

Layer / File(s) Summary
Layout planning, move execution, and persistence
Thaw/MenuBar/MenuBarItems/LayoutSolver.swift, Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift
LCS-based layout application replaces the removed full-sort path. Move outcomes, stale destinations, adaptive timeouts, failure backoff, resolved-only startup applies, order digests, and unfinished-batch gates are added.
Identity handling and layout validation
Thaw/MenuBar/MenuBarItems/MenuBarItemTag.swift, Thaw/MenuBar/MenuBarItems/MenuBarItem.swift, Thaw/MenuBar/LayoutBar/LayoutBarItemView.swift, ThawTests/MenuBar/Layout/*, ThawTests/MenuBar/Items/*
Canonical identifiers support volatile LyricsX titles and instance indexes. Saved sections remove duplicate identities. Movement refusal diagnostics distinguish immovable item types.
Layout editor and divider state
Thaw/MenuBar/LayoutBar/LayoutBarContainer.swift, Thaw/MenuBar/ControlItem/ControlItem.swift, ThawTests/MenuBar/Layout/LayoutBarContainerTests.swift, ThawTests/MenuBar/ControlItem/ControlItemDefaultsSeedingTests.swift
Badge-only insertion and divider preferred-position writes are supported and tested.

Settings, routing, and appearance

Layer / File(s) Summary
Thaw Bar and cursor settings
Thaw/Settings/Models/*, Thaw/Settings/SettingsPanes/*, Thaw/Utilities/SettingsURIHandler.swift, Thaw/MenuBar/MenuBarSection*, Thaw/MenuBar/Search/MenuBarSearchPanel.swift
Adds useThawBarForAlwaysHidden and moveCursorToRevealedItem across defaults, profiles, search, Settings URI handling, display routing, and cursor placement.
Appearance borders and display synchronization
Thaw/MenuBar/Appearance/*, Thaw/Settings/Models/DisplaySettingsManager.swift, Thaw/Settings/Models/ProfileManager.swift
Splits menu-bar and Thaw Bar borders. Legacy Codable data remains supported. Active display spacing is synchronized during setup and profile application.
Settings and UI validation
ThawTests/Settings/*, ThawTests/MenuBar/Appearance/*, ThawTests/MenuBar/Section/*
Adds coverage for settings persistence, URI scopes, profile defaults, border migration, spacing fallback, cursor positioning, and Thaw Bar routing.

Capture and utility hardening

Layer / File(s) Summary
Window capture and transparency scanning
Shared/Bridging/Bridging.swift, Thaw/Utilities/Extensions.swift
Window resolution uses one consistent snapshot with one forced refresh retry. Alpha scanning uses validated pixel-buffer geometry through AlphaChannelView.
Subprocess and source matching updates
Thaw/MenuBar/Spacing/*, Thaw/Utilities/HookRunner.swift, MenuBarItemService/SourcePIDCache.swift, Thaw.xcodeproj/project.pbxproj
Subprocess executable invocation and output handling are updated. Thaw-owned accessibility children remain eligible for source matching. The Swift package requirement is raised to version 1.0.0.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MenuBarItemManager
  participant LayoutSolver
  participant MenuBarItem
  participant Accessibility
  participant SavedOrder
  MenuBarItemManager->>SavedOrder: load and prune saved section order
  MenuBarItemManager->>LayoutSolver: build LCS move sequence
  LayoutSolver->>MenuBarItem: resolve canonical item identity
  MenuBarItemManager->>Accessibility: execute and verify move
  Accessibility-->>MenuBarItemManager: landing or displacement outcome
  MenuBarItemManager->>SavedOrder: persist only after move-batch gates pass
Loading

Possibly related PRs

  • thaw-app/Thaw#331: Introduced profile layout application and persistence paths updated by this PR.
  • thaw-app/Thaw#604: Earlier LayoutSolver and layout-reconciliation refactoring that this PR extends.
  • thaw-app/Thaw#897: Added the related badge-only layout drop behavior.

Suggested labels: performance

Suggested reviewers: stonerl, alvst

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Most linked objectives are implemented, but #794, #886, and #895 are not clearly satisfied by the summarized changes. Add evidence for workspace-wide appearance, the Notch placeholder, and stable strategy-4 divider pairing, or remove those issues from the PR.
Out of Scope Changes check ⚠️ Warning The PR includes changes not clearly tied to linked objectives, including AlphaChannelView, subprocess and hook updates, spacing executable changes, and capture-topology work. Split unrelated utility, dependency, spacing, and capture changes into separate PRs, or provide explicit linked requirements and rationale.
Docstring Coverage ⚠️ Warning Docstring coverage is 74.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary changes: restoring launch layout and preventing repeated move or layout-storm loops.
Description check ✅ Passed The description is comprehensive and follows the template, with linked issues, scope, behavior, tests, limitations, and checklist details.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/menubar-reliability
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/menubar-reliability

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

🤖 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 7907-7911: Renumber the sub-phase comments around the control-item
movement logic so their labels match execution order: the block currently marked
“Sub-phase 0” at the later location executes first and should be “Sub-phase 1,”
while the earlier block currently marked “Sub-phase 1” should be “Sub-phase 0.”
- Around line 4484-4494: The landing check in postMoveEvents duplicates move()’s
verification and performs an unnecessary Window Server query while
eventSemaphore is held. Remove the itemHasCorrectPosition call from
postMoveEvents, return or otherwise propagate the move outcome to move(), and
use move()’s single position check to supply the landed versus
displacedWithoutLanding result to nextMoveOperationTimeout.
- Line 8069: Recompute the section classification after the H_ctrl move in the
surrounding MenuBarItemManager flow, updating currentVisibleSet,
currentHiddenSet, and currentAHSet from the post-move sectionByWindowID state.
Recalculate crossSectionMoves and totalSectionMismatch from this fresh
classification, then use those values in the always-hidden gate guarding the
ahCtrlUID block so per-item fallback runs only when post-move mismatches remain.
- Around line 4346-4350: Update the notch override branch around
notchMouseDownPoint so it only applies when useGestureGeometry is false; in
gesture mode, preserve pressPoint and the matching postGestureDragSteps
interpolation path. Keep the existing legacy offscreen notch behavior unchanged.

In `@Thaw/Utilities/Defaults.swift`:
- Around line 278-283: Remove all remaining references to
useLCSSortingOnNotchedDisplays from docs/URI_SCHEMES.md and the ThawCtl
ContentView export list, ensuring URI inputs and CLI exports no longer advertise
the deleted setting. If the URI handler still accepts the raw legacy key, add a
one-shot migration mapping it before removal; otherwise remove its handling
consistently.

In `@ThawTests/Fixtures/LayoutStormLog.swift`:
- Around line 92-97: Change sectionMap from a computed static var into a stored
static let initialized once from currentVisible and currentHidden, preserving
the existing visible/hidden mapping.

In `@ThawTests/MenuBar/Items/SectionOrderDigestTests.swift`:
- Line 100: Update the `after` assignment in the section-order digest test to
materialize `before.reversed()` as an `Array` before concatenating the
additional item, preserving the existing reversed ordering and appended value.
🪄 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: 3cfc950d-e2af-4a89-970a-b5649992f4c5

📥 Commits

Reviewing files that changed from the base of the PR and between 54345d4 and 69ccf4b.

📒 Files selected for processing (22)
  • Thaw/MenuBar/MenuBarItems/LayoutSolver.swift
  • Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift
  • Thaw/Resources/Localizable.xcstrings
  • Thaw/Settings/Models/AdvancedSettings.swift
  • Thaw/Settings/Models/Profile.swift
  • Thaw/Settings/Search/SearchIndex.swift
  • Thaw/Settings/SettingsPanes/AdvancedSettingsPane.swift
  • Thaw/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift
  • Thaw/Utilities/Defaults.swift
  • Thaw/Utilities/Extensions.swift
  • Thaw/Utilities/SettingsURIHandler.swift
  • ThawTests/Fixtures/LayoutStormLog.swift
  • ThawTests/MenuBar/Items/MoveOperationTimeoutTests.swift
  • ThawTests/MenuBar/Items/SectionOrderDigestTests.swift
  • ThawTests/MenuBar/Layout/LayoutStormReplayTests.swift
  • ThawTests/MenuBar/Layout/PlanFullSortSequenceTests.swift
  • ThawTests/Settings/Models/AdvancedSettingsSnapshotTests.swift
  • ThawTests/Settings/Models/AdvancedSettingsTests.swift
  • ThawTests/Settings/Models/ProfileDecodingDefaultsTests.swift
  • ThawTests/Settings/Search/SearchIndexTests.swift
  • ThawTests/Settings/URI/SettingsURIHandlerCoverageTests.swift
  • ThawTests/Support/ProfileTestFixtures.swift
💤 Files with no reviewable changes (12)
  • ThawTests/Settings/URI/SettingsURIHandlerCoverageTests.swift
  • Thaw/Utilities/SettingsURIHandler.swift
  • ThawTests/MenuBar/Layout/PlanFullSortSequenceTests.swift
  • ThawTests/Settings/Search/SearchIndexTests.swift
  • Thaw/MenuBar/MenuBarItems/LayoutSolver.swift
  • ThawTests/Support/ProfileTestFixtures.swift
  • ThawTests/Settings/Models/ProfileDecodingDefaultsTests.swift
  • ThawTests/Settings/Models/AdvancedSettingsTests.swift
  • Thaw/Resources/Localizable.xcstrings
  • Thaw/Settings/Models/Profile.swift
  • ThawTests/Settings/Models/AdvancedSettingsSnapshotTests.swift
  • Thaw/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift

Comment thread Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift Outdated
Comment thread Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift Outdated
Comment thread Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift Outdated
Comment thread Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift Outdated
Comment thread Thaw/Utilities/Defaults.swift Outdated
Comment thread ThawTests/Fixtures/LayoutStormLog.swift Outdated
Comment thread ThawTests/MenuBar/Items/SectionOrderDigestTests.swift Outdated
diazdesandi and others added 8 commits August 5, 2026 11:05
LyricsX titles its menu bar item with the lyric line currently on screen, and identity is namespace:title. Every song change — every line — minted a fresh uniqueIdentifier, so the item read as a brand-new arrival and was filed under the user's new-items section. Dragging the lyrics into visible could not stick, because the item that was moved stopped existing at the next line.

The volatile-title machinery already existed for iStat Menus, which has the same problem in a different shape, but it did not cover this. Two reasons:

  1. The owner allowlist held exactly one bundle identifier.
  2. canonicalMetricTitle collapses digits, and a lyric has none. Adding LyricsX to the existing list would have changed nothing —"I walked through the door" canonicalizes to itself.

So this adds a second title shape rather than a second entry. A metric title has a stable skeleton worth keeping ("CPU #" and "Network #" still tell two iStat items apart); a lyric has none, every character is the volatile part.
For such an owner the whole title collapses to a constant, leaving the instance index as the only distinguishing part. That is sufficient while the owner contributes one item, and is why this stays an allowlist rather than becoming a heuristic.

Signed-off-by: René Jiménez <diazdesandi@proton.me>
Signed-off-by: René Jiménez <diazdesandi@proton.me>
Signed-off-by: René Jiménez <diazdesandi@proton.me>
…ows (#794)

Signed-off-by: René Jiménez <diazdesandi@proton.me>
Signed-off-by: René Jiménez <diazdesandi@proton.me>
…890)

Signed-off-by: René Jiménez <diazdesandi@proton.me>
Signed-off-by: René Jiménez <diazdesandi@proton.me>
Signed-off-by: VailElla <63608800+VailElla@users.noreply.github.com>

@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: 3

Caution

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

⚠️ Outside diff range comments (1)
Thaw/MenuBar/MenuBarItems/MenuBarItemTag.swift (1)

258-273: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use canonical identity during saved-position lookup.

This function makes two LyricsX titles one persistent identity. LayoutSolver.savedPositionByBaseID still compares raw namespace:title values. During planUnmanagedPlacement, a current lyric misses its saved lyric entry and falls through to newItemDefault. A profile apply can then move LyricsX into the configured new-items section.

After raw exact matching fails, compare canonical identifiers before the base-ID fallback. Preserve the instance index. Add a profile-placement regression test.

🤖 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 `@Thaw/MenuBar/MenuBarItems/MenuBarItemTag.swift` around lines 258 - 273,
Update LayoutSolver.savedPositionByBaseID and its planUnmanagedPlacement lookup
so that, after raw namespace:title matching fails, it compares
canonicalPersistentIdentifier values before applying the base-ID fallback,
preserving any instance index. Add a profile-placement regression test verifying
a canonicalized LyricsX identity reuses its saved position instead of
newItemDefault.
🤖 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 `@Shared/Bridging/Bridging.swift`:
- Around line 774-781: Update the recovery branch in captureWindowsImageSCK so a
successful shareableContentIncludingOffscreen refresh replaces the cached
snapshot, re-resolves the requested windows, and recomputes unionBounds from
fresh before creating the SCContentFilter and source rect. Re-evaluate
hostDisplay(in:) against the refreshed windows, and retain a caller-provided
screenBounds only when its coordinate contract remains valid for the refreshed
topology.

In `@Thaw/MenuBar/MenuBarItems/LayoutSolver.swift`:
- Around line 1209-1223: Update the saved-order repair flow around the
section-wise mapValues filter so canonical identifiers are deduplicated across
the complete saved order rather than resetting seenCanonical per section.
Establish deterministic section precedence, retain each canonical identifier
only in its preferred section before planLeftmostMove builds its lookup, and add
a regression test covering the same canonical LyricsX or iStat identifier
appearing in different sections.

In `@ThawTests/MenuBar/ControlItem/ControlItemDefaultsSeedingTests.swift`:
- Around line 21-76: Update ControlItemDefaultsSeedingTests to isolate and
restore shared Defaults.store state: mark the suite serialized, and ensure each
test that mutates the hidden or visible preferred position preserves its
original value and restores it with defer (or use a scratch defaults store).
Keep the existing assertions and seeding behavior unchanged.

---

Outside diff comments:
In `@Thaw/MenuBar/MenuBarItems/MenuBarItemTag.swift`:
- Around line 258-273: Update LayoutSolver.savedPositionByBaseID and its
planUnmanagedPlacement lookup so that, after raw namespace:title matching fails,
it compares canonicalPersistentIdentifier values before applying the base-ID
fallback, preserving any instance index. Add a profile-placement regression test
verifying a canonicalized LyricsX identity reuses its saved position instead of
newItemDefault.
🪄 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: 31aea040-96d0-4200-befa-f9a6563ee369

📥 Commits

Reviewing files that changed from the base of the PR and between 69ccf4b and 5ff1317.

⛔ Files ignored due to path filters (1)
  • Thaw.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved is excluded by !**/Package.resolved
📒 Files selected for processing (14)
  • Shared/Bridging/Bridging.swift
  • Thaw.xcodeproj/project.pbxproj
  • Thaw/MenuBar/ControlItem/ControlItem.swift
  • Thaw/MenuBar/MenuBarItems/LayoutSolver.swift
  • Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift
  • Thaw/MenuBar/MenuBarItems/MenuBarItemTag.swift
  • Thaw/MenuBar/Spacing/MenuBarItemSpacingManager.swift
  • Thaw/Resources/Info.plist
  • Thaw/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift
  • Thaw/Utilities/HookRunner.swift
  • ThawTests/MenuBar/ControlItem/ControlItemDefaultsSeedingTests.swift
  • ThawTests/MenuBar/Items/MenuBarItemTagCanonicalizationTests.swift
  • ThawTests/MenuBar/Layout/PrunedSectionOrderTests.swift
  • ThawTests/Utilities/ShareableContentCacheTests.swift

Comment thread Shared/Bridging/Bridging.swift Outdated
Comment thread Thaw/MenuBar/MenuBarItems/LayoutSolver.swift Outdated
Comment thread ThawTests/MenuBar/ControlItem/ControlItemDefaultsSeedingTests.swift Outdated

@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.

Caution

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

⚠️ Outside diff range comments (2)
ThawTests/MenuBar/Items/SectionOrderDigestTests.swift (1)

27-27: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Materialize the reversed collection before calling orderDigest.

MenuBarItemManager.orderDigest accepts [String], but items.reversed() has type ReversedCollection<[String]>. Swift does not implicitly convert this view to an array, so the test does not compile.

Proposed fix
-                != MenuBarItemManager.orderDigest(items.reversed())
+                != MenuBarItemManager.orderDigest(Array(items.reversed()))

Verify the declaration and call with:

#!/bin/bash
set -euo pipefail

rg -n -C 3 \
  'static nonisolated func orderDigest|orderDigest\(items\.reversed\(\)\)' \
  Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift \
  ThawTests/MenuBar/Items/SectionOrderDigestTests.swift
🤖 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 `@ThawTests/MenuBar/Items/SectionOrderDigestTests.swift` at line 27,
Materialize the reversed collection before passing it to
MenuBarItemManager.orderDigest in the SectionOrderDigestTests assertion,
converting items.reversed() to the required [String] array while preserving the
existing comparison.
Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift (1)

4476-4484: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Send gesture drag events before waiting for displacement.

When useGestureGeometry is true, Line 4471 waits for an origin change before Line 4477 sends the first mouseDragged event. A normal drag owner moves only after mouseDragged. The wait can time out, and the fallback sends mouseUp without sending any drag event.

Send postGestureDragSteps immediately after mouseDown. Then wait for the origin change. Keep the existing wait order for legacy press-at-destination moves.

Proposed fix
             try await scrombleEvent(
                 mouseDown,
                 item: item,
                 timeout: timeout
             )
+            if useGestureGeometry {
+                try await postGestureDragSteps(
+                    item: item,
+                    source: source,
+                    from: pressPoint,
+                    to: targetPoints.end,
+                    warpAlongPath: warpIsOnScreen
+                )
+            }
             itemOrigin = try await waitForMoveEventResponse(
                 from: item,
                 initialOrigin: itemOrigin,
                 timeout: timeout
             )
-            if useGestureGeometry {
-                try await postGestureDragSteps(
-                    item: item,
-                    source: source,
-                    from: pressPoint,
-                    to: targetPoints.end,
-                    warpAlongPath: warpIsOnScreen
-                )
-            }
🤖 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 `@Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift` around lines 4476 - 4484,
Reorder the useGestureGeometry drag flow so postGestureDragSteps is invoked
immediately after mouseDown, before waiting for the origin change. Then retain
the displacement wait and existing fallback behavior, while preserving the
current wait-before-drag order for legacy press-at-destination moves.
🤖 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.

Outside diff comments:
In `@Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift`:
- Around line 4476-4484: Reorder the useGestureGeometry drag flow so
postGestureDragSteps is invoked immediately after mouseDown, before waiting for
the origin change. Then retain the displacement wait and existing fallback
behavior, while preserving the current wait-before-drag order for legacy
press-at-destination moves.

In `@ThawTests/MenuBar/Items/SectionOrderDigestTests.swift`:
- Line 27: Materialize the reversed collection before passing it to
MenuBarItemManager.orderDigest in the SectionOrderDigestTests assertion,
converting items.reversed() to the required [String] array while preserving the
existing comparison.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 356ffa20-40d2-4269-880b-885c8f169c99

📥 Commits

Reviewing files that changed from the base of the PR and between 5ff1317 and fa965b6.

📒 Files selected for processing (5)
  • Thaw/MenuBar/MenuBarItems/MenuBarItemManager.swift
  • ThawCtl/Sources/ThawCtl/ContentView.swift
  • ThawTests/Fixtures/LayoutStormLog.swift
  • ThawTests/MenuBar/Items/SectionOrderDigestTests.swift
  • docs/URI_SCHEMES.md
💤 Files with no reviewable changes (1)
  • docs/URI_SCHEMES.md

Signed-off-by: René Jiménez <diazdesandi@proton.me>
REVERT BEFORE MERGE. This exists so a build handed to the reporters on
#854, #905, #923, #924 and #927 exercises the fixes without asking them
to run `defaults write` first — three testers, three chances to forget,
and a "still broken" report that means nothing.

    postMoveEventsToWindowOwner   false → true
    bulkApplyIdleThresholdMs      0     → 300
    enforceConcealedSectionOrder  true  → false

automaticArrangementEnabled is deliberately left alone. Its enabled state
*stops* automatic arranging, so turning it on would disable the
late-arrival re-sort and the saved-layout restore — the behaviour the
testers need to exercise — and mask every other fix while reading as
"Thaw stopped restoring my layout".

Each flag keeps its shipping default documented on the Defaults.Key case;
only DefaultValue moves. Reverting this commit restores the shipping
posture exactly.

Signed-off-by: Rene <diazdesandi@proton.me>
@github-actions github-actions Bot added onboarding First-run / tour permissions Accessibility, screen recording, authorization flow labels Aug 10, 2026
Bumps the swift group with 3 updates: [github.com/sparkle-project/sparkle](https://github.com/sparkle-project/Sparkle), [github.com/swiftlang/swift-subprocess](https://github.com/swiftlang/swift-subprocess) and [github.com/apple/swift-system](https://github.com/apple/swift-system).


Updates `github.com/sparkle-project/sparkle` from 2.9.4 to 2.9.5
- [Release notes](https://github.com/sparkle-project/Sparkle/releases)
- [Changelog](https://github.com/sparkle-project/Sparkle/blob/2.x/CHANGELOG)
- [Commits](sparkle-project/Sparkle@2.9.4...79bc9e8)

Updates `github.com/swiftlang/swift-subprocess` from 0.5.0 to 1.0.0
- [Release notes](https://github.com/swiftlang/swift-subprocess/releases)
- [Commits](swiftlang/swift-subprocess@0.5.0...b3937ab)

Updates `github.com/apple/swift-system` from 1.7.5 to 1.8.0
- [Release notes](https://github.com/apple/swift-system/releases)
- [Commits](apple/swift-system@1.7.5...704705c)

---
updated-dependencies:
- dependency-name: github.com/sparkle-project/sparkle
  dependency-version: 2.9.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swift
- dependency-name: github.com/swiftlang/swift-subprocess
  dependency-version: 1.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: swift
- dependency-name: github.com/apple/swift-system
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: swift
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
A saved position is an index into a per-section array, so an identifier
that no longer names anything on the bar does not merely sit there
harmlessly: it pushes every entry behind it one slot right. An item whose
app is reinstalled, renamed, or re-identified comes back one place off
from where the user left it, and does so on every apply from then on.

StaleIdentifierLedger counts, per canonical identifier, how many
consecutive applies planned for it without a live item answering. Past a
threshold the entry is retired and dropped before the section-order
lookup rather than after, so the indices behind it close up.

Two guards keep the count honest. A pass whose unmatched fraction is
above a ceiling is discarded rather than recorded, because that is a
degraded reading of the bar and not evidence about any one item. And the
counts carry the build they were earned under, so improving identity
resolution does not leave items retired on the strength of misses the
old behaviour caused.

Sampling happens at the one point in the apply that holds both halves at
once, past every early return, so an apply that never looked at the bar
cannot count as evidence that an item is gone.

Signed-off-by: René Jiménez <diazdesandi@proton.me>
Two classes of persisted entry can never name a live item again, and
both were reaching the planner on every apply.

A source-PID resolution that hands a foreign window Thaw's own PID
writes the item under our namespace: #927's reporter carried
`com.stonerl.Thaw:WiFi` for an item Control Center owns. Only the
control items and the spacers belong there, so anything else is
dropped. The order of that check matters more than the drop does.
Building the set of titles claimed by a real owner now skips these
entries, because counting one as a real owner is what made the
provisional-duplicate rule delete the *genuine* Control Center twin —
which is why that reporter had lost `com.apple.controlcenter:WiFi` and
kept the poisoned copy, and why the live WiFi and Bluetooth items were
planned as unmanaged on all fourteen passes in their log.

WindowServer's `System Status Item Clone` windows are the second class.
The cache already refuses them, but layouts captured before that gate
existed hold one entry per clone; the same reporter carried six under a
single owner.

Pruning alone does not reach the profiles. The saved section order has
always been repaired at load and written straight back, so a widened
rule takes effect on the next launch, but profiles were only ever pruned
on the way out through `resolvedItemOrder`. The damage stayed in the
file and seeded the in-memory saved order again at every startup, and a
profile is also the one copy a user can re-apply by hand.
`repairPersistedLayouts` rewrites them, filtering `itemSectionMap` by
the same verdict because `resolvedItemSectionMap` hands that map back
verbatim when present. It runs once per build rather than once ever, so
a later build that recognizes more can revisit files an earlier one
called clean, and it leaves anything it cannot decode untouched.

Signed-off-by: René Jiménez <diazdesandi@proton.me>
…t gate

A user who drags every hidden item into visible leaves the two dividers
correctly adjacent, because nothing is between them. hiddenSectionHasRoom
read that as the #795 collapse and refused to persist — and the saved
order it refused to write is the same order whose non-zero hidden count
made it refuse. The gate's own effect preserved its trigger, so Thaw went
permanently read-only on that bar: no save, and no apply either, since
applySavedLayout consults the same answer. Reinstalling did not help,
because the frozen order is on disk. #924's log shows the warning firing
from the tick hidden reached zero through every pass after it.

The live count cannot break the tie on its own. A collapse reads as zero
live hidden items too — that misclassification is the fault — so
releasing on an empty live section alone would hand #868 straight back.
What separates the two is where the items went. A collapse leaves them
parked thousands of points off every display while the reading calls
them visible, a position no genuinely visible item can hold; an emptied
section leaves every visible item on the bar. So an empty live section
is trusted only when nothing out there is parked.

Membership is decided by geometry rather than by asking the cache, for
two reasons. The apply path is handed a bar and runs before any recache,
so it has no cache to consult. And restricting the parked check to items
at or right of the hidden divider keeps a closed always-hidden section
full of legitimately parked items from reading as a fault, which is the
state the reporter spent most of their session in.

Off-display is the tell, not left-of-divider: in #868's geometry the
misread items sit just right of the collapsed divider while being
nowhere near a screen.

Signed-off-by: René Jiménez <diazdesandi@proton.me>
kCGWindowName degrades bar-wide. In #881's 12:38 log the live hidden
section came back as

    com.apphousekitchen.aldente-pro:com.apphousekitchen.aldente-pro
    com.rogueamoeba.soundsource:com.rogueamoeba.soundsource
    leits.MeetingBar:leits.MeetingBar
    com.stonerl.Thaw:com.stonerl.Thaw:1
    …and seven more

two minutes after the same items had read normally, and #927's reporter
carried 23 of the same shape on an unrelated machine with a disjoint set
of apps.

Caching that reading is what makes the damage self-sustaining. Every item
looks new, so the whole bar is persisted under a second set of
identifiers, and every later flip between the two spellings presents a
bar's worth of late arrivals to lateArrivingProfileIdentifiers, which
schedules a re-sort, which posts moves and captures the cursor. #881's
reporter rode that loop to a streak of nine consecutive bulk applies
ending with unenacted moves.

Treat it the way an empty reading is already treated — a failed
observation rather than the bar changing — and hold the last known good
cache. Two signals, either sufficient:

  - An item in our own namespace titled with our own bundle identifier.
    We title our items Thaw.ControlItem.*, so this can only be a degraded
    read. It is also the signal with consequences of its own, since
    MenuBarItem recognizes control items by that prefix and a degraded
    reading therefore arrives with the dividers missing — which is what
    the same logs report.

  - Half or more of the reading self-titled, for bars of at least four.
    One app whose window really is named after its own bundle identifier
    reaches half of a two- or three-item bar alone. It cannot reach half
    of four.

Only once there is a cache to fall back to; a first launch has nothing
better and behaves as before. A partial degradation that trips neither
signal still reaches the cache, and prunedSectionOrder clears those
entries at the next load rather than letting them accumulate.

Signed-off-by: René Jiménez <diazdesandi@proton.me>
The live guard added in the previous commit keeps new self-titled
identities out of the cache, but every layout written by an earlier build
still holds them: 21 in #881's saved order, 23 in #927's profile. Those
entries match nothing on the bar, so each apply plans moves that cannot
be enacted — #927's reporter logged 53 fallbacks over 52 minutes.

Drop them in prunedSectionOrder, which repairPersistedLayouts() already
runs over the profile JSON and MenuBarItemManager over the saved order.

Both halves are canonicalized before comparing, because pruning runs
after canonicalizedSectionOrder, which rewrites a nested helper's
namespace and carries its title through verbatim. A degraded Little
Snitch entry arrives as

    at.obdev.littlesnitch:at.obdev.littlesnitch.agent

whose halves are no longer literally equal.

Self-titled entries are excluded from titlesWithRealOwner for the same
reason misattributed ones already are. A degraded

    com.microsoft.OneDrive:com.microsoft.OneDrive

is not evidence that anything owns that title, and letting it count would
license the #788 rule to delete the genuine Control Center twin beside
it — the failure that cost #927's reporter their real WiFi item.

Only exact equality counts, so a title that continues past the bundle
identifier keeps its entry. An app whose window name genuinely is its own
bundle identifier does lose its saved entry and becomes unmanaged, which
is the one user-visible cost of this rule.

Signed-off-by: René Jiménez <diazdesandi@proton.me>
Showing a hidden item moves it into the visible section, so on the next
cache cycle currentLayoutDivergesFromSaved finds it sitting somewhere
other than where savedSectionOrder records it and reports divergence.
That is the second of applySavedLayout's two triggers, so a bulk apply is
dispatched to drag the item home — while its menu is open. The only thing
between that dispatch and the user's bar is isAnyMenuBarItemMenuOpen, and
#924's logs show the probe bailing at "no candidate menu windows on
screen" on 120 of 173 and 428 of 428 calls, with two successful menu
detections across the whole session. A false negative there is the menu
closing the instant it opens.

The item's position is not drift. Thaw put it there deliberately and the
rehide is what brings it back; this pass has no business racing it. So
the divergence rule now skips items with a live TemporarilyShownItemContext.
The exemption is keyed on tagIdentifier rather than the tag, because the
move that reveals the item can change its window ID and the two must
still match afterwards.

Scoped to the trigger, not the plan. applyProfileLayout still sees these
items when it computes the batch, which is deliberate: dropping them from
its input would hide them from partitionUnmanagedUIDs and let the planner
anchor neighbours against a phantom, the same hazard the isSystemClone
filter guards. Nor does this help when the apply is armed by
windowIDsChanged instead — an unrelated app quitting during an open menu
still reaches the probe. That is the general unreliability of the probe
rather than this regression.

The rule moves to a pure static so both of its exemptions can be tested.
The notch-overflow carve-out was already there but derived inline from
appState and NSScreen, which is why it had no coverage; it now arrives as
a set from the caller and keeps its existing behaviour, pinned by two
tests. The caller still decides that the feature is on and the display is
notched before passing anything.

Refs: #924

Signed-off-by: René Jiménez <diazdesandi@proton.me>
REVERT ALONGSIDE 99d174b.

99d174b flipped three defaults so testers exercise the fixes without
running defaults write first, but left the version alone. Every DMG built
off this branch therefore reports 2.0.0-rc.2.1, the same string as the
release that already shipped over Sparkle with those flags off. Neither
the reporter nor the log header can say which posture produced a given
report.

That ambiguity already cost a round. A reporter on run 31418776871 was
asked to turn postMoveEventsToWindowOwner off to bisect it; menus still
closed, and the flag turned out to also gate isMovableAddressingWindowOwner,
so the test both failed to isolate the change and stopped unresolved
Control Center items from being shown at all. Attribution has to come
from the build, not from asking.

2.0.0-rc.3+test.1 (50). DiagnosticLogger reads
CFBundleShortVersionString into every log header, so a log now identifies
its own posture. The build number moves too because CFBundleVersion is
what Sparkle compares, and 49 is taken by the real rc.2.1.

Signed-off-by: René Jiménez <diazdesandi@proton.me>
Signed-off-by: René Jiménez <diazdesandi@proton.me>
Activating an item from the search panel showed it, opened its menu, and
then tore both down two to three seconds later. The rehide check was
reaching a negative reading by two separate routes, and either one alone
is enough to lose the menu.

The first is PID breadth. An item's window and the menu that window opens
do not always belong to the same process; on macOS 26 Control Center
hosts the status item while the app draws the menu. Two sites — the
fallback interface capture and appHasVisiblePopup — matched candidates
against a single PID, and for an item whose sourcePID never resolved the
`?? ownerPID` collapse made that Control Center's, so the app's open menu
could never be found. ClickReactionVerifier already matched against both
PIDs, so the sites disagreed with each other. Carry the pair on the
context as interfacePIDs and match both everywhere.

The second is that "not found" was read as "closed". isShowingInterface
returned a Bool, so an observation that the menu is gone and an admission
that it was never identified were the same answer, and the rehide acted
on both. Split them into InterfaceState.showing/absent/unknown and spend
up to four further checks on unknown before rehiding anyway. An item that
lingers is a far cheaper failure than a menu that closes under the user,
and the bound keeps an unidentifiable interface from parking the item in
the visible section forever.

Extract windowIsOpenInterface as a pure static so the PID and window
level rules are testable. Its tests pin the level trade in both
directions: a menu-bar-sized status window is the item itself and must
not count, while floating and modal levels — which the previous liberal
"above normal" match accepted — are ordinary app windows that kept the
reading positive indefinitely.

Refs #924

Signed-off-by: René Jiménez <diazdesandi@proton.me>
#881's 08:41 log dragged the same six items 69 times in four minutes,
each move a synthetic Cmd-drag that takes the cursor. Two faults
compound: one supplies a trigger that never stops, the other supplies
work that never converges.

rebalanceNotchOverflowIfNeeded handed off to a profile apply at the top
of the method, before the cooldown and before the budget was ever
computed. Every cache tick therefore armed a full apply, and every apply
recaches, which runs the pass again. The bar in that log never once
overflowed — the pass returned before reaching computeNotchOverflowBudget
and not a single ejection was planned across 73 handoffs. Move the
handoff below the planner and gate it on the planner having found real
overflow, and stamp the rebalance timestamp on the way out so the
deferral is rate-limited like the ejection path. Reading the planner for
emptiness is sound under the pass's all-unmanaged inputs because the
tiers are a priority order over one budget: they decide which items are
chosen, not whether any are, which the two new cases pin from both
directions.

applyProfileLayout meanwhile proceeded with a nil always-hidden divider.
Without that boundary findSection collapses always-hidden into hidden, so
Phase 1 read all nine always-hidden items as misplaced and planned eight
cross-section moves. The moves landed — the mover finds the divider by
tag even when ControlItemPair could not — and changed nothing, so the
next pass planned the same set. The divider went unresolved in 552 of 578
cycles, and 69 of 75 Phase 1 passes ran on ahCtrlUID=nil. saveSectionOrder
already refuses to persist this reading (#849); refusing to move on it is
the same judgement one step earlier, on the same predicate.

Also drops a clickPID binding left unused when the interface lookup moved
to the context's PID set.

Refs #881

Signed-off-by: René Jiménez <diazdesandi@proton.me>
Activating an item from the search panel showed it, opened its menu, and
then took both down inside a second. f5c114f went after the same report
and moved it the wrong way: it was two to three seconds before.

That commit widened the interface capture to both of an item's PIDs
without widening what counts as an interface. The fallback took the first
new on-screen window owned by either one, and the pair includes the
window's owner — Control Center, for every item it hosts, which opens
short-lived windows of its own around a click. Latching onto one is worse
than latching onto nothing: a tracked window short-circuits
interfaceState, skipping both the grace period and the unknown budget, so
the moment it goes the reading is a confident absent and the rehide drags
the item home under the open menu. The same false negative f5c114f set
out to remove, arriving sooner and with more conviction.

A candidate now has to look like an interface — a menu-level window
first, then any window too tall to be a status item, which is how
interfaceState already recognizes the popovers and non-standard-level
menus that never reach pop-up level. Nothing qualifying tracks nothing,
leaving the reading unknown where it belongs. ClickReactionVerifier's
window goes through the same test: it answers whether the owner reacted
at all and settles for any new window of the owner's when no menu-level
one appeared, which is sound evidence of a reaction and a poor guess at
the menu.

The second fault is that none of the retry intervals were real. The KVO
publisher's default options include .initial, so re-subscribing replays
the current frontmost app, and runRehideTimer re-subscribes on every
call — including from the retry paths it schedules. Every "look again in
three seconds" was looking again in two hundred milliseconds, and the
four unknown checks documented as roughly twelve seconds of grace were
spending eight hundred. Drop the replay so only a real app switch fires,
and make the post-show timer a one-second poll rather than the fifteen-
second ceiling: with the replay gone nothing else re-arms a check, and
dismissing a menu with Escape changes no frontmost app, so the ceiling
would have parked the item out on the bar for fifteen seconds every time.

Refs #924

Signed-off-by: René Jiménez <diazdesandi@proton.me>
An item cached without a source process has a provisional identity: its
namespace falls back to the owner of its window — Control Center, for
everything it hosts — and its display name to "Menu Bar Item". The first
AX scan after login routinely lands there, because other apps'
accessibility trees are still warming up in the seconds it runs.

Nothing ever took it back. cacheItemsIfNeeded recaches on a change in the
set of window IDs, and a late-resolving item's window does not go
anywhere, so the bad reading survived the session and only a relaunch
cleared it. SourcePIDNegativeCachePolicy was built for exactly this case
— short deadlines on the first failures so a warmer scan can land — and
its own reasoning names the half it cannot fix from the service side:
"the app stops requesting once settled". This is the app not stopping.
When the window set is unchanged, ask the service about the items the
cache is holding without a source, and recache if any of them now
resolve.

Read from the cache rather than by differencing window IDs against the
resolved-PID map: those are the items actually on display under a
provisional identity, and the set can only shrink as they resolve, so a
probe cannot talk the cache into recaching what it just cached. Control
items are excluded for the reason enumeration excludes them from
resolution to begin with — their AX children are disabled dividers, so
the request is a guaranteed miss that can start a full scan of every
running app. The probe costs one round trip per tick while anything is
unresolved and nothing once everything has resolved; the TTL ladder is
what bounds how often a request behind it becomes a real scan.

The search panel's name memo needed the other half. It is keyed by tag
and cleared on close and on rename, but the whole point of a late
resolution is that the row is already on screen. Clear it when the item
cache changes, which is the only event that can change what a name
resolves to; keystrokes, the cost the memo exists to absorb, do not
reach it.

Signed-off-by: René Jiménez <diazdesandi@proton.me>
…t opened

Only third-party items, and not in rc.2.1: useAXClickDelivery shipped
there defaulted off behind a Settings toggle, and this branch turned it on
and removed the toggle. What it turned on reads a blocked accessibility
call as a failed one.

AXShowMenu on a status item opens the menu and then blocks. The menu runs
a modal tracking loop, the app cannot answer the accessibility message
until it closes, and the activator sets a 250 ms messaging timeout — so
the call throws on precisely the items where it worked. #924's 15:27:18
log has the whole thing in three hundred milliseconds: AXShowMenu goes
out, the on-screen window count climbs from 14 to 15 while it is blocked,
the call comes back actionFailed, and a synthetic click lands 24 ms later.
The menu the user asked for appeared and was gone inside the same second.

Both escalations from a thrown action are another activation of the same
item, and an item whose menu is open toggles it shut: AXPress closes what
AXShowMenu opened, and the fallback click closes it again. So an action
that throws is now checked before escalating past it — ClickReactionVerifier
already knows how to see an owner reacting, and after a call that blocked
for its whole timeout the answer needs no waiting, hence reactionSoFar.
The same check guards the caller's fallback, for the errors raised before
the activator gets to its own.

Apple's own items were unaffected because they answer the message.

The rule is extracted as performFirstEffectiveAction and pinned from both
sides: a threw-but-worked action must not escalate, and an inert one still
must. actionFailed now means the item was genuinely left alone, which is
what makes the fallback click safe.

Refs #924

Signed-off-by: René Jiménez <diazdesandi@proton.me>
Signed-off-by: René Jiménez <diazdesandi@proton.me>
Signed-off-by: René Jiménez <diazdesandi@proton.me>
@sonarqubecloud

Copy link
Copy Markdown

Signed-off-by: René Jiménez <diazdesandi@proton.me>
Signed-off-by: René Jiménez <diazdesandi@proton.me>
@github-actions github-actions Bot added the updates Sparkle / release channels / appcast label Aug 12, 2026
Signed-off-by: René Jiménez <diazdesandi@proton.me>
- Use Defaults.bool(forKey:) instead of manual cast in MenuBarItem
- Add [weak self] to smart-rehide Task in MenuBarManager
- Mark StaleIdentifierLedger.currentBuildVersion nonisolated
- Gate ±1 control-item drop bias to zero-width dividers only
- Add comment explaining CGDisplayBounds coordinate-space switch
- Add parked-divider recovery episode rearm/below-threshold/custom tests
- Add wide control-item destination gets no bias test
- Correct cooldown-bypass test docstring and document hard-cap invariant

Signed-off-by: René Jiménez <diazdesandi@proton.me>
- Rehide: use 30s cursor watchdog and isBulkApplyInProgress gate
  around rehideTemporarilyShownItems so the 1s default watchdog
  cannot force-show the cursor mid-batch and per-item hide/show
  oscillation is suppressed (#899).
- Ice Bar: hover-hide now uses rehideInterval instead of the 0.2s
  showOnHoverDelay when the Thaw Bar is the active presentation,
  so the panel stays open for the user-configured timeout instead
  of snapping shut before the user can click an icon.
- Focus rehide: add diagnostic comment in the else branch noting
  that autoRehide must be on for any strategy to fire.

Signed-off-by: René Jiménez <diazdesandi@proton.me>
The screen recording indicator appears as an unresolved
com.apple.controlcenter:Item-0 with a nil sourcePID. It was counted
as an unmanaged item in the notch overflow footprint, which made the
rebalance see 98pt of unmanaged space and attempt to eject 20 visible
items — all failing with cannotComplete because the recording item
can't be moved. Each cache tick re-triggered the loop.

Now items with hasProvisionalIdentity (unresolved Control Center
generic slots) are excluded from the budget alongside the existing
transient tags (AudioVideoModule, FaceTime, ScreenCaptureUI, GameMode),
so the rebalance computes unmanagedCount=0 and never fires while the
recording indicator is the only unmanaged item (#935).

Signed-off-by: René Jiménez <diazdesandi@proton.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

appearance Tint, shapes, menu bar appearance editor enhancement Iteration, improvement, or optimization of an existing feature feature New capability that did not exist before fix PR that fixes a defect (issue reports use bug) icebar Ice / Thaw Bar popup layout Saved layouts, LayoutBar, reorder, spacing menubar Hide/show, sections, control items, backends, capacity onboarding First-run / tour ops CI, release, GitHub hygiene, scripts — repo operations, not a product surface permissions Accessibility, screen recording, authorization flow profiles Profiles and layout snapshots refactor Code restructuring without behavior change settings Settings UI not covered by a more specific area test Test additions or updates updates Sparkle / release channels / appcast

Projects

Status: Backlog

4 participants