| title | CodexPlusBar Learnings | |||
|---|---|---|---|---|
| status | active | |||
| last_updated | 2026-03-18 | |||
| purpose | Keep non-obvious project knowledge in one place so future agents can avoid repeated tool mistakes, API misunderstandings, and fragile fixes. | |||
| update_policy |
|
- How To Use This File
- How To Update This File
- Build And Test Commands
- Tooling Notes
- Backend API Notes
- Swift And Testing Pitfalls
- UI And Data Flow Notes
- Read this file before you change networking, session handling, account loading, dashboard UI, or tests.
- Treat this file like a workshop notebook: it is for practical lessons, not polished documentation.
- If a note becomes outdated, add a new note that corrects it. Do not silently delete the old lesson unless it is clearly wrong and harmful.
- Add new notes only when they would save the next agent time or prevent a bug, bad tool call, or wrong assumption.
- Append under the closest section with a date prefix like
2026-03-18. - Keep each note short and concrete:
- problem
- fix
- prevention
- Good example:
2026-03-18: /wham/usage is account-scoped by ChatGPT-Account-ID, so do not add workspace-switch fallback logic for multi-account limit fetches. - If you learn something from a failed build, test, or tool call, record the exact trap and the safer pattern.
- 2026-03-18: Use
xcodebuild test -project CodexPlusBar.xcodeproj -scheme CodexPlusBar -destination 'platform=macOS'as the main verification command for this repo. - 2026-03-18: For normal local verification in this repo, prefer the Makefile entry points the project already wraps:
make test,make agent-verify, andTRACE_PRIVATE_API=1 make build-and-run-background.
- 2026-03-18: Proxyman is useful for confirming private ChatGPT web calls in this app. It is especially helpful for checking real request headers like
ChatGPT-Account-IDand matching them to returned account data. - 2026-03-18: When you need quick repo-wide search here,
rgis enough for most code and test discovery. There is no existing root onboarding file beyondAGENTS.md, so future onboarding rules should live there. - 2026-03-19: Pencil MCP flagged
SF Pro Display,SF Pro Text, andSF Monoas invalid font families during the menubar/app redesign, but the canvas still rendered readable fallback text. For Pencil work here, always verify with screenshots instead of trusting font validation warnings alone. - 2026-03-19: For multiple bundled font families in this macOS app, point
ATSApplicationFontsPathat the top-levelFontsfolder and also register.ttf/.otffiles at launch. If Info.plist only points at one subfolder, Manrope and Cormorant can fall back silently and the redesign loses its intended typography. - 2026-04-20: The persistent WebKit profile API names differ in Swift from many docs/examples. Use
WKWebsiteDataStore(forIdentifier:)to open a profile store andWKWebsiteDataStore.remove(forIdentifier:completionHandler:)to delete one. Do not search fordataStoreForIdentifierin Swift code and assume it will compile. - 2026-04-21: Sketch MCP
document.save(...)can write the.sketchfile to the requested path even when the returneddoc.pathstill shows the old unsaved/iCloud location. After saving through Sketch MCP, verify the target file on disk instead of trusting the reported path string. - 2026-04-24: The local screenshot skill helper can fail on this Mac with a Swift JIT
NSScreensymbol error even after Screen Recording permission is granted. For visual verification, fall back to macOSscreencapture -x <path>and inspect the PNG withview_image. - 2026-05-28: Chrome launched with a custom temporary
--user-data-dirand--remote-debugging-port=0exposes a browser DevTools websocket from/json/versionthat accepts bothBrowser.getVersionandStorage.getCookies. Use a dedicated or temporary profile for probes; never read the user's default Chrome cookie database. - 2026-05-28: Do not keep
--remote-debugging-port=0, DevTools, or headless flags on the visible ChatGPT sign-in browser. Cloudflare can treat those as automation signals and loop the human check. Launch normal visible Chrome first, then use a short hidden CDP launch only after the user clicks Sync. - 2026-05-28: Touch ID in Chrome unlocks a passkey from the current passkey store; it is not the passkey store itself. A dedicated
--user-data-dircannot see passkeys saved only in the user's normal Chrome profile, so OpenAI may fall back to "security key" UI even on a Mac with Touch ID. Use Apple Passwords/iCloud Keychain, Google Password Manager sync in the helper profile, or a different-device passkey flow. - 2026-05-28: Do not switch CodexPlusBar's cookie source to the user's normal Chrome profile, even on a personal Mac. Chrome 136+ blocks remote debugging for the default data directory, so a normal-profile sync would require cookie database scraping, a browser extension, or UI scripting. Keep the dedicated profile for CDP import; use normal Chrome only as a manual setup aid.
- 2026-07-08: Google Chrome Sync sign-in accepts
service=chromiumsyncwithlogin_hint, but returns HTTP 400 ifcontinuepoints at ChatGPT. Open the Google account hint URL and the ChatGPT login URL as separate Chrome tabs instead of nesting ChatGPT inside the Google sign-in URL. - 2026-05-28:
gh release create --targetcan reject a short commit hash withRelease.target_commitish is invalid. Use the full 40-character SHA for snapshot releases, then fetch the created tag locally withgit fetch origin tag <tag>. - 2026-05-29: The default archive/DMG path can produce an ad-hoc signed app and an unsigned DMG because the project has no Release Developer ID settings yet. For notarized snapshots, copy the archived app to a packaging workspace, sign it with
Developer ID Applicationplus--options runtime --timestamp, create the DMG from that signed copy, sign the DMG, then notarize and staple before checksum/upload. - 2026-07-07: For release checksums, generate the
.sha256file from insidebuild/distwith the DMG basename, then verify from that same folder. If the checksum file storesbuild/dist/...,shasum -cfails aftercd build/disteven though the DMG itself is valid. - 2026-07-09: Do not run
xcrun stapler stapleandxcrun stapler validatein parallel.validatecan race before the ticket is attached and report a false failure; runstaple, thenvalidate, thenspctl. - 2026-07-17:
notarytoolKeychain profile names are case-sensitive. This Mac's working release profile isCodexPlusBar-notary;CodexPlusBar-Notarydoes not exist, so check the exact saved name before treating notarization credentials as missing.
- 2026-03-18:
/backend-api/accounts/check/v4-2023-04-27is the source of truth for multi-account catalog data. It provides account names, plan types, entitlement dates, accessibility flags, deactivation state, andaccount_ordering. - 2026-03-18:
/backend-api/wham/usageis account-scoped by theChatGPT-Account-IDheader. The URL does not change per account; the header decides which account limit payload you get back. - 2026-03-18: Do not overwrite an explicit
ChatGPT-Account-IDin the request signer. If the caller already set it, preserve it. - 2026-03-18: In the native menu bar app, header-only
wham/usagerequests can still return the currently pinned account for every response. The working fix was to switch the app session to each target account first, then callwham/usage, then restore the original account at the end of the sweep. - 2026-03-18: The real browser workspace switch call is a cookie-only
GET /api/auth/session?exchange_workspace_token=true&workspace_id=...&reason=setCurrentAccount. Do not addAuthorization,ChatGPT-Account-ID, orOAI-Languageto that request. Decode the switch response directly and verify the returned account before calling/wham/usage. - 2026-03-18: ChatGPT workspace switching in the native app also depends on the
_accountcookie, not justworkspace_idin/api/auth/session. Set_accountto the target account before each switch and before restore, or the backend can keep the session pinned to the old account. - 2026-03-18: The
timezone_offset_minquery foraccounts/checkmust follow the browserDate.getTimezoneOffset()sign convention. Example: Asia/Jakarta should send-420, not420. - 2026-03-18:
accounts/checkcan return duplicate logicalaccount_idvalues under different entries. Normalize and merge account catalogs before building dictionaries or starting per-account limit fetches, or the app can crash onDictionary(uniqueKeysWithValues:). - 2026-04-07: When
/backend-api/wham/usageis slow, plain timeout text is not enough to debug. Emit trace-gated structured logs (event=limit_fetch_timeoutandevent=limit_fetch_slow_success) with account, sweep index, checkpoint (switch/request/decode/restore), elapsed/timeout thresholds, and session+_accountcookie before/after. Prevent future guesswork by greppingevent=limit_fetch_before changing timeout values. - 2026-04-07: Timeout traces can look "missing" when they depend on stdout capture and trace env flags. Keep slow-success logs trace-gated, but always persist timeout diagnostics to a durable local file (
~/Library/Application Support/CodexPlusBar/Logs/limit-fetch-timeouts.logby default, overridable withCODEXPLUSBAR_TIMEOUT_DIAGNOSTICS_LOG) and pass the same typed diagnostics into the account card message so users can see exact timeout context without opening logs. - 2026-04-07: If multi-account refresh fails only at
checkpoint=switcharound the same cutoff (for example ~20s), the trap is a single shared timeout covering both switch and usage phases. Fix by splitting timeout budgets per stage (switch,request,restore) and set a longer switch request timeout on/api/auth/session?exchange_workspace_token=trueso URLSession does not cancel early. Prevent regressions by asserting stage-specifictimeout_msvalues and explicit switch request timeout in tests. - 2026-04-20: Even in the Plus-only flow,
AuthSessionService.fetchCurrentSessioncan still hit/codex/settings/usageafter/api/auth/sessionwhen the cached auth context does not yet haveclientVersion, account, or token details. In tests, either seed a fallback auth context or mock the bootstrap HTML too, or the request sequence looks wrong even though the runtime path is correct. - 2026-05-28: ChatGPT can store the auth session as chunked cookies like
__Secure-next-auth.session-token.0and.1. Treat those numeric chunks as a valid session-cookie family and import all ChatGPT cookies; otherwise Chrome can be visibly logged in while the native app still reportsunauthorized. - 2026-07-15: A ChatGPT session cookie can change between chunked (
.0,.1) and unchunked forms after a new login. Replace the profile's existingchatgpt.comcookies during Chrome import instead of adding to them, and never fall back to the sharedHTTPCookieStorage; mixed old/new cookie families make/api/auth/sessionreturn logged out even while Chrome is visibly signed in. - 2026-04-21: Menu bar entitlement expiry should come from
/backend-api/accounts/check/v4-2023-04-27, not from the auth session expiry or thewham/usagepayload. Fetch the catalog after a successful usage refresh, match by account ID, and preserve the last known expiry if the catalog request fails so a transient metadata miss does not erase a useful expiry line. - 2026-04-24:
accounts/checkcan return the current personal account twice: once under the UUID key and once underaccounts.default. Thewham/usageaccount ID can matchaccount_owner_idoraccount_user_id, not onlyaccount.account_id, so entitlement expiry matching must keep those aliases and fall back to thedefaultentry before showingExpiry unavailable.
- 2026-03-18: In Swift Testing, avoid nesting
#require(...)inside another#require(...)expression. Split the values into separate local constants first, or the macro can recurse badly and fail compilation. - 2026-03-18: Swift 6 can struggle with type inference around
Dictionary(uniqueKeysWithValues:)when the input comes fromcompactMap. If inference fails, give the dictionary an explicit type like[String: WorkspaceLimitSnapshot](...). - 2026-03-18: For this repo, bounded async loading was safer to implement with chunked batches of
Task { @MainActor ... }than with the earlier task-group attempt that ran into Swift 6 isolation/compiler trouble. - 2026-03-18: When testing dates from ISO strings, compare against a parsed ISO8601 date instead of hand-written epoch seconds. Manual epoch values are easy to get wrong and create fake decoder failures.
- 2026-03-18:
ChatGPTRequestSignernow gives/backend-api/wham/calls browser-style headers likeAccept: */*,Referer: /codex, andX-OpenAI-Target-*. If signer tests still expect the older JSON usage-page headers, the tests fail even though the runtime fix is correct. - 2026-03-19: This repo treats warnings as errors, so old single-parameter SwiftUI
onChangeclosures now fail macOS builds. Use the two-parameter macOS 14+ form during UI refactors, or the test/build step will stop on a deprecation instead of a runtime bug. - 2026-04-20: In AppKit-hosted SwiftUI tests here, a styled SwiftUI
Buttonmay not expose its visible label throughNSButton.title. For disclosure regressions, verify the mounted content state directly (for exampleWKWebViewpresent or absent) and use a small injected initial state when needed instead of scraping button titles. - 2026-04-20: In AppKit-hosted SwiftUI tests here, static
Textinside styled cards or sidebar rows may not appear asNSTextFielddescendants at all. For UI copy/layout contracts, prefer testing a shared presentation model or explicit accessibility value instead of scraping sidebar labels from theNSViewtree. - 2026-04-20: For narrow MenuBarRootView regressions where AppKit view scraping still hides styled row content, a lightweight structural check on
type(of: rootView.body)can confirm an old inline-card view is gone. Use that only for small containment regressions; keep real behavior assertions in shared presentation tests first. - 2026-04-21: The same view-scraping trap also applies to inline
Savebuttons insideProfileManagerWindowView. Even when the buttons are visible in SwiftUI, the hostedNSViewtree here may expose neitherNSButton.titlenor a matchingNSTextField. Prevent flaky tests by asserting editable-field structure or shared presentation/state, not visible button copy. - 2026-04-28:
xcodebuild -only-testing:CodexPlusBarTests/...can compile this Swift Testing target but run0 tests, which is a false green for focused checks. Usemake testfor final verification, and confirm the Swift Testing summary shows the expected test count. - 2026-07-08: A failed
make testcan report the Swift Testing failure and then leavemake/xcodebuildhanging. Confirm the failure first, then kill only the stuckmake/xcodebuildPIDs before continuing; do not leave the session open. - 2026-07-15: Swift 6 could not type-check one chained
enumerated+compactMap+sorted+mapemail-ranking expression. Build the typed ranked array in a small loop, then sort and map in separate steps; this compiles faster and keeps ranking logic readable.
- 2026-03-18: The app now uses a dashboard-first account flow, not a selection-first workspace flow. Keep names and expiry dates visible for all returned accounts, and keep raw account IDs internal where possible.
- 2026-03-18: Partial limit failures should stay local to the affected account card. If
accounts/checksucceeds, the dashboard should still render even when one or morewham/usagecalls fail. - 2026-03-18: A full multi-account limit sweep can take longer than the old 12-second controller stage timeout. Do not wrap the whole dashboard load in one timeout or wait for every account before updating the UI. Load the current account first, update cards progressively, and map cancellations to friendly timeout/interrupted messages instead of showing raw
cancelled. - 2026-04-06: The account catalog refresh can also exceed the old shared 12-second stage timeout even when the request is still in flight. If users see
Loading the account list took too long, inspect the unified log for a-999cancellation around 12 seconds before blaming ChatGPT; the safer fix is separate stage budgets soaccounts/checkis not cut off as aggressively as session validation. - 2026-03-18: The compact menu bar surface should summarize urgency and counts, while the account window holds the full scrollable card grid and the embedded session repair web view.
- 2026-03-18: In the account window, a vertical
ScrollViewinsideVSplitViewcan collapse to intrinsic content width unless the inner dashboard stack is explicitly framed to the available width. When that happens,LazyVGridcards and the session web view squeeze into unreadable narrow columns. - 2026-03-19: In the menu bar panel,
CodexCardinside aScrollViewcan also hug its intrinsic content width. When a compact card should fill the panel, explicitly frame the card or its root content tomaxWidth: .infinity, or the panel shows large empty gutters even though the outer window is wide enough. - 2026-03-19: A
MenuBarExtra(.window)panel can still shave the right edge even when the NSWindow is already at the intended size. The practical fix here was to keep the outer window at484 x 560, then reserve an explicit outer chrome inset forCodexShelland give the shell content an explicit measured frame inside that surface instead of trusting implicitZStackpadding proposals. - 2026-03-19: If the menu bar panel still truncates on the right after the outer window is correct, inspect the live accessibility bounds before widening anything. In this app the real trap was a
ScrollViewcontent area growing wider than the484ptpanel plusfixedSize(horizontal: true)badge rows, so the fix was to lock the menu content to the computed inner width and let the left text truncate before the badges. - 2026-04-20: On this Mac, the
MenuBarExtra(.window)status item could render and receive clicks without ever realizing a popup window, even with a minimal static popup body. The reliable fix was to keep the SwiftUI panel view but host it through an AppKitNSStatusItemplusNSPopoverbridge instead of relying on the SwiftUI popup scene. - 2026-03-18: Hide expired and deactivated accounts before creating dashboard items and before starting per-account limit fetches. If you only hide them in SwiftUI later, the app still wastes
wham/usagecalls and can leave hidden rows stuck in a loading state. - 2026-03-18: For countdown text like
Reset in ..., useresetAtas the source of truth.reset_after_secondsis only correct at fetch time and goes stale if the UI stays open. - 2026-03-18:
TimelineViewinside aMenuBarExtralabel caused an invisible status item and multi-GB layout churn on macOS 26. Keep menu bar labels plain and drive time updates through explicit observed state instead. - 2026-03-19: The embedded ChatGPT sign-in web view can navigate through
/auth/and usage pages during normal browsing. Do not treat those navigations as account-data refresh signals, or the app will ask ChatGPT for limits far more often than the 5-minute timer. - 2026-03-19: For ADHD-focused dashboard work, do not create cards, repeated facts, or header copy that add no decision value. Keep shared facts like refresh time once at the top, keep expiry visible where it helps account choice, and remove decorative summary blocks that only increase cognitive load.
- 2026-04-07: The app should now show only
5Hand7Dwindows. Keep decoding tolerant if ChatGPT still sendscode_review_rate_limit, but do not surface that window in models or UI to avoid stale, misleading dashboard sections. - 2026-04-07: A background refresh can fail during
checkingSessionafter the app already has valid cards on screen. Do not forceauthStateback to.signingInfor every refresh; restore the previous presentation state when that first stage times out, and keep a longer default budget than 12 seconds because real/api/auth/sessioncalls can exceed it. - 2026-03-23: For menu bar priority selection, keep the chosen account ID in
@AppStorageat the view layer and let both the panel content and the status label read the same key. If only the panel owns that state, the menu bar text and the opened panel drift out of sync. - 2026-04-20: In this AppKit-bridged menu bar app,
@AppStoragein the SwiftUI panel does not automatically refresh the customNSStatusItemlabel. When the top label depends on a stored preference, observeUserDefaults.didChangeNotificationin the status-item controller and recompute the title there, or the panel selection changes but the menu bar text stays stale. - 2026-03-24: For a transparent custom account window title bar on macOS, do not stop at
titlebarAppearsTransparent = true. Add.fullSizeContentViewto theNSWindowstyle mask too, or macOS can leave a visible top chrome seam that looks like an app border bug. - 2026-03-24: If you want real desktop-through glass in that transparent title-bar strip, lowering the shell alpha is not enough. Use an AppKit
NSVisualEffectViewwith.titlebarmaterial and.behindWindowblending for the top strip, then keep the main shell body separate and opaque enough for readable content. - 2026-03-24: If the goal is a truly clear native title bar, do not paint your app shell or custom glass into the title-bar height after enabling
.fullSizeContentView. Leave that strip empty and start the styled body belowcontentLayoutRect, or the title bar turns into fake content again. - 2026-03-24: If a 1px dirty line still remains after moving the body below a clear title bar, the problem can be the body shell itself: clipped backdrop and shadow pixels touching the transparent strip. Bury the body upward by one device pixel and stop shadow bleed at the top edge.
- 2026-03-24: The new SwiftUI Liquid Glass APIs (
glassEffect,Glass.clear,Glass.tint) are macOS 26+ only. In this macOS 14-targeted app, gate them with availability and keep an AppKit title-bar visual-effect fallback for older systems. - 2026-03-24: If a transparent title-bar glass cap still looks rectangular, the problem can be the glass layer shape, not the window. Clip the title-bar material itself to a top-rounded shape that matches the shell radius, and keep the tint very light or the glass starts reading like a dark slab.
- 2026-04-20: A custom
NSStatusItemlabel can disappear in the live menu bar even when the button still has a real title if you force a custom title foreground color throughattributedTitleandcontentTintColor. Let the menu bar text use the system contrast color instead, and keep any styling limited to font or symbol choice. - 2026-04-20: In the menu bar panel, do not wrap a whole profile row in one outer
Buttonif the row also needs visible inline actions like copy, email, or pin. Keep one primary button region inside the row and render secondary controls as sibling buttons, or SwiftUI composition and interaction become fragile. - 2026-05-20: For visual checks of the LSUIElement menu bar app, clicking the panel footer icon can close the popover before the manager window appears. If that happens, post the distributed reopen notification
com.linda.CodexPlusBar.reopenAccountWindowto openCodexPlusBar Profilesreliably. - 2026-07-07: For theme visual checks, do not trust a nearby menu bar label without confirming the owner app. Other status items can look similar. Use the distributed reopen notification
com.linda.CodexPlusBar.reopenAccountWindow, then verify the window owner isCodexPlusBarbefore taking screenshots. - 2026-07-07: When adding Everforest Medium and Soft presets, do not use quiet gray as useful support text on raised surfaces. WCAG checks showed it can fail on bg2 and bg3 layers, so keep useful secondary text on
fg/strongTextand reserve gray for truly quiet metadata. - 2026-07-07: In an LSUIElement menu bar app, adding a SwiftUI
Settingsscene is not discoverable enough by itself. Add a visible compact-panel settings doorway, such as a labeled gear button, and keep the normal Settings scene as the destination. - 2026-06-06: When the UI needs urgency sorting by expiry, do not reorder
PlusProfileController.profilesdirectly. That array also drives selection, move-up/move-down, and persistedsortOrder, so changing it turns a temporary display preference into saved data churn. Keep a separate display-only sort helper and apply it in the sidebar or menu bar view layer. - 2026-07-07: Do not force theme changes through
.id(...)onCodexShellor whole-window roots. It makes SwiftUI throw away view identity, which can feel like a slow refresh and can reset scroll, selection, or draft state. Use small@AppStorage/environment theme context updates so only color tokens redraw. - 2026-07-07: Keep Everforest data accents separate from status accents. Use green for healthy usage data, aqua for success, blue for info, yellow/orange/red for warning-to-danger data, and strong text for light-theme dense values. This avoids color meaning drift while keeping WCAG and delta L* checks useful.
- 2026-07-07: In Everforest Light, do not use the strongest dark neutral as the global
primaryText. It makes labels, values, and button text collapse into one black/gray mass. Keep strong text for headings and dense values, use a WCAG-safefgfamily for labels, and darken accent hues only for readable action/status/data text. - 2026-07-07: Card background changes must cover both shared
CodexCardand custom row-card fills such asProfileSummaryRowandEmailToolsSidebarRow. Tests can protect the token path, but still openCodexPlusBar Profilesand screenshot it because custom row cards can bypassCodexCard. - 2026-07-07: Raw Everforest accent colors can fail WCAG normal-text contrast after card fills move to
bg0, especially red on Dark Soft. Route small accent text throughreadableAccentToken(...), then keep tests on the real card fill and main surfaces. - 2026-07-07: Tiny menu-bar text can still look washed out even when primary text passes WCAG if useful roles such as
quietTextare skipped and disabled controls use whole-view opacity. Add quiet/disabled text to WCAG checks, add a delta L* text guard, and change disabled controls to readable text/fill tokens instead of dimming the entire control. - 2026-07-08: Profile tags need a tag-specific tone layer, not only generic
CodexStatusTone. Active and Pending can be technically different but still too close in Everforest dark compact chips, so pair status color with SF Symbols and test tag text against its own selected fill with WCAG and delta L*. - 2026-07-08: Profile tag product semantics override the default Everforest pending/attention taxonomy here: Need action is red, Active is green, and Pending is yellow. Keep that mapping in
CodexProfileTagToneand protect it with tests so future theme cleanup does not make Need action and Pending look the same again. - 2026-07-08: Do not reuse profile tag text color as the tag border color. It makes Active, Need action, and Pending chips feel boxed-in and too loud. Keep tag meaning in the icon, label, and tinted fill, then keep the border as a low-contrast quiet edge with tests proving text WCAG/delta L* still passes.
- 2026-07-08: Empty Profile Manager detail fields should look empty. Placeholder copy like sample phone numbers, password hints, or note examples can read as saved data and makes users re-check the form. Keep the field labels visible, but leave the field body blank until real saved text exists.
- 2026-07-09: The Profile Manager OTP panel should default to a hidden safe state. Do not mount the one-second
TimelineViewor generate/validate TOTP until the user clicksShow OTP; reset reveal on profile change, window close, or 2FA key edits, and protect hidden/revealed presentation states with tests. - 2026-07-09: Hidden OTP should look redacted, not say
Hidden. UseProfileManagerOneTimePasswordPresentation.isCodeMaskedplusCodexTheme.oneTimePasswordMaskFillTokenso the code slot renders a quiet mask, avoids extra reading work, and stays inside the WCAG/delta L* theme-test path. - 2026-07-16: For reusable phone choices on macOS, an editable
NSComboBoxfits the existing SwiftUI form. Filter its item list fromcontrolTextDidChange, compare digit-only values so punctuation does not block matches, and reset the filter after selection or profile sync so reopening the dropdown shows every saved number.