diff --git a/.github/pr-proof/cached-menu-card-shell-swap.log b/.github/pr-proof/cached-menu-card-shell-swap.log new file mode 100644 index 0000000000..f24d58cdd7 --- /dev/null +++ b/.github/pr-proof/cached-menu-card-shell-swap.log @@ -0,0 +1,33 @@ +CodexBar cached menu-card shell swap - native runtime proof +Captured: 2026-08-20T12:27:38+05:30 +Commit: f411a02817c20ebc7ff5f844d78925fe8f4b177c +Host: macOS 26.3 (25D125), arm64 + +Setup +- Ad-hoc debug app bundle using the binary built from the commit above and launched + under a disposable bundle/preferences domain. +- Keychain access and web extras were disabled. The attached frame contains no + account identity or usage values. +- MenuSwitchFlickerProbe drove the real merged NSMenu and captured its WindowServer + composite with CGWindowListCreateImage. + +Run +- Two native-menu sessions, six programmatic provider/overview switches each. +- 176 + 236 = 412 captured samples; nil captures: 0 in both sessions. +- Production trace recorded 12 cached-swap paths and one reconcile path (the initial + menu construction for session two). +- The attached frame is the first cached Claude -> Codex switch, captured at + session +449.2 ms. The switch was handled at +406 ms; the cached swap ran from + process +504.6 ms to +511.3 ms. + +Visual check around that cached switch +- Pre-switch frames at +409.2 ms and +429.0 ms were byte-identical. +- Post-switch frames at +449.2 ms and +469.9 ms were byte-identical. +- A system-blue pixel scan found zero blue pixels below y=140 in all four frames. + The only system-blue region was the expected selected provider segment in the + switcher header. No native blue selection layer appeared behind the card. + +Limit +- This host is macOS 26.3, not the reporter's macOS 27 build. The source regression + and subclass-preservation test are deterministic; macOS 27 visual confirmation + remains useful. diff --git a/.github/pr-proof/cached-menu-card-shell-swap.png b/.github/pr-proof/cached-menu-card-shell-swap.png new file mode 100644 index 0000000000..ae006432f8 Binary files /dev/null and b/.github/pr-proof/cached-menu-card-shell-swap.png differ diff --git a/Sources/CodexBar/StatusItemController+MenuReconcile.swift b/Sources/CodexBar/StatusItemController+MenuReconcile.swift index dafa499038..f02bda659a 100644 --- a/Sources/CodexBar/StatusItemController+MenuReconcile.swift +++ b/Sources/CodexBar/StatusItemController+MenuReconcile.swift @@ -118,7 +118,12 @@ extension StatusItemController { let requiresNativeImageReplacement = self.shouldReplaceNativeImageItemDuringReconcile(liveItem) || self.shouldReplaceNativeImageItemDuringReconcile(newItem) - if liveItem.isSeparatorItem == newItem.isSeparatorItem, !requiresNativeImageReplacement { + let hasCompatibleItemClass = + ObjectIdentifier(type(of: liveItem)) == ObjectIdentifier(type(of: newItem)) + if liveItem.isSeparatorItem == newItem.isSeparatorItem, + hasCompatibleItemClass, + !requiresNativeImageReplacement + { if !liveItem.isSeparatorItem { self.swapMenuItemContents(liveItem, newItem) } diff --git a/Tests/CodexBarTests/MenuCardViewRecyclingTests.swift b/Tests/CodexBarTests/MenuCardViewRecyclingTests.swift index 3d16275311..26a6999ded 100644 --- a/Tests/CodexBarTests/MenuCardViewRecyclingTests.swift +++ b/Tests/CodexBarTests/MenuCardViewRecyclingTests.swift @@ -366,6 +366,41 @@ extension StatusMenuTests { #expect(displacedIncoming.first === incoming) } + @Test + func `cached provider content preserves menu item subclasses across switch back`() { + let previousRendering = StatusItemController.menuCardRenderingEnabled + StatusItemController.menuCardRenderingEnabled = true + defer { StatusItemController.menuCardRenderingEnabled = previousRendering } + + let settings = self.makeSettings() + settings.statusChecksEnabled = false + let controller = self.makeRecyclingController(settings: settings) + defer { controller.releaseStatusItemsForTesting() } + + let plainItem = NSMenuItem(title: "Overview", action: nil, keyEquivalent: "") + let cardItem = controller.makeMenuCardItem(Text("Codex"), id: "codex", width: 300) + let menu = NSMenu() + menu.addItem(plainItem) + + let displacedPlain = controller.replaceMenuContentKeepingRowsVisible( + menu, + fromIndex: 0, + with: [cardItem]) + + #expect(menu.items.first === cardItem) + #expect(menu.items.first is MenuCardMenuItem) + #expect(displacedPlain.first === plainItem) + + let displacedCard = controller.replaceMenuContentKeepingRowsVisible( + menu, + fromIndex: 0, + with: displacedPlain) + + #expect(menu.items.first === plainItem) + #expect(!(menu.items.first is MenuCardMenuItem)) + #expect(displacedCard.first === cardItem) + } + @Test func `cached provider content swap preserves both item sets for switch back`() { let settings = self.makeSettings()