@@ -426,11 +426,6 @@ extension MenuBarItemManager {
426426 /// of the previous cache.
427427 private( set) var cachedItemWindowIDs = [ CGWindowID] ( )
428428
429- /// A list of the menu bar item tags (namespace:title) at the time
430- /// of the previous cache. Used to detect actual app restarts
431- /// instead of relying on window IDs which can be reassigned.
432- private( set) var cachedItemTags = [ String] ( )
433-
434429 /// Runs the given async closure as a task and waits for it to
435430 /// complete before returning.
436431 ///
@@ -448,15 +443,9 @@ extension MenuBarItemManager {
448443 cachedItemWindowIDs = itemWindowIDs
449444 }
450445
451- /// Updates the list of cached menu bar item tags.
452- func updateCachedItemTags( _ itemTags: [ String ] ) {
453- cachedItemTags = itemTags
454- }
455-
456446 /// Clears the list of cached menu bar item window identifiers.
457447 func clearCachedItemWindowIDs( ) {
458448 cachedItemWindowIDs. removeAll ( )
459- cachedItemTags. removeAll ( )
460449 }
461450 }
462451
@@ -808,7 +797,6 @@ extension MenuBarItemManager {
808797 }
809798
810799 let previousWindowIDs = await cacheActor. cachedItemWindowIDs
811- let previousItemTags = await cacheActor. cachedItemTags
812800 let displayID = Bridging . getActiveMenuBarDisplayID ( )
813801 MenuBarItemManager . diagLog. debug ( " cacheItemsRegardless: displayID= \( displayID. map { " \( $0) " } ?? " nil " ) , previousWindowIDs count= \( previousWindowIDs. count) " )
814802
@@ -831,9 +819,6 @@ extension MenuBarItemManager {
831819 let itemWindowIDs = currentItemWindowIDs ?? items. reversed ( ) . map { $0. windowID }
832820 await cacheActor. updateCachedItemWindowIDs ( itemWindowIDs)
833821
834- let itemTags = items. map { " \( $0. tag. namespace) : \( $0. tag. title) " }
835- await cacheActor. updateCachedItemTags ( itemTags)
836-
837822 await MainActor . run {
838823 MenuBarItemTag . Namespace. pruneUUIDCache ( keeping: Set ( itemWindowIDs) )
839824 self . pruneMoveOperationTimeouts ( keeping: Set ( items. map ( \. tag) ) )
@@ -912,8 +897,7 @@ extension MenuBarItemManager {
912897 let didRestoreSections = await restoreItemsToSavedSections (
913898 items,
914899 controlItems: controlItems,
915- previousWindowIDs: previousWindowIDs,
916- previousItemTags: previousItemTags
900+ previousWindowIDs: previousWindowIDs
917901 )
918902 if didRestoreSections {
919903 MenuBarItemManager . diagLog. debug ( " Restored item to saved section; scheduling recache " )
@@ -935,8 +919,7 @@ extension MenuBarItemManager {
935919 let didRestoreOrder = await restoreSavedItemOrder (
936920 items,
937921 controlItems: controlItems,
938- previousWindowIDs: previousWindowIDs,
939- previousItemTags: previousItemTags
922+ previousWindowIDs: previousWindowIDs
940923 )
941924
942925 if didRestoreOrder {
@@ -2842,26 +2825,23 @@ extension MenuBarItemManager {
28422825 private func restoreItemsToSavedSections(
28432826 _ items: [ MenuBarItem ] ,
28442827 controlItems: ControlItemPair ,
2845- previousWindowIDs _: [ CGWindowID ] ,
2846- previousItemTags: [ String ]
2828+ previousWindowIDs: [ CGWindowID ]
28472829 ) async -> Bool {
28482830 guard !savedSectionOrder. isEmpty else { return false }
28492831 guard !suppressNextNewLeftmostItemRelocation else { return false }
28502832 guard !lastMoveOperationOccurred( within: . seconds( 2 ) ) else { return false }
28512833
2852- // Use item tags (namespace:title) instead of window IDs to detect app restarts.
2853- // Window IDs can be reassigned by the WindowServer even when apps are still running,
2854- // which would incorrectly trigger restore and cause items to move randomly.
2855- let currentTags = Set ( items. map { " \( $0. tag. namespace) : \( $0. tag. title) " } )
2856- let previousTagsSet = Set ( previousItemTags)
2857-
2858- // Only restore when previous item tags have disappeared (actual app restart).
2859- // If same items are present (even with different window IDs), skip restore.
2860- guard !previousTagsSet. isEmpty && !previousTagsSet. isSubset ( of: currentTags) else {
2861- MenuBarItemManager . diagLog. debug ( " restoreItemsToSavedSections: no app restart detected (same items present), skipping " )
2834+ // Only restore when previous window IDs have disappeared (app restarted).
2835+ // This prevents undoing the user's manual section moves on regular cache refreshes.
2836+ let currentWindowIDSet = Set ( items. map ( \. windowID) )
2837+ let previousWindowIDSet = Set ( previousWindowIDs)
2838+ guard !previousWindowIDSet. isEmpty && !previousWindowIDSet. isSubset ( of: currentWindowIDSet) else {
2839+ MenuBarItemManager . diagLog. debug ( " restoreItemsToSavedSections: no app restart detected (window IDs unchanged), skipping " )
28622840 return false
28632841 }
28642842
2843+ // Get current item tags.
2844+ let currentTags = Set ( items. map { " \( $0. tag. namespace) : \( $0. tag. title) " } )
28652845 let savedTags = Set ( savedSectionOrder. values. flatMap { $0 } )
28662846 let savedTagsInCurrent = savedTags. intersection ( currentTags)
28672847
@@ -3001,8 +2981,7 @@ extension MenuBarItemManager {
30012981 private func restoreSavedItemOrder(
30022982 _ items: [ MenuBarItem ] ,
30032983 controlItems: ControlItemPair ,
3004- previousWindowIDs _: [ CGWindowID ] ,
3005- previousItemTags: [ String ]
2984+ previousWindowIDs: [ CGWindowID ]
30062985 ) async -> Bool {
30072986 guard !savedSectionOrder. isEmpty else { return false }
30082987
@@ -3018,15 +2997,16 @@ extension MenuBarItemManager {
30182997 // will have no recent move timestamp.
30192998 guard !lastMoveOperationOccurred( within: . seconds( 2 ) ) else { return false }
30202999
3021- // Use item tags (namespace:title) instead of window IDs to detect app restarts.
3022- // Window IDs can be reassigned by the WindowServer even when apps are still running,
3023- // which would incorrectly trigger restore and cause items to move randomly.
3024- let currentTags = Set ( items. map { " \( $0. tag. namespace) : \( $0. tag. title) " } )
3025- let previousTagsSet = Set ( previousItemTags)
3026-
3027- // Only restore when previous item tags have disappeared (actual app restart).
3028- // If same items are present (even with different window IDs), skip restore.
3029- guard !previousTagsSet. isSubset ( of: currentTags) else { return false }
3000+ // Only restore when previous window IDs have disappeared, indicating
3001+ // an app restarted (old windows destroyed, new ones created). During
3002+ // move operations macOS can briefly report duplicate windows for the
3003+ // same item, which adds transient IDs to the current set. Checking
3004+ // for removed IDs (rather than any set difference) avoids false
3005+ // positives from these duplicates and from user drag-and-drop, which
3006+ // only repositions existing windows without removing any.
3007+ let currentWindowIDSet = Set ( items. lazy. map ( \. windowID) )
3008+ let previousWindowIDSet = Set ( previousWindowIDs)
3009+ guard !previousWindowIDSet. isSubset ( of: currentWindowIDSet) else { return false }
30303010
30313011 // Don't interfere with items that are currently temporarily shown.
30323012 let activelyShownTags = Set ( temporarilyShownItemContexts. map {
0 commit comments