Skip to content

Commit 6c300d1

Browse files
authored
fix: queue manual refresh while busy (aqua5230#12)
When a manual refresh is requested while another refresh is still in flight, queue it instead of dropping it. The finally block now preserves both codex_model assignment and web-language injection before clearing the busy flag and draining one queued refresh. Co-authored-by: ericweichun <ericweichun@users.noreply.github.com>
1 parent 1d910fa commit 6c300d1

2 files changed

Lines changed: 59 additions & 14 deletions

File tree

menubar.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ class AppDelegate(NSObject):
326326
codex_model = objc.ivar()
327327
burn_rate_trackers = objc.ivar()
328328
_refresh_in_flight = objc.ivar()
329+
_refresh_queued = objc.ivar()
329330
_fs_stream = objc.ivar()
330331
_history_entries_cache = objc.ivar()
331332
_history_entries_cache_fingerprint = objc.ivar()
@@ -350,6 +351,7 @@ def initWithMock_interval_(self, mock: bool, interval: int) -> AppDelegate:
350351
"codex_weekly": BurnRateTracker(),
351352
}
352353
self._refresh_in_flight = False
354+
self._refresh_queued = False
353355
self._fs_stream = None
354356
self._history_entries_cache = None
355357
self._history_entries_cache_fingerprint = None
@@ -391,7 +393,7 @@ def timerFired_(self, timer: Any) -> None:
391393
self._refresh()
392394

393395
def refreshNow_(self, sender: Any) -> None:
394-
self._refresh()
396+
self._refresh(queue_if_busy=True)
395397

396398
def installHook_(self, sender: Any) -> None:
397399
thread = threading.Thread(target=self._install_hook_in_background, daemon=True)
@@ -642,8 +644,10 @@ def togglePopover_(self, sender: Any) -> None:
642644
button = self.status_item.button()
643645
self.popover.showRelativeToRect_ofView_preferredEdge_(button.bounds(), button, NSMinYEdge)
644646

645-
def _refresh(self) -> None:
647+
def _refresh(self, queue_if_busy: bool = False) -> None:
646648
if self._refresh_in_flight:
649+
if queue_if_busy:
650+
self._refresh_queued = True
647651
return
648652
self._refresh_in_flight = True
649653
thread = threading.Thread(target=self._refresh_in_background, daemon=True)
@@ -683,18 +687,25 @@ def _refresh_in_background(self) -> None:
683687
)
684688

685689
def _applyRefreshResult_(self, result: dict[str, Any]) -> None:
686-
state = result["state"]
687-
codex_5h_pct = result["codex_5h_pct"]
688-
codex_model = result.get("codex_model", "unknown")
689-
self.codex_5h_pct = codex_5h_pct
690-
self.codex_model = codex_model
691-
self.latest_state = state
692-
if self.popover.isShown():
693-
self.popover_controller.setState_(self.latest_state)
694-
self.popover.setContentSize_(_popover_size(state, self.active_panel))
695-
self._inject_web_language(state.language)
696-
self.status_item.button().setTitle_(self._compose_title(state))
697-
self._refresh_in_flight = False
690+
should_refresh_again = False
691+
try:
692+
state = result["state"]
693+
codex_5h_pct = result["codex_5h_pct"]
694+
codex_model = result.get("codex_model", "unknown")
695+
self.codex_5h_pct = codex_5h_pct
696+
self.codex_model = codex_model
697+
self.latest_state = state
698+
if self.popover.isShown():
699+
self.popover_controller.setState_(self.latest_state)
700+
self.popover.setContentSize_(_popover_size(state, self.active_panel))
701+
self._inject_web_language(state.language)
702+
self.status_item.button().setTitle_(self._compose_title(state))
703+
finally:
704+
should_refresh_again = bool(self._refresh_queued)
705+
self._refresh_queued = False
706+
self._refresh_in_flight = False
707+
if should_refresh_again:
708+
self._refresh()
698709

699710
def _inject_web_language(self, language: str) -> None:
700711
content_view = self.popover_controller.content_view

tests/test_menubar.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,23 +968,57 @@ def button(self) -> FakeButton:
968968
delegate.popover = FakePopover(shown=True)
969969
delegate.status_item = FakeStatusItem(button)
970970
delegate._refresh_in_flight = True
971+
delegate._refresh_queued = False
971972

972973
delegate._applyRefreshResult_({"state": state, "codex_5h_pct": 12})
973974

974975
assert controller.calls == [state]
975976
assert delegate.latest_state == state
976977
assert delegate.codex_5h_pct == 12
978+
assert delegate._refresh_in_flight is False
977979
assert button.titles
978980

979981
controller.calls.clear()
980982
delegate.popover = FakePopover(shown=False)
981983
delegate._refresh_in_flight = True
984+
delegate._refresh_queued = False
982985

983986
delegate._applyRefreshResult_({"state": state, "codex_5h_pct": 34})
984987

985988
assert controller.calls == []
986989
assert delegate.latest_state == state
987990
assert delegate.codex_5h_pct == 34
991+
assert delegate._refresh_in_flight is False
992+
993+
994+
def test_refresh_now_queues_when_refresh_is_busy() -> None:
995+
delegate = menubar.AppDelegate.alloc().initWithMock_interval_(True, 60)
996+
delegate._refresh_in_flight = True
997+
delegate._refresh_queued = False
998+
999+
delegate.refreshNow_(None)
1000+
1001+
assert delegate._refresh_queued is True
1002+
1003+
1004+
def test_apply_refresh_result_clears_busy_flag_when_ui_update_fails() -> None:
1005+
class FailingPopover:
1006+
def isShown(self) -> bool:
1007+
return False
1008+
1009+
def setContentSize_(self, size: object) -> None:
1010+
raise RuntimeError("size failed")
1011+
1012+
delegate = menubar.AppDelegate.alloc().initWithMock_interval_(True, 60)
1013+
delegate.popover = FailingPopover()
1014+
delegate._refresh_in_flight = True
1015+
delegate._refresh_queued = False
1016+
1017+
with pytest.raises(RuntimeError, match="size failed"):
1018+
delegate._applyRefreshResult_({"state": menubar._empty_state(), "codex_5h_pct": None})
1019+
1020+
assert delegate._refresh_in_flight is False
1021+
assert delegate._refresh_queued is False
9881022

9891023

9901024
def test_switching_visible_panel_reopens_popover(monkeypatch: pytest.MonkeyPatch) -> None:

0 commit comments

Comments
 (0)