Skip to content

Open data-stale dropdowns in two phases (populateMenu off the click path) - #1375

Closed
hhh2210 wants to merge 3 commits into
steipete:mainfrom
hhh2210:claude/codex-bar-menu-lag-kd181h
Closed

Open data-stale dropdowns in two phases (populateMenu off the click path)#1375
hhh2210 wants to merge 3 commits into
steipete:mainfrom
hhh2210:claude/codex-bar-menu-lag-kd181h

Conversation

@hhh2210

@hhh2210 hhh2210 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Refs #1374, #1274, #1325.

Summary

  • open data-stale dropdowns in two phases: when the only pending invalidations are data-refresh ticks, menuWillOpen attaches the menu's existing content immediately and schedules the rebuild through the existing scheduleOpenMenuRebuildIfStillVisible machinery, so it runs right after the menu is on screen instead of synchronously on the click path
  • structural / privacy / localization invalidations (anything that bumps latestRequiredMenuRebuildVersion) still rebuild synchronously before display, so stale structure is never shown
  • the in-flight-refresh stale-preserve path keeps precedence (perf: cut menu readiness signature cost on store changes #1351 semantics); refresh completion still drives the rebuild in that case
  • closing before the deferred rebuild runs cancels it via the existing forgetClosedMenu cleanup, and the merged menu re-defers until next open
  • tests-only mode (isMenuRefreshEnabled == false) keeps the synchronous rebuild, since the post-display rebuild would never run without open-menu tracking

Why

#1314 deferred the closed merged menu until next open and explicitly scoped out the consequence: "This still does not remove the first populateMenu or reuse hosting views; it eliminates the redundant closed rebuild only."

Since every background data tick while the menu is closed leaves it deferred-stale, the next click pays the full populateMenu (MenuDescriptor build + NSHostingView creation + SwiftUI layout) synchronously inside menuWillOpen before the dropdown can appear — with refreshFrequency = oneMinute and Merge Icons on, effectively every open after ≥1 idle minute. Root-cause walkthrough in #1374; #1325's sample caught populateMenu / addMenuCards as the hot frames on that path, and the post-#1297 retest in #1274 measured it at ~306 menuWillOpen → ~153 refreshMenuForOpenIfNeeded inclusive main-thread samples.

Scope / honesty

  • This does not reduce total rebuild work; it moves the rebuild off the click-blocking path, the same way Reduce merged menu rebuild latency #1286 moved it off the dismiss path. The post-display rebuild runs once while the menu is visible, through the smart-update path that preserves the provider switcher (the path open-menu invalidations already use).
  • The content shown during the brief post-display window is the same content the menu showed when it last closed; only data-refresh invalidations can take this path, so it is data-staleness by definition, never stale structure.
  • First-ever open (menu has no items yet) and required invalidations are unchanged: synchronous rebuild before display.

Validation

  • three focused regression tests in StatusMenuTwoPhaseOpenTests.swift: data-stale open keeps existing items and rebuilds after display; required invalidation still rebuilds synchronously before display; closing before the deferred rebuild cancels it and re-defers the merged menu
  • one updated expectation in StatusMenuOpenRefreshTests.swift: the non-merged stale open now asserts the two-phase deferral instead of the synchronous rebuild (the mechanism is shared, and the deferral is the same win there)
  • swift test (full suite) + SwiftFormat/SwiftLint were green on the fork mirror of this branch at the pre-rebase head (abe46ca3); the rebase onto 6f6cb097 only resolved a CHANGELOG collision with Fix Antigravity summaries for untracked quotas #1369, no code changes

Behavior proof status

Packaged before/after proof on macOS 26.5 (peekaboo open-click timing + sample stack identity: populateMenu under menuWillOpen vs under the deferred-task thunk, #1314-style) is being collected and will be posted as a follow-up comment on this PR. A retest from the #1274/#1314 measurement setup would be very welcome — asked in #1374.

claude added 3 commits June 10, 2026 12:29
When the merged menu is invalidated by data-refresh ticks while closed
(the deferred-until-next-open path from steipete#1274/steipete#1314), menuWillOpen ran
the full populateMenu - including SwiftUI hosting-view creation and
layout - synchronously on the click path before the dropdown could
appear. PR steipete#1314 deliberately scoped this out: "This still does not
remove the first populateMenu."

Attach the existing stale content immediately instead, and schedule the
rebuild from current store data through the open-menu rebuild scheduler
so it runs right after the menu is on screen. Structural, privacy, and
localization invalidations bump latestRequiredMenuRebuildVersion and
still rebuild synchronously before display, and the in-flight-refresh
stale-preserve path keeps precedence. Closing the menu before the
deferred rebuild runs cancels it and re-defers the merged menu until
its next open.

Refs steipete#1274, steipete#1325

https://claude.ai/code/session_01ATo2T7154YGB9ey1TuPt6B
SwiftFormat's redundantAsync rule flags the required-invalidation test
because it never awaits.

https://claude.ai/code/session_01ATo2T7154YGB9ey1TuPt6B
The non-merged attached menu now also keeps stale content on open and
rebuilds right after display, so assert the deferred rebuild instead of
a synchronous one.

https://claude.ai/code/session_01ATo2T7154YGB9ey1TuPt6B

Copilot AI 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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds “two-phase open” behavior for data-stale merged status menus (show existing content immediately, then rebuild after display), with tests and changelog entry to validate/describe the change.

Changes:

  • Defer stale (data-refresh-only) merged-menu rebuild until after the menu is displayed.
  • Add/extend tests covering two-phase open and cancellation behavior.
  • Document the fix in the changelog.

Reviewed changes

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

File Description
Tests/CodexBarTests/StatusMenuTwoPhaseOpenTests.swift New tests validating deferred rebuild behavior and cancellation on close.
Tests/CodexBarTests/StatusMenuOpenRefreshTests.swift Extends existing test to assert the two-phase open sequence.
Sources/CodexBar/StatusItemController+MenuTracking.swift Implements the “defer stale rebuild until after display” decision logic and scheduling.
CHANGELOG.md Notes the user-visible menu-bar behavior change/fix.

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

Comment on lines +128 to +139
if self.canDeferStaleMenuRebuildUntilAfterDisplay(menu) {
#if DEBUG
self.menuLogger.debug(
"menu open deferred stale rebuild until after display",
metadata: [
"items": "\(menu.items.count)",
"provider": provider?.rawValue ?? "nil",
])
#endif
self.scheduleOpenMenuRebuildIfStillVisible(menu, provider: provider)
return
}
Comment on lines +157 to +164
private func canDeferStaleMenuRebuildUntilAfterDisplay(_ menu: NSMenu) -> Bool {
// Without open-menu tracking the post-display rebuild would never run, leaving stale content.
guard self.isMenuRefreshEnabled else { return false }
guard !menu.items.isEmpty else { return false }
let key = ObjectIdentifier(menu)
guard let menuVersion = self.menuVersions[key] else { return false }
return menuVersion >= self.latestRequiredMenuRebuildVersion
}
Comment on lines +58 to +60
for _ in 0..<40 where controller.menuVersions[key] != controller.menuContentVersion {
await Task.yield()
}
Comment on lines +152 to +154
for _ in 0..<40 {
await Task.yield()
}
@clawsweeper

clawsweeper Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codex review: needs real behavior proof before merge. Reviewed June 10, 2026, 12:36 AM ET / 04:36 UTC.

Summary
The PR defers data-only stale menu rebuilds until after a dropdown is displayed and adds focused menu lifecycle tests plus a changelog entry.

Reproducibility: yes. for the introduced correctness problem by source inspection: a closed data tick leaves the baseline old, the PR defers the root-open rebuild, and a later open-menu change back to that old signature can skip refresh. The original click-latency improvement still needs real app proof.

Review metrics: 2 noteworthy metrics.

  • Changed files: 4 files affected. The patch changes one runtime menu lifecycle file, two test files, and one changelog entry.
  • Regression coverage: 3 tests added, 1 expectation updated. The tests cover intended two-phase open cases but not the readiness-baseline revert path called out in review.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🦐 gold shrimp
Result: blocked until real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Fix the readiness-baseline re-anchor after deferred open rebuilds and add regression coverage for a reverted open-menu store change.
  • Post redacted packaged-app proof, such as timing/sample output or a recording with diagnostics, showing populateMenu moved off the click path.

Proof guidance:

  • [P1] Needs real behavior proof before merge: The PR body says packaged before/after proof is still being collected, and no real after-fix output or media is present yet. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Mantis proof suggestion
A desktop proof run would materially help verify the user-visible menu timing and deferred rebuild behavior in the real app. A maintainer can ask Mantis to capture proof by posting a new PR comment that starts with the OpenClaw Mantis account mention, followed by:

visual task: verify CodexBar's merged menu opens immediately after a data-refresh tick, then updates while visible without blocking the click path.

Risk before merge

  • [P1] Merging as-is can leave an open menu showing the deferred-rebuild data after a later store change because the readiness baseline can remain at the old pre-refresh signature.
  • [P1] The PR body says real packaged before/after behavior proof is still pending, so the click-latency and stack-move claim is not yet demonstrated on a real app run.

Maintainer options:

  1. Fix the deferred-baseline path (recommended)
    Update the deferred open rebuild completion to record the readiness baseline that matches the newly rendered data and cover the reverted-store-data case with a focused test.
  2. Pause until runtime proof is posted
    Hold merge until the contributor posts redacted packaged-app proof showing the menu opens before populateMenu and then updates while visible.
  3. Accept stale visible data risk
    Maintainers could intentionally accept the baseline risk for latency, but that would trade correctness for responsiveness and should be explicit.

Next step before merge

  • [P1] Contributor or maintainer action is needed because the patch has a baseline correctness blocker and missing after-fix proof that automation cannot supply.

Security
Cleared: The diff only changes menu lifecycle code, tests, and changelog text; no security or supply-chain sensitive surface was added.

Review findings

  • [P2] Re-anchor readiness after the deferred rebuild — Sources/CodexBar/StatusItemController+MenuTracking.swift:137
Review details

Best possible solution:

Keep the two-phase open shape, but re-anchor readiness after the deferred rebuild, add a reverted-live-data regression test, and post redacted packaged-app timing or sample proof before merge.

Do we have a high-confidence way to reproduce the issue?

Yes for the introduced correctness problem by source inspection: a closed data tick leaves the baseline old, the PR defers the root-open rebuild, and a later open-menu change back to that old signature can skip refresh. The original click-latency improvement still needs real app proof.

Is this the best way to solve the issue?

No; deferring data-only rebuilds is a plausible direction, but the current implementation needs to preserve the readiness-baseline semantics from current main and provide real behavior proof.

Full review comments:

  • [P2] Re-anchor readiness after the deferred rebuild — Sources/CodexBar/StatusItemController+MenuTracking.swift:137
    This schedules the rebuild before menuWillOpen can treat the root menu as freshly rendered, so the readiness baseline can remain at the pre-refresh signature. If store data changes while the menu is open back to that old signature, didMenuAdjunctReadinessChange() returns false and the visible menu can stay on the deferred-build data. Re-anchor the baseline after the deferred rebuild and cover the reverted-data case before merging.
    Confidence: 0.88

Overall correctness: patch is incorrect
Overall confidence: 0.88

AGENTS.md: found and applied where relevant.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 6f6cb097dc58.

Label changes

Label changes:

  • add P2: This is a bounded menu responsiveness PR with a correctness blocker in the menu refresh path.
  • add merge-risk: 🚨 other: The merge risk is stale visible usage data after a deferred rebuild, which is meaningful but outside the more specific owned risk labels.
  • add rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🦐 gold shrimp.
  • add status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR body says packaged before/after proof is still being collected, and no real after-fix output or media is present yet. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Label justifications:

  • P2: This is a bounded menu responsiveness PR with a correctness blocker in the menu refresh path.
  • merge-risk: 🚨 other: The merge risk is stale visible usage data after a deferred rebuild, which is meaningful but outside the more specific owned risk labels.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🦐 gold shrimp.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR body says packaged before/after proof is still being collected, and no real after-fix output or media is present yet. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

What I checked:

Likely related people:

  • Yuxin-Qiao: The readiness-baseline resync behavior that the patch bypasses was introduced in the merged menu-readiness work. (role: recent baseline contributor; confidence: high; commits: 322c8f8c2d36; files: Sources/CodexBar/StatusItemController+MenuRefreshScheduling.swift, Sources/CodexBar/StatusItemController+Menu.swift, Tests/CodexBarTests/StatusMenuReadinessBaselineTests.swift)
  • hhh2210: Prior merged work on the same merged-menu deferral and open-refresh tests shaped the current stale-menu behavior this PR extends. (role: recent merged-menu deferral contributor; confidence: high; commits: 88eb603fecf3; files: Sources/CodexBar/StatusItemController+MenuTracking.swift, Tests/CodexBarTests/StatusMenuOpenRefreshTests.swift)
  • Peter Steinberger: Merge and release commits in the local history carry the relevant menu-tracking and readiness changes into current main. (role: merger and release integrator; confidence: medium; commits: 88eb603fecf3, 322c8f8c2d36, 920997c6a365; files: Sources/CodexBar/StatusItemController+MenuTracking.swift, Sources/CodexBar/StatusItemController+MenuRefreshScheduling.swift, Sources/CodexBar/StatusItemController+Menu.swift)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 10, 2026
@steipete

Copy link
Copy Markdown
Owner

Closing this approach after landing #1376 in e97bfb0db9eb0883ad92d173d8accc547879bcd2.

#1376 fixes the observed WindowServer stall by deferring parent-menu recomposition caused by data refreshes until AppKit menu tracking ends. This PR instead schedules the full populateMenu rebuild immediately after display while the menu is being tracked, which reintroduces the same expensive parent-menu reconstruction during the unsafe window.

#1374 remains open. Its first-open stale-content latency still needs a design that does not rebuild the full parent menu during tracking.

Thanks @hhh2210 for the investigation, tests, and clearly documented tradeoffs.

@steipete steipete closed this Jun 10, 2026
hhh2210 added a commit to hhh2210/CodexBar that referenced this pull request Jun 10, 2026
The two-phase open returns from refreshMenuForOpenIfNeeded while the menu
is still marked stale, so menuWillOpen skips the root-open baseline resync.
Anchor the baseline in rebuildOpenMenuIfStillVisible once the deferred
rebuild renders current store data; otherwise an open-menu store change
reverting to the pre-rebuild signature compares equal against the stale
baseline and the visible menu keeps the deferred-rebuild data.

Addresses the ClawSweeper P2 on steipete#1375.
@hhh2210

hhh2210 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Pushed d85192a addressing the ClawSweeper P2 (readiness-baseline re-anchor) — the analysis was correct, thanks.

What was wrong: the two-phase open returns from refreshMenuForOpenIfNeeded while the menu is still marked stale, so the !menuNeedsRefresh gate in menuWillOpen skipped resyncMenuAdjunctReadinessBaselineForRootOpen, and the deferred rebuild (rebuildOpenMenuIfStillVisible) repopulated + markMenuFreshed without recording a new baseline. An open-menu store change reverting to the pre-rebuild signature would then compare equal in didMenuAdjunctReadinessChange() and skip the refresh, leaving the visible menu on the deferred-rebuild data.

Fix: rebuildOpenMenuIfStillVisible now calls resyncMenuAdjunctReadinessBaseline() right after markMenuFresh, gated to non-hosted-subview menus. This is the moment the visible root menu and live store data are in sync, matching the contract documented on resyncMenuAdjunctReadinessBaseline. For the existing mid-open invalidation flow this is a no-op, because the observation handler's didMenuAdjunctReadinessChange() already advanced the baseline before scheduling the rebuild; the in-flight-refresh stale-content path (#1351) is unaffected since it never reaches the rebuild completion.

Regression coverage: added deferred open rebuild re-anchors readiness baseline so reverted store data still refreshes to StatusMenuTwoPhaseOpenTests. It drives the deferred rebuild completion deterministically (no interleaved observation task can mask the regression) and asserts the reverted store value still registers as a readiness change. Verified red on the previous commit, green with the fix; swift test + make check pass.

Packaged behavior proof (peekaboo open-click timing + sample stack identity, before/after) is still being collected and will follow as a separate comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants