Repository navigation
Conversation
Co-Authored-By: Claude <[email protected]>
Co-Authored-By: Claude <[email protected]>
Co-Authored-By: Claude <[email protected]>
Co-Authored-By: Claude <[email protected]>
Co-Authored-By: Claude <[email protected]>
Co-Authored-By: Claude <[email protected]>
Each editable list (presets, the freeze list, agent apps, tmux targets) now opens with a search field that filters its rows and a + button beside it. For app lists the + opens a picker of installed apps with their icons, searchable by name or bundle identifier, which also adds a typed bundle identifier that no installed app has. App rows show the app's icon and mark ids with no app installed.
The Low Power Mode floor, the end floor and the tmux nudge delay were steppers. Each is now a small box that takes digits only, with its unit after it, and sets the value on Return or when it loses focus, pulled into the setting's range. The floors still keep their order through the Config setters, and the box shows where the setter left the value.
The password field had no visible box, the name saved as you typed and the password had its own Save button. Both are now bordered fields, and one Save button under the section writes the name to config.json and then stores the password under it. The button lights up only while the name differs from the saved one or the password differs from what the keychain holds; the password the window loads counts as saved.
Removing the sidebar toggle from the sidebar column made the column ignore navigationSplitViewColumnWidth and sit at 144 pt, which cut off "Battery & Thermal". The toggle is now removed on the split view, and the column opens at its 230 pt ideal width.
Long gray paragraphs under rows and sections now sit behind an info button beside the setting or section title: hovering shows a one-line help tag, clicking opens the full text in a popover. Warnings, live status and one-line descriptions stay on the pane, as the HIG keeps warnings out of popovers. The popover text lives on each search entry, so the sidebar search still finds words that are only in a popover.
…g battery charging An extend leaves the fields editable; its completion now clears them only if they still hold what was submitted. On the adapter with the battery draining, the power row names the adapter and the draw instead of "Charging".
A search pick cancelled by a newer one no longer clears the newer highlight. A number box takes one digit more than its range's top, so 100 in 0...95 becomes 95, not 10. The hotspot status reads the saved password, not a draft. Save's steps move into SettingsView.hotspotSave, with tests that run them against a config and an in-memory keychain. The app picker says "No apps found".
Everything above the footer sits in a scroll view that takes its content's height up to a cap: the screen's usable height less room for the footer and the arrow. A long list of warnings now scrolls, and Settings and Quit stay on screen.
"Lid close", "Agent apps" and "Battery & thermal", as the writing rules ask. The lid-close notice now says to open the Lid close pane.
…ts field A grouped form right-aligns the text of a field that carries its own label, so .multilineTextAlignment(.leading) did nothing. The name and password fields now sit in LabeledContent with their labels hidden, which keeps the text at the left. The line about using the name from the Wi-Fi menu moves from under Save to under the name field.
…dited during the load A load that kept the user's draft also skipped recording what the keychain holds, so the status said the password was missing. The load now records it unless a save began since, which may have replaced it.
SettingsView.runHotspotSave does the whole press: the name to config.json, then the password to the keychain under it or a clear, the field's saving state, and the failover's report. The view calls it, and tests now run it against a config and an in-memory keychain for a new name, a new password, an emptied field and nothing changed.
The line shows in every running session; the journal and the missing live re-check are how the app knows, not something the popover needs to say.
The countdown already says the Mac is being kept awake; only the two mismatches (a session not holding sleep, a hold with no session) are worth a row, and both stay warnings.
Eight presets wrapped the chips onto a second row. Four fit one row with Custom. A config.json that already lists its presets keeps them.
… reads 0 A start read the bit back once; powerd or another tool could set it back to 0 later in the session and a closed lid would then sleep the Mac with the popover none the wiser. The read takes no lock; only a 0 takes the transaction and runs disablesleep 1 again. A set that fails shows "Sleep is not held" and posts once per session; the next check retries.
The row reads the Network pane's verdict (HotspotSetupStatus): off with no hotspot name, needs the password, needs Location access, or ready. The password is checked with the same prompt-free keychain peek as the Network pane's recheck, at launch and on each popover open. The row gives way to the hotspot password warning, which already says what is wrong.
The merge kept the base's status item, menu and Settings, so #118's popover code and tests referred to names the base does not have. - Bring back Motion.countdownTransition and the base's HotspotPasswordField (buttonTitle, no isSaved), which the base's Pills and one-page Settings use. #118's HotspotSaveTests went with its Settings save button. - Give the popover its own helpers where the base already used the name: MenuBarModel.popoverInput for the fields, StatusLines.actionRows, WiFiStatusName.row, StatusMenu.popoverItems and openSettingsFromPopover. The menu keeps StatusMenu.items, so no call can reach the other list by its labels. - Keep the base copy of each test #118 changed and add #118's version beside it, named "InThePopover" where the names clash. Drop the three that only repeated a base test, and #118's Motion.tick test: the base has no per-second countdown animation (1cd9d3a). - Add CountingStatusSource and MainActor marks the base's isolation needs. The popover is not wired to a style yet, so its tests do not pass at this commit.
Brings in 934c5a8 ("Ends at" for an end-time countdown in VoiceOver and Minimal), a635fe2 (the slots test waits on a deadline), 969d24d (a late Minimal or Compact-ring release ends nothing) and 751fc19 (the releasing.md wording for those holds). Git merged the files cleanly. a635fe2's waitUntil in UIStatusTests is the same helper #118 added to that class, so #118's copy is dropped and its tests use the base's.
The base keeps #147's one-page Settings: no sidebar, panes or search. The merge left #118's code for its paned Settings in the tree, unused. - Remove SettingsPanes.swift (panes, search, anchors, help text), EditableList.swift, InfoButton.swift and NumberBox.swift, and their tests (SettingsSearchTests, EditableListTests, NumberBoxTests). - Keep HotspotSetupStatus, which the popover's failover row reads: it moves to HotspotFailoverRow.swift, and its three tests move to HotspotFailoverRowTests unchanged. The comments no longer point at a Network pane.
#118 replaced the pills with its popover. On this base the pills, Compact and Minimal stay as they are, so the popover becomes a fourth style, stored as "popover" and chosen in Settings like the others. - The status bar button takes clicks only in Popover style. In the other styles it has no action and the SwiftUI view takes the clicks, as on the base, so their right-click menu and #93's press handling are unchanged. Opening the popover, its chips, Start and hold to end all check the style. - Popover's bar is the eye and, during a session, a short countdown in the countdown style: the time left to the minute, or the end time. It reads only the text it shows, so it is redrawn once a minute and never every second. The "Show the countdown in the menu bar" switch turns it off. - A style change closes the popover, removes its click monitors and the button's action, and redraws a pending start's countdown in the new style's form. A value typed in the popover goes to the pills model, as one typed in the Compact panel does, so a refused start after a switch to Pills reopens the pills on it. - A hold in the popover ends the session only while the popover is up in Popover style. The ring is turned off once the popover closes or an end is on its way, so a hold that outlives the popover ends nothing. - The popover gives focus back to the app the user was in when Esc, the item or a style change closes it. A close because focus went elsewhere leaves focus there, as the Compact panel does (#143). #118's popover tests now run in Popover style. Tests that pinned the bar as the eye alone now pin the eye and the short countdown.
A start or extend refused while an end is pending puts up "start refused: the previous session is still being ended; retrying shortly" or the extend's line. Nothing took either down when the retry finished the end or left it to the recovery agent. The line stayed until the next success, so the popover and the Compact panel kept saying the end was being retried after the session was gone and sleep was back. The start line on main does the same. end() now clears lastError where it clears pendingEnd, and only while lastError is still one of the two refusals. A newer line, the end's own failure included, stays. RecoverySafetyTests: an extend refused for an end waiting on a busy lock, and a start refused for an end that took the session down but could not restore sleep, both leave the popover's rows and the Compact panel once the retry restores sleep. Without the clear both fail. A retry whose disablesleep 0 fails and hands the end to the agent keeps its "could not restore sleep" line. An unconditional clear fails that test. StillRunningCommandTests used the start refusal as the newer line the command's exit must leave alone, and expected it to outlast the end that finishes after the exit. That end now takes it down, so the test posts a newer failure of its own and checks that one. It still fails if the exit clears lastError. The spec says when the reason goes away.
testExtendQueuedBehindAnEndThatIsLeftPendingIsRefused ran with a 1 s lock timeout that both waits shared. On a loaded runner the extend could be asked for after the end was already pending, failing the setup check, or time out before the test freed the lock and report the lock instead of reaching performExtend's check. SessionManager takes the wait for the recovery lock as an init argument, acquireRecoveryLock, which defaults to RecoveryLock.acquire. Harness.makeManager passes it through. HeldLockWaits in TestSupport holds the next waits on AsyncGates and gives each up as busy, or tries the real lock once. The test now holds the end's wait until both requests are queued and then gives it up as busy. It frees the lock and only then lets the extend's wait take it. No timeout decides the order, and the test takes about 5 ms instead of 1 s. Without performExtend's check it still fails the same 6 assertions. If the manager skipped the injected wait, the test fails on the holds left, rather than hanging.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
#118's SleepHeldLine.warning, the popover's row for a session whose sleep check could not set SleepDisabled again, joined its two halves with an em dash. The repository's writing rule has no em or en dashes in user-facing strings. It now reads "Sleep is not held, so this session is not keeping the Mac awake". The right-click menu's sleep line, from main, keeps its text.
The live sleep check reads SleepDisabled without the lock, and on a 0 takes the transaction, reads again and sets the bit. Two ways out of that path said nothing: a transaction refused (the recovery lock busy, a command still running, an unreadable journal) and a read under the lock that failed. Each logged and returned, so after a read of 0 the popover kept no "Sleep is not held" row and no "Sleep is back on" notice posted, though sleep was known to be back on. Closing the lid then slept the Mac with no word. Both now put the warning up as a failed set does, once per session, unless an end was asked for meanwhile or the backstop restored sleep (maySetSleepAgain). A failed first read still knows nothing new and leaves the warning as it is; only a read of 1 or a set that holds takes it down. SleepCheckTests: a check refused for a busy recovery lock, then one whose read under the lock fails, each warn with one notification. A failed first read keeps the warning and a read of 1 takes it down. Without the change both checks leave sleepNotHeld false.
The popover opens on empty fields unless a refusal is showing. Closed and opened again while a Custom start was on its way, it cleared the fields, and the refusal only set its line. The retry then had nothing typed, so Start or Return asked for the default preset, not the time the user had typed. On a refusal the popover's fields get the submitted value back when a reopen since the submit cleared them, and the fields stay out. A value typed after the reopen is kept instead. PopoverStyleTests: a 2 h Custom start refused after a close and reopen leaves 2 in Hours and the retry starts 2 h; with 3 typed after the reopen the retry starts 3 h. Without the change the fields are empty and the retry starts the 4 h default.
The popover is .applicationDefined and closes on Insomnia resigning active unless the pointer is on the item, since a click on the item also resigns the app just before the item's action toggles the popover. A switch by keyboard (Command-Tab) with the pointer resting on the item resigned the app with no action to follow, and the popover stayed over the next app until another click. With the pointer on the item a resign now waits itemClickGrace (0.5 s) for that click. The action comes with the same mouse-down. If the popover from that opening is still up and Insomnia is still in the background after it, the popover closes. Off the item a resign closes it at once, as before. PopoverStyleTests: a resign on the item with no click closes the popover after the wait; a resign followed by the item's click and a second click that opens it again leaves the new popover up; a resign off the item closes it at once. Without the wait the first popover stays up.
StatusPopoverView's body builds its rows through statusItems, which read the battery's watts and time left from IOKit on every call. Typing in the fields or the header's minute redrew the popover and read both again on the main thread, with nothing about the battery changed. MenuBarModel.popoverPower holds the reading. The popover takes it when it opens and again when the battery's percent or charging state changes while it is up, and its rows read it. statusItems still reads IOKit when it gets no reading, as the tests that call it directly expect. UIStatusTests: two redraws of an open popover, one after typing, read the watts no more; a charging change reads them again; a change with the popover closed reads nothing. Without the change each redraw reads them.
On macOS 27 a hosting view's own hit test can return nil, so the check that Pills, Compact and Minimal take their clicks failed there although they hit-test as on the base. The test now puts the bar's host and a plain hosting view of the same bar in one window and checks that the two agree. On macOS 26 it still checks that the plain view takes the click, and the Popover style's nil holds on both.
The sleep check's read under the lock can fail after an end, a quit or the deadline came while it ran. Sleep goes back on then, so the check now asks maySetSleepAgain before it reports "Sleep is not held", as it does before a set. Before, it posted "Sleep is back on" for a session that was ending.
The value put back after a reopen came from the fields as they were when the start was sent, also for a chip. A time typed in Custom before a 1 hour chip then came back after the chip's start was refused, and Start retried that time instead. Only the popover's own Start or Return now puts its fields back.
A close gives focus back to the app the user was in, and that yield takes a moment to land. A reopen in that moment saw Insomnia still active and did not ask for focus. The yield then landed with the pointer on the item, and the popover closed half a second later, or before 8439392 stayed up without taking typing. A reopen after a yield now asks for focus again, so the request goes after the yield and Insomnia ends up in front.
Since ec548b6 the popover read the watts and the time left on open and on a change in the battery's percent or charging state. Both can change without either: after unplugging, IOKit's estimate goes from Calculating... to a time with the percent the same, and the draw moves. An open popover now reads them again every 10 seconds, and stops when it closes. A redraw still reads the stored values.
The sleep check's set can fail after an end, a quit or the deadline came while it ran, and it reported "Sleep is back on" for a session that was ending. Both of the set's failure paths now ask maySetSleepAgain first, as the locked read's does since b729140. A set still running keeps its bookkeeping either way.
38725cc made a reopen right after a close ask for focus again, but the close's yield could still land with the pointer already moved off the item into the popover. That resign closed the popover at once. The reopen now remembers that it asked for focus after a yield, and the first resign of that opening waits for the activation behind it, wherever the pointer is. A yield counts as on its way for one second. After that it has landed or been turned down, and an open while Insomnia is active asks for nothing, so windows are not parked for an activation that cannot raise them.
The run loop keeps a repeating timer. A controller released with the popover up left the 10 s power timer firing for nothing, since its weak capture only returned. The timer now invalidates itself when the controller is gone. A close, a style change and popoverDidClose already stopped it.
495be68 tied the close's yield to the next opening only. With two quick close and reopen cycles, Insomnia was in front at the second open, so that open recorded no app to give focus back to, its close yielded nothing, and the third opening forgot the first yield. That yield then landed with the pointer in the popover and closed it at once. Worse, the second close left focus with Insomnia instead of the app the user was in. Now every resign within a second of a yield waits for the activation behind it, and only the latest waiting resign may close. An open with a yield on its way gives focus back to that yield's app. The second is a guess at how long a yield takes to land; it normally lands well inside it. The activation asked for after a yield no longer parks windows. A yield the system turned down brings no activation to put them back, so they stayed hidden for half a second. A Settings window behind another app can come forward on a reopen this quick instead.
On a MacBook a click or a hold inside the popover closed it and gave focus back to the app behind it, so Custom, the fields and the end ring could not be used with the pointer. Most clicks inside also reached the global mouse-down monitor, carrying the popover's window number and a location in the popover's own coordinates. Read as screen coordinates, a click on the header at x 1231 came out at x 145, far outside, and the popover closed. The monitors now convert a click's location to the screen through its window, and a click on the popover never closes it. A local click with no window goes by where it is. A resign with the pointer in the popover waits as one on the item does, and closes only if Insomnia is still in the background after that. testAClickInsideThePopoverLeavesItOpen builds the copy the global monitor got; testAClickInAnotherWindowClosesThePopover adds clicks with no window. Both fail without this. On the MacBook, 8 of 8 trials of a click on the header and then Custom kept the popover open, from Finder and from TextEdit; before, 0 of 10.
On a MacBook Esc left the popover open unless a field had the cursor, and with only the time buttons out none does. `onExitCommand` runs only for a focused view. A local key monitor, up while the popover is, now closes it on Esc in the popover's window and gives focus back, as before. Esc while a field is composing text is left to the field. testEscClosesThePopoverWithNoFieldFocused sends Esc through the app with no field focused; it fails without this. On the MacBook Esc now closes the popover with Hours focused and with the time buttons only, from Finder and from TextEdit, and focus goes back to that app.
On a MacBook a click on Custom brought the fields out with no caret, Accessibility reported the popover itself as focused, and a typed 3 went nowhere. A debug build showed the focus set on the `customShown` change dropped at once: the fields come in with that same update and were not in the window yet. The focus is now asked for on the next turn. With that build Custom put the field editor in Hours, and a 3 typed from Finder or from TextEdit filled it and turned Start on. testCustomPutsTheCursorInHours pins where the cursor lands. This test process lays the popover out sooner and did not drop the focus before the change either, so the MacBook run is what shows the fix.
When a session ended with the popover up, the item shrank from 76 to 26 pt and the popover's arrow ended about 50 pt left of the eye. The popover follows the item's resize by itself, but the menu bar then moves the item to keep its right edge, and the popover stayed where it was. While it is up, a move or resize of the item's window now shows it again on the item, which puts the arrow back. testThePopoverFollowsTheItemWhenItMoves takes the popover off the item's window so the move does not carry it, as the menu bar's does not; it fails without this. On the MacBook the popover's centre now matches the item's after an end (1249) and after a start (1224) with it open; before, it stayed 50 pt off.
With a reopen right after a close, the reopen's activation lands only after the close's yield. A click on the item in that gap closed the popover with another app in front, so nothing was given back, and the activation then brought Insomnia forward with nothing open. The close now remembers the app in front, and the activation gives focus back to it when it lands. The same holds for a popover closed before the activation of a plain open lands.
A close before the activation lands now follows the apps the user goes to after it, and the activation gives focus back to the last one, not to the app that was in front at the close. The wait for the activation ends one landing window after the last ask, removing its observers and the app it kept, and a released controller removes them too.
|
Full suite at |
VM runs at 499021c: the popover on macOS 27.0.1 and 26.6.2The four problems the MacBook run found at Setup. Two VMs on the maintainer's Mac mini, without a battery. All input was synthetic and posted inside the VM. Each run started from default settings and picked Popover in Settings > General > Menu bar.
The ring on macOS 26. In the first macOS 26 run, 0 of 6 holds ended the session. A second run found the cause in the test setup. A macOS reminder asking to allow screen access for the test's remote session sat over the ring, and an event tap showed the down and up going to that window instead of Insomnia. A separate probe read the button as held through every hold. With the reminder moved aside, the same bundle ended:
The same MacBook-built bundle on macOS 27 ended 6/6 popover holds with Settings closed and 5/5 with it open, so the build is not a factor. One extra hold, right after the reminder was moved with the popover still open, didn't end the session. Reopening the popover and holding again did. The cause wasn't found, and that hold is outside the 13 counted. A hold on macOS 26, before, at the middle of the ring and after: Custom then 3 on macOS 27: Folders: macOS 27.0.1 (report, run log) and macOS 26.6.2 (first run, ring diagnosis). Not covered: a physical mouse or trackpad, display sleep, Spaces, a second display, Reduce Motion, a light menu bar and a battery. Both VMs had keyboard navigation on, so a reopened popover shows the focus ring on Settings…. |




