Add opt-in NSPopover menu with a persistent SwiftUI hierarchy#1428
Add opt-in NSPopover menu with a persistent SwiftUI hierarchy#1428Jassy930 wants to merge 33 commits into
Conversation
Add usePopoverMenu: Bool feature flag to SettingsStore following the exact usageBarsShowUsed pattern (three-point: state struct, loadDefaultsState, computed property). Defaults to false; UserDefaults key "usePopoverMenu". Includes TDD tests in PopoverMenuFeatureFlagTests.swift. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
新增 Sources/CodexBar/Popover/MenuViewModel.swift,作为 popover 重构的 单一可观察状态源;新增配套测试 4 个(全部通过)。不接 UI,不改现有行为。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ctive) 阶段 0 骨架:NSPopover(.transient) 承载持久 SwiftUI 根视图; show/close 同步 MenuViewModel.isVisible;PopoverRootView 占位(310pt 宽 Color.clear)。 测试走真实路径(NSStatusBarButton),未采用 GUI 降级预案。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, inactive) Adds menuViewModel, popoverMenuController?, and usePopoverMenu computed property to StatusItemController as gated holding points. No existing logic is touched; the feature flag defaults to false so behavior is byte-for-byte identical until Task 1.1 wires the popover activation path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s on (merged mode) - attachMenus(): when usePopoverMenu is true, set statusItem.menu = nil, lazily init PopoverMenuController, and wire button target/action to handleStatusItemClick; original NSMenu path is unchanged when flag is off - openMenuFromShortcut(): merged-mode branch now calls popoverMenuController?.show() when usePopoverMenu is on; falls back to performClick when off (identical to before) - New @objc handleStatusItemClick(_:): guards on usePopoverMenu, refreshes providers, calls popoverMenuController?.toggle(relativeTo: button) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PopoverRootView 重写为完整阶段 1 实现:持久视图内嵌 SwiftUI provider 切换器(纯文字按钮,>1 provider 时显示)+ UsageMenuCardView 卡片; 切换 provider 只改 viewModel.select(...),SwiftUI 增量更新不重建视图。 store 注入保证 @observable 观察链,数据变化自动重渲。 attachMenus 构造点用局部常量 + [weak self] 捕获,无强引用环。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 1.3:给 PopoverMenuController 加 NSEvent local key monitor,Esc(keyCode 53) 触发 close();show() 时装载 monitor,close() 时卸载,deinit 兜底防泄漏。 暴露 handleKeyDownForTesting 测试接缝,新增 escapeKeyClosesPopover / nonEscapeKeyNotHandled 两个测试,全套 3411 测试通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- MenuViewModel: add selectNext/selectPrevious/selectProvider(atIndex:) navigation helpers with cycleSelection(by:) wrapping through Overview + all providers - StatusItemMenuProviderNavigationDirection: add Equatable conformance for test assertions - PopoverMenuController: replace handleKeyDown with unified handle(characters:keyCode:modifiers:) that dispatches character-level shortcuts (Cmd+R/,/Q, Cmd+1-9) and keyCode-level keys (Esc, ← →) via injected callbacks onRefresh/onSettings/onQuit/onNavigate/onSelectIndex - StatusItemController.attachMenus: wire callbacks on first controller creation with [weak self] to prevent retain cycles - Tests: 7 MenuViewModelTests + 9 PopoverMenuControllerTests all pass (3420 total, 0 failures) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…set (review fixes) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Task{@mainactor} wrapper deferred handleDidClose() to a later runloop,
so suppressNextToggleOpen was set AFTER the button.action toggle fired —
defeating the double-trigger guard and leaving the popover un-closable by
clicking the status item. NSPopoverDelegate fires on the main thread, so
assumeIsolated runs the cleanup synchronously before the action.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Apply swiftformat to the Phase 0/1 popover files. - Extract popover attach/wiring from StatusItemController into a new StatusItemController+Popover.swift extension so the main class body stays under swiftlint's type_body_length limit (and groups popover code for upcoming phases). - Annotate SettingsStore.loadDefaultsState with swiftlint:disable:next function_body_length (one-line-over a large flat defaults loader). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Codex review: needs changes before merge. Reviewed June 13, 2026, 7:25 AM ET / 11:25 UTC. Summary Reproducibility: yes. for the review finding from source: current main attaches Review metrics: 3 noteworthy metrics.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Mantis proof suggestion Risk before merge
Maintainer options:
Copy recommended automerge instructionNext step before merge
Security Review findings
Review detailsBest possible solution: Fix the fallback semantics, then land only after maintainer acceptance of the hidden opt-in soak path while keeping the legacy NSMenu default until provider/account parity is proven enough for a later cleanup PR. Do we have a high-confidence way to reproduce the issue? Yes for the review finding from source: current main attaches Is this the best way to solve the issue? No, not as-is: the popover path should preserve provider-less fallback semantics before merge. After that narrow repair, the default-off soak strategy remains a maintainer product/maintenance decision. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 69831cad6cac. Label changesLabel changes:
Label justifications:
Evidence reviewedAcceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
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
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2007863359
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| private func performLegacyMenuAction(_ action: MenuDescriptor.MenuAction) { | ||
| let (sel, payload) = self.selector(for: action) | ||
| let item = NSMenuItem() | ||
| item.representedObject = payload | ||
| _ = self.perform(sel, with: item) |
There was a problem hiding this comment.
Set the selected provider before dispatching actions
When the popover is showing a provider different from lastMenuProvider (or in split-icon mode where lastMenuProvider may still be nil), provider-dependent actions like Usage Dashboard, Status Page, and Changelog are dispatched without updating that legacy context. MenuDescriptor.MenuAction.dashboard/statusPage/changelog carry no provider payload and the target selectors resolve through lastMenuProvider, so clicking these rows from a Claude/OpenAI/etc. popover can open Codex or the previously opened provider instead of the visible provider. Set lastMenuProvider from the current popover selection/per-provider view model before calling the legacy selector.
Useful? React with 👍 / 👎.
| if action != .quit { | ||
| self.popoverMenuController?.close() | ||
| } |
There was a problem hiding this comment.
Close the active split popover after menu actions
In split-icon popover mode, onAction uses this same performMenuAction, but this only closes the merged popoverMenuController. Since per-provider popovers live in providerPopoverControllers, clicking actions such as Settings, Dashboard, Switch Account, or Refresh from a split provider popover leaves that popover visible instead of dismissing like the NSMenu path; close the active provider popover as well (or call the all-popovers close helper) for non-quit actions.
Useful? React with 👍 / 👎.
- 新增 PopoverActionSectionsView(SwiftUI),将 MenuDescriptor.Section 列表 渲染为 popover 底部动作区,与 NSMenu 路径共用同一数据源。 - 在 StatusItemController+Popover 添加 performMenuAction(_:) 与 performLegacyMenuAction(_:),通过 selector(for:) 复用 NSMenu 全部分发逻辑; 有 payload 时构造临时 NSMenuItem 传递 representedObject,无 payload 时直接 perform(sel),quit 动作不关闭 popover。 - attachMergedPopover() 补充 makeSections/onAction 注入,PopoverRootView 在 content 区之后渲染动作区并底部 padding 6pt。 - 新增测试 PopoverActionDispatchTests:shortcutLabel 映射 + metaSection 数据源契约。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
新增 PopoverCardPlan 纯数据结构,StatusItemController+Popover 实现 popoverCardPlan(for:) 完整复刻 addMenuCards 分流逻辑(Codex workspace stacked / Token stacked / Kilo multi-scope / 单卡),含 storageText 与 showBuyCredits;PopoverRootView 改为消费 makeCardPlan 注入渲染计划, 并新增 onBuyCredits 回调。新增 PopoverCardPlanTests 8 个测试。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 2.3: 新增 PopoverAccountSwitcherView(纯渲染 SwiftUI Segment 切换器); 在 StatusItemController+Popover 中加入 PopoverAccountSwitcherModel 构造逻辑, Codex 路径接入 handleCodexVisibleAccountSelectionFromPopover(对应 NSMenu 的 handleCodexVisibleAccountSelection),Token 路径接入 handleTokenAccountSelectionFromPopover(对应 makeTokenAccountSwitcherItem 中的 onSelect 闭包);PopoverRootView 注入 makeAccountSwitcher 参数并在 单 provider 内容分支的卡片上方插入切换器 + Divider。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
实现 Task 2.4:popover 的 .overview 分支从占位升级为完整 Overview 模式。 - StatusItemController+Popover: 新增 PopoverOverviewRow 结构体(id = provider.rawValue)、 popoverOverviewRows()(与 addOverviewRows 同源:reconcileSelected→compactMap model→过滤 isOverviewErrorOnly→附 storageText)、 popoverOverviewEmptyText()(两档空态文案与 NSMenu 一致),以及更新 makeSections 闭包(overview 时 includeContextualActions: false,provider 取可用优先逻辑对齐 populateMenu)。 - PopoverRootView: 注入 makeOverviewRows/overviewEmptyText 闭包,实现 overviewContent: 空态 Text + 非空时 ForEach OverviewMenuCardRowView + 行间 Divider + 点击切 provider。 - Tests/PopoverOverviewTests: 9 个测试(id 稳定性 3、overview↔provider 切换 6),全绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- MenuViewModel: add includesOverview flag; navigationStops now conditionally includes .overview based on includesOverview - PopoverRootView: inject switcherIcon closure; render Overview tab (square.grid.2x2) when includesOverview; show brand icon + label for providers when switcherIcon returns non-nil - StatusItemController+Popover: add refreshPopoverViewModelInputs() to sync providers/includesOverview and correct .overview selection when overview is absent; add popoverSwitcherIcon(for:) via ProviderBrandIcon (avoids private switcherIcon in +Menu.swift) - StatusItemController+Actions: use refreshPopoverViewModelInputs() in handleStatusItemClick and openMenuFromShortcut - Tests: update MenuViewModelTests and PopoverOverviewTests to set includesOverview=true where navigation through overview is expected; add two new tests for includesOverview=false behavior Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add onSelectionChanged callback to MenuViewModel (fires only on actual change), wire it in attachMergedPopover to write back mergedMenuLastSelectedWasOverview / selectedMenuProvider to settings, and update refreshPopoverViewModelInputs to restore the last selection from settings on every popover open (mirrors resolvedSwitcherSelection logic from the NSMenu path). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With >4 tabs the single-row HStack overflowed the 310pt popover. Switch to LazyVGrid(.adaptive(minimum: 64)) so tabs wrap into multiple rows, matching the NSView switcher's width-driven multi-row behavior. Few tabs keep the compact single-row layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iew fixes) - C1: stacked 路径(Codex/Token/Kilo)空 cards 时 fallback 到单卡片,通过私有 helper applyStackedFallback 消除重复,与 NSMenu addStackedMenuCards/addStackedCodexMenuCards 语义对齐 - C2: stacked 路径强制 showBuyCredits=false,仅单卡片路径保留 popoverCanShowBuyCredits 计算,与 NSMenu stacked 路径 return false 早退行为一致 - I1: refreshPopoverViewModelInputs 中 includesOverview 增加 enabledProviders.count > 1 条件,单 provider 时不显示切换器 - I3: onSelectionChanged 注册移至 refreshPopoverViewModelInputs 调用之前,确保首次 restore 写回 settings 不丢失 - m2: performLegacyMenuAction 统一构造 NSMenuItem 并 perform(_:with:),消除两路分支与 addCodexAccount ABI 歧义 - m1: PopoverOverviewTests 第一个测试从重言式改为实际构造 PopoverOverviewRow 并断言 row.id == provider.rawValue Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
新增 PopoverChartKind 枚举(6 类图表:usageBreakdown/creditsHistory/costHistory/usageHistory/storageBreakdown/zaiHourly), popoverChartEntries(for:) 入口清单方法(顺序/去重对齐 NSMenu),popoverOverviewChart(for:model:) overview 图表入口, 以及 popoverChartView(for:width:) 懒构造工厂(数据获取与 +HostedSubmenus.swift append* 方法完全对齐)。 新增 PopoverChartTests:19 项测试全绿,全量 3483 项通过,lint 0 violations。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 3.2:在 PopoverRootView 接入六类图表二级 popover 下钻入口。 - 注入 makeChartEntries/makeOverviewChart/makeChartView 三闭包 - @State presentedChart 驱动 .popover(item:) 侧边浮层(整体 bounds 锚点) - 单 provider 视图:卡片区之后渲染下钻入口行列表(带 chevron.right) - Overview 行:有下钻时在行尾附加独立 chevron 按钮,行主体 Button 保持切 provider - onChange selection/isVisible 自动收起子 popover - attachMergedPopover 补三个 [weak self] 闭包注入 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…der mode Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- attachProviderPopovers mirrors attachMenus(fallback:) traversal: each enabled/fallback provider gets its own MenuViewModel.singleProvider + PopoverMenuController (no switcher, no overview). - Extract makePopoverController factory shared by merged & per-provider modes (vm parameterized so closures capture their own view model). - Mutual exclusion: opening one provider's popover closes all others. - Keyboard shortcut open (openMenuFromShortcut) toggles per-provider popover; Cmd+R/,/Q wired per controller; arrows/digits intentionally no-op in single-provider panels. - clearPopoverButtonActions cleans residual button actions when switching back to NSMenu mode (both merged and split paths). - Buy Credits now closes whichever popover is frontmost via closeAllProviderPopovers. - Plan docs for phases 2-4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…led actions, zai details Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add MenuRowHoverHighlight ViewModifier (accent bg + white text, RoundedRect r4, h-padding 5) - Extract ActionRowButton, OverviewRowView, ChartEntryRowView, BuyCreditsRowView as independent Views to hold @State isHovered; disabled rows skip hover - subtitle/shortcut labels adopt white.opacity(0.75) on hover for legibility - a11y: root VStack .accessibilityElement(children:.contain) + label "CodexBar menu" - a11y: switcher container .accessibilityElement(children:.contain) - a11y: overviewTab .accessibilityLabel("Overview"); providerTab .accessibilityLabel(provider.rawValue) - a11y: overview row Button label "\(provider) overview" + hint "Show \(provider) details"; chevron Button label = chart.title - a11y: PopoverActionSectionsView adds shortcutHint() mapping (refresh/settings/quit) applied via .accessibilityHint on ActionRowButton - a11y: PopoverAccountSwitcherView segment button .accessibilityLabel(title) + .accessibilityAddTraits(.isSelected) when selected Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…parison Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…filter, focus ring) - PopoverActionSectionsView: apply actionable section filter identical to NSMenu addActionableSections (only render sections containing action/submenu entries, suppressing pure-text usage/account info sections) - PopoverRootView switcher: rewrite provider tabs to vertical stacked layout (icon 18pt above, caption2 title below) with solid accentColor capsule for selected state and transparent+secondary for unselected, matching NSView ProviderSwitcherView stackedIcons mode - Overview tab: vertical layout with square.grid.2x2 icon + "Overview" label - Provider titles: use ProviderDescriptorRegistry.descriptor.metadata.displayName (same source as NSView switcherTitle(for:)) instead of rawValue - Switcher indicator bars: inject switcherIndicator closure in PopoverRootView; StatusItemController wires it via switcherWeeklyRemaining + branding.color (identical data path to NSView quotaIndicator); 3pt rounded bar, gray track + brand-color fill, hidden when tab is selected - Fixed 3-column LazyVGrid for >3 tabs (vs previous adaptive grid) - focusEffectDisabled() on all interactive rows to eliminate blue focus rings in popover (ActionRowButton, ChartEntryRowView, BuyCreditsRowView, OverviewRowView buttons, ProviderSwitcherTabView) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
对齐原版 NSMenu addMenuCardSections 拆段形态: - PopoverCardPlan 新增 SectionedCard struct,携带各段存在标志与段 chevron 图表字段 - StatusItemController+Popover buildSectionedCard:按 NSMenu openAIWebContext/hasOpenAIAPIUsageSubmenu 同判定决定是否拆段,各 chart 字段完全照抄 makeUsageSubmenu/addMenuCardSections 条件 - PopoverRootView 新增拆段渲染路径 sectionedCard(_:):Header+Usage/Storage/Credits/ExtraUsage/Cost 五段顺序与 Divider 规律逐行对齐 Menu.swift:1283-1341 - ChartSectionContainer 可复用段容器:content wrap + 可选 chevron(topTrailing) + hover 高亮 + 点击触发 popover - PopoverCostCompactRow:title "Cost" + subtitle 详情行,对齐 NSMenuItem title+subtitle 观感 - popoverChartEntries 收缩为仅返回 usageHistory/zaiHourly(原版仅有的两个独立入口行) - 拆段时 plan.showBuyCredits 置 false,由 sectioned.showBuyCredits 接管避免重复渲染 - 测试:SectionedCard 纯逻辑测试(空 store 无拆段、拆段时顶层 showBuyCredits=false、stacked 不拆段) + chartEntries 收缩验证(entries 只含 usageHistory/zaiHourly) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same mechanism as NSMenu's MenuCardHighlighting: section views consume @Environment(\.menuItemHighlighted) and switch to highlight colors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the single global @State presentedChart + root-level .popover(item:) with per-row @State isPresentingChart + .popover(isPresented:, arrowEdge: .trailing) on each trigger view (ChartSectionContainer, ChartEntryRowView, OverviewRowView). Each view now receives makeChartView: (PopoverChartKind, CGFloat) -> AnyView? injected from PopoverRootView; the two .onChange(of: viewModel.selection/isVisible) that were manually clearing the global state are removed — SwiftUI diff destroys local @State automatically when rows are replaced on provider switch, and child popovers disappear with the host NSPopover on panel close. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
实现 HoverChartPopover ViewModifier,复刻 NSMenu 子菜单 hover 级联行为: 悬停 180ms 自动打开图表 popover,离开后 350ms 宽限(穿越间隙不闪关), 点击立即打开保留。改造 ChartSectionContainer、ChartEntryRowView、 OverviewRowView 三视图移除内联 .popover,统一挂 modifier。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ssal Address review findings on steipete#1428: - Write lastMenuProvider from the active panel's selection before dispatching legacy menu actions (dashboard/statusPage/changelog and Buy Credits resolve their target provider through that field, exactly like NSMenu's menuWillOpen used to set it). The same resolution helper now also feeds MenuDescriptor.build so content and action targets agree. - performMenuAction now closes every popover (merged + per-provider), matching NSMenu's dismiss-on-action behavior in split icon mode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the thorough review — both findings were real bugs, fixed and pushed:
Real-behavior proof (flag on, scripted open/provider-cycle/close loops, @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
Summary
This PR adds an opt-in NSPopover-based menu that replaces the per-open NSMenu rebuild cycle with a single persistent SwiftUI hierarchy driven by
@Observablestate. It directly targets the architectural root cause behind the long tail of merged-menu hang reports (#1321 and the follow-up perf PRs): every menu open / provider switch currently rebuilds NSMenuItems, instantiates freshNSHostingViews, and synchronously measures them on the main thread (layoutSubtreeIfNeeded+fittingSize). The existing caches (width/height/switcher-content) only memoize measurements — the SwiftUI view construction itself is re-done on every interaction.With the popover implementation, switching providers mutates one
@Observableproperty and SwiftUI diffs the persistent tree.Off by default. Nothing changes unless the user opts in:
The NSMenu path is untouched when the flag is off (verified by the full test suite). A follow-up branch that removes the legacy NSMenu implementation once this has soaked is ready (
popover-cleanup, −15k lines) — happy to open it as a stacked PR if you want to go that way.What the popover implements (aligned with the NSMenu UI)
switcherShowsIconshonoredaddMenuCardSections(usage/credits/extra-usage/compact cost rows with per-section chart drill-downs)OverviewMenuCardRowView, row tap switches provider, chevron opens the per-provider chartMenuDescriptor.builddata source, dispatched through the existingselector(for:)mapping with the active panel's provider context written tolastMenuProviderfirst; in-flight login disabling viaswitchAccountSubtitle/codexAddAccountSubtitleselectedMenuProvider/mergedMenuLastSelectedWasOverview), open-time stale-provider refresh, storage-footprint refresh, icon animation freeze while the panel is visiblemenuItemHighlightedenvironment the section views already consume; manual accessibility tree (labels/hints incl. shortcut descriptions)Review follow-ups addressed
Both ClawSweeper findings were real and are fixed in the branch:
onAction/onBuyCreditsnow writelastMenuProviderfrom the active panel's selection (same resolution that feedsMenuDescriptor.build, so menu content and action targets always agree) before invoking the legacy selectors. Dashboard/status/changelog from a Claude panel now route to Claude.performMenuActionnow closes every popover (merged + per-provider) for non-quit actions, matching NSMenu's dismiss-on-action behavior.Scripts/package_app.shwere dropped from the branch;Scripts/is now untouched (git diff main -- Scripts/is empty).Proof (terminal output, flag on, built from this branch)
Scripted run against the freshly built bundle: AppleScript opens the popover, cycles providers with →, and closes with Esc, 4 rounds, while
sampleprofiles the process.Zero NSMenu rebuild/measure frames on the main thread during open/switch loops; the popover code path is what's executing. I can additionally post redacted screenshots (with
hidePersonalInfoenabled) on request.Before / after benchmark (same machine, same build, instrumented)
To quantify the win I temporarily lowered
slowMenuOperationThresholdto0and printed everylogMenuOperationDurationIfSlowsample, then drove the app with AppleScript andsample(instrumentation was on a throwaway branch, not in this PR).populateMenu)populateMenu/makeMenuCardItem/measuredHeight/hostedSubviewFittingHeightframes on the main thread during open + provider-switch loops (sample)Honest framing: the NSMenu path is not slow on every open — the existing caches make a steady-state reopen ~0.1 ms. The cost lands on every content change: first open, provider switch, post-refresh redraw all synchronously rebuild NSMenuItems, instantiate fresh
NSHostingViews, and measure them on the main thread (≈33 ms each on an idle machine; this scales up under memory pressure or with deep content such as the Zai hourly chart, which is where the recurring "switching is laggy" reports come from). The popover removes the rebuild step entirely — the view tree is built once and switching providers mutates one@Observableproperty — so that per-change cost drops to 0, as the profiling above shows.Commands run
swift build/swift test— 3493 tests, 404 suites, all passing (flag off and on)make check(SwiftFormat + SwiftLint--strict) — 0 violationsScripts/compile_and_run.sh— bundle-level validation, app stays runningsampleprofiling (output above)Notes for review
Sources/CodexBar/Popover/plusStatusItemController+Popover.swift; shared card/chart/descriptor code is reused, not duplicated.🤖 Generated with Claude Code