The menu bar gets a fourth style, Popover, chosen in Settings > General > Menu bar. Pills stays the default.
In Popover style the bar shows the eye, with a short countdown during a session ("1h 59m", or the end time). Any click on it opens a popover: preset chips to start or extend, a custom length, the end ring, the battery's time left, and the Mac's status and warnings. #118 also adds a live sleep check that runs in every style: every 30 s during a session, and when the popover opens, it sets SleepDisabled again if something else turned it off.
This lands #118 by @aretelew at 2cdf3fa. The commit is merged here unchanged, so GitHub marks #118 merged when this one merges and its author keeps the credit. #118 replaces the pills and the right-click menu with the popover for everyone and rebuilds Settings into panes. That conflicts with #147's one-page Settings and with #96's styles, so it can't merge as it is. Here the popover ships as a style, and the Settings rebuild is left out (listed below).
To @aretelew: please don't push to your branch now. A new commit on it isn't in this pull request, so #118 would stay open instead of showing as merged.
The base is
integration/menu-bar-styles(#152), which this stacks on. When #152 merges, this pull request's base moves down the stack and nothing else changes.What ships from #118
Not shipped: the Settings panes, sidebar, search, list editor, number boxes, info buttons and one-button hotspot save (#147's one-page Settings stays); the removal of the pills and the right-click menu in the other styles; the four default presets (the eight on main stay); the palette renames; and #118's docs for a popover-only app, rewritten for four styles.
Conflicts resolved
git show --remerge-diff a15fc53shows each one. The rule for every file was the base's code for Pills, Compact, Minimal, the right-click menu and Settings, with #118's popover code added beside it.commit()becomespopoverCommit().popoverInput,popoverItems,actionRows).secrets:for the failover row, then the base's display sleep, time change and session command lines, which Menu bar popover and Settings overhaul #118's side dropped.InThePopover.27a8164.c6185f4merges #152's latest head. Both sides added a call afterobserveMinimalProgress()in the controller's init, and both stay.Maintainer commits
efea40a: builds the merge against the base's three styles: shared names split, the base's countdown transition and hotspot password field back, Menu bar popover and Settings overhaul #118 tests that repeated a base test dropped.8a122d2: merges Menu bar styles: land #96 (Compact and Minimal) #152 at751fc19.e26d0e3: removes Menu bar popover and Settings overhaul #118's Settings panes, search, list editor and number boxes, which nothing used after the merge kept the one-page Settings.3c87372: Popover becomes the fourth style, stored as"popover". Clicks open the popover only in that style, so the other three keep Menu bar: hold-to-end works on macOS 27 and shows on a light bar #93's press handling and their right-click menu. A style change closes the popover and removes its monitors, and a pending start carries over. The popover gives focus back to the app you were in when it closes.aaa5902: the live sleep check reads and sets SleepDisabled only while a session holds sleep, no end or quit is pending, the end time is ahead and session.json is still there. It writes the journal before pmset. 355fa93 could set it again under an end waiting for its retry, after the deadline, or after the backstop restored sleep.292b25c: keeps the eight default presets.d4bb920: a test that a draining battery with a time to full reads as on the adapter.75623cc: keeps only the design tokens the popover reads and the base palette, so Pills, Compact and Minimal look as on Menu bar styles: land #96 (Compact and Minimal) #152.7adf851,27a8164: comments, docs/spec.md, README and docs/releasing.md for four styles and Popover's release checks.9895364: a click outside that leaves Insomnia in front gives focus back to the app you were in.c16c291,54327af: the sleep check checks again after its locked read. An End or Quit after that last check waits for the set and then restores sleep; a test holds the real command runner at launch to pin this.9f71c48,fc2e244: a chip's duration, to the second, goes with a start or extend handed to another style, so a refused start retries the same length.69ebedc: PopoverStyleTests wait for the request to finish, not for a two-second deadline.c1540d7: an extend asked for while an end waits for its retry is refused, as a start already is. Before, the extend moved the deadline and the retry then ended the session anyway, discarding the added time. This was on main too, for every style and--session extend.c9757af: the popover's chips, Custom field, Return and ring read the session manager's end state, as Menu bar styles: land #96 (Compact and Minimal) #152's Compact panel does. During any end the header reads Ending… and the controls are off; the ring stays usable while an end waits for its retry.b4bb03f: the "still being ended" refusal for a start or an extend goes away when that end resolves. Before, it stayed up after sleep was restored. Main's start refusal had the same problem.650c666: the queued extend test holds the lock waits on gates instead of timing them, through an injected lock wait that only tests replace.14d2240: the popover's "Sleep is not held" row has no em dash.0012166: a sleep check that read SleepDisabled as 0 and then can't set it again reports "Sleep is not held": a busy lock, an unreadable journal,sudo pmsetholding the recovery lock, or a failed read under the lock. Before, it only logged, so the popover dropped its row and no notice came.4d6759a: a Custom start refused after a close and reopen of the popover puts its value back in the fields, so Retry starts the same length.8439392: a switch to another app with the pointer on the item closes the popover. Before, it stayed open over the next app.ec548b6: the popover reads the watts and the battery's time left when it opens and when the battery changes, not on every redraw.de9fb9d: the bar host's click-through test compares the host with a plain hosting view, since on macOS 27 a hosting view's own hit test can return nil.b729140: a failed read under the lock warns only if no end, quit or deadline came while it ran.8bb65f7: only the popover's own Start or Return puts its value back after a reopen. A chip's refused start left an older Custom draft in the fields, and Retry asked for that.38725cc: a reopen right after a close asks for focus again. The close's yield to the app the user was in could land after the reopen and close it half a second later.aad23f0: an open popover reads the watts and the time left again every 10 seconds. Both can change with the percent and the charging state the same, as when the estimate settles after unplugging.e8f1aa5: a failed set warns only if no end, quit or deadline came while it ran.495be68: the resign from that yield waits for the reopen's activation even with the pointer already in the popover. A yield counts as on its way for one second.6ffff65: the same with two quick close and reopen cycles. Every resign within a second of a yield waits, the last one decides, and the second close gives focus back to the app the user was in. The activation after a yield parks no windows.9f0cae6: the power timer stops itself if the controller is released with the popover up.57dd7c8: a click inside the popover keeps it open. On a MacBook most clicks there also reached the monitor for clicks in other apps, with the location in the popover's own coordinates, so the popover closed and the click was lost. Custom, the fields and the end ring could not be used with the pointer.c778a68: Esc closes the popover with no field focused, as with the time buttons out. Before, it worked only once a field had the cursor.cb49c1a: Custom puts the cursor in Hours. The focus was asked for before the fields were in the window, and dropped.c9d89f9: the popover's arrow follows the item when the menu bar moves it. After a session ended with the popover up, the arrow pointed about 50 pt beside the eye.9821a61: a close before the activation its open asked for has landed gives focus back once it lands. A click on the item right after a quick close and reopen left Insomnia in front with nothing open.499021c: that focus goes to the app the user went to last, if they switched apps before the activation landed. The wait ends a second after the last ask, with nothing left watching.Each behavior change has a test that fails without it.
Left for the owner
The existing behavior is in place for each.
Tests
The full suite at
499021c: 1655 tests, 0 failures, on macOS 26.2. The release build with warnings as errors is clean. Hosted CI runs on macOS 26 and 27.Review
Clean at
499021c, round 10. Round 1 found the sleep check's missing second look and a preset chip's duration lost on a style change. Round 2 found a preset with seconds rounded down on retry, and a gap before the sleep check's command launches, which turned out safe and is now pinned by a test. A review of the merge with #152 found the extend accepted during a pending end. Round 3 found the refusal message staying after the end and a test that depended on timing. Round 4 had no findings at650c666.Greptile then found five problems at
650c666, fixed in14d2240toec548b6. Rounds 5 to 9 found the rest of the fixes listed above: a warning during an end or after the deadline, a chip's refused start bringing back an older Custom draft, a battery estimate that went stale, and a series of focus races around a quick close and reopen. Each has a test that fails without its fix.Evidence
The MacBook run at
650c666(macOS 26.2) found four problems in the popover:57dd7c8toc9d89f9fix these, and each fix was checked on the same MacBook with a local build before it was committed. Sessions, the bar's countdown, the sleep check and the other three styles worked at650c666.At
499021c, VMs on macOS 27.0.1 and 26.6.2 passed the same checks: clicks inside, Custom focus, Esc, the end ring, the arrow, Command-Tab away, quick close and reopen cycles, the idle rows and the other three styles. The first macOS 26 run's ring holds failed because a system reminder sat over the ring; with it moved aside, 13 of 13 holds ended the session. Details and pictures are in this comment.

Confidence Score: 5/5
The PR appears safe to merge; all previous findings are addressed and no new actionable issue was found.Summary
Adds Popover as a fourth menu bar style while keeping Pills as the default.
macOS tests and visual checks were not run in this Linux environment.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Menu bar style] --> B[Pills, Compact or Minimal] A --> C[Popover] C --> D[Session controls and status] D --> E[Session manager] F[30-second check or popover open] --> G[Read sleep setting] G --> H{Sleep protection lost?} H -->|Yes| I[Take recovery lock and check again] I --> J[Write journal before restoring protection] I --> K[Show warning if repair cannot finish]Reviews (2) · Last reviewed commit: "Popover: focus goes to the app the user ..." · Reviewed by Greptile