Skip to content

Commit 9a623b2

Browse files
committed
fix: schedule settled refreshes after Codex activity
1 parent 76e4938 commit 9a623b2

2 files changed

Lines changed: 124 additions & 0 deletions

File tree

menubar.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
INSTALL_BUTTON_EXTRA_HEIGHT = BUTTON_HEIGHT + 10.0
144144
UPDATE_DISMISS_SECONDS = 24 * 3600
145145
UPDATE_ALERT_BODY_LIMIT = 2000
146+
SETTLED_REFRESH_DELAYS = (3.0, 15.0, 45.0)
146147

147148
logger = logging.getLogger(__name__)
148149

@@ -415,6 +416,7 @@ class AppDelegate(NSObject):
415416
_history_entries_cache_fingerprint = objc.ivar()
416417
_quota_notifier = objc.ivar()
417418
_switch_menu_action_taken = objc.ivar()
419+
_settled_refresh_timers = objc.ivar()
418420
language = objc.ivar()
419421

420422
def initWithMock_interval_(self, mock: bool, interval: int) -> AppDelegate:
@@ -442,6 +444,7 @@ def initWithMock_interval_(self, mock: bool, interval: int) -> AppDelegate:
442444
self._history_entries_cache = None
443445
self._history_entries_cache_fingerprint = None
444446
self._switch_menu_action_taken = False
447+
self._settled_refresh_timers = []
445448
return self
446449

447450
def applicationDidFinishLaunching_(self, notification: Any) -> None:
@@ -491,6 +494,7 @@ def timerFired_(self, timer: Any) -> None:
491494

492495
def refreshNow_(self, sender: Any) -> None:
493496
self._refresh(queue_if_busy=True)
497+
self._schedule_settled_refreshes()
494498

495499
def installHook_(self, sender: Any) -> None:
496500
thread = threading.Thread(target=self._install_hook_in_background, daemon=True)
@@ -885,6 +889,33 @@ def _refresh(self, queue_if_busy: bool = False) -> None:
885889

886890
def refreshFromFileEvent_(self, _sender: Any) -> None:
887891
self._refresh(queue_if_busy=True)
892+
self._schedule_settled_refreshes()
893+
894+
def _schedule_settled_refreshes(self) -> None:
895+
for timer in self._settled_refresh_timers:
896+
with contextlib.suppress(Exception):
897+
timer.invalidate()
898+
899+
timers = []
900+
for delay in SETTLED_REFRESH_DELAYS:
901+
timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
902+
delay,
903+
self,
904+
"settledRefreshTimerFired:",
905+
None,
906+
False,
907+
)
908+
NSRunLoop.currentRunLoop().addTimer_forMode_(timer, NSRunLoopCommonModes)
909+
timers.append(timer)
910+
self._settled_refresh_timers = timers
911+
912+
def settledRefreshTimerFired_(self, timer: Any) -> None:
913+
self._settled_refresh_timers = [
914+
pending
915+
for pending in self._settled_refresh_timers
916+
if pending is not timer and pending.isValid()
917+
]
918+
self._refresh(queue_if_busy=True)
888919

889920
def _refresh_in_background(self) -> None:
890921
submitted = False

tests/test_menubar.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,6 +1489,99 @@ def test_refresh_now_queues_when_refresh_is_busy() -> None:
14891489
assert delegate._refresh_queued is True
14901490

14911491

1492+
def test_schedule_settled_refreshes_replaces_pending_timers(
1493+
monkeypatch: pytest.MonkeyPatch,
1494+
) -> None:
1495+
class FakeTimer:
1496+
def __init__(self, delay: float) -> None:
1497+
self.delay = delay
1498+
self.invalidated = False
1499+
1500+
def invalidate(self) -> None:
1501+
self.invalidated = True
1502+
1503+
def isValid(self) -> bool:
1504+
return not self.invalidated
1505+
1506+
class FakeNSTimer:
1507+
created: list[FakeTimer] = []
1508+
1509+
@staticmethod
1510+
def scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
1511+
delay: float,
1512+
_target: object,
1513+
_selector: str,
1514+
_user_info: object,
1515+
_repeats: bool,
1516+
) -> FakeTimer:
1517+
timer = FakeTimer(delay)
1518+
FakeNSTimer.created.append(timer)
1519+
return timer
1520+
1521+
class FakeRunLoop:
1522+
added: list[FakeTimer] = []
1523+
1524+
@staticmethod
1525+
def currentRunLoop() -> type[FakeRunLoop]:
1526+
return FakeRunLoop
1527+
1528+
@staticmethod
1529+
def addTimer_forMode_(timer: FakeTimer, _mode: object) -> None:
1530+
FakeRunLoop.added.append(timer)
1531+
1532+
class Delegate:
1533+
_settled_refresh_timers: list[FakeTimer]
1534+
1535+
def __init__(self) -> None:
1536+
self._settled_refresh_timers = [FakeTimer(1.0)]
1537+
1538+
old_timer = Delegate()._settled_refresh_timers[0]
1539+
delegate = Delegate()
1540+
delegate._settled_refresh_timers = [old_timer]
1541+
monkeypatch.setattr(menubar, "NSTimer", FakeNSTimer)
1542+
monkeypatch.setattr(menubar, "NSRunLoop", FakeRunLoop)
1543+
1544+
menubar.AppDelegate._schedule_settled_refreshes(cast(Any, delegate))
1545+
1546+
assert old_timer.invalidated is True
1547+
assert [timer.delay for timer in delegate._settled_refresh_timers] == list(
1548+
menubar.SETTLED_REFRESH_DELAYS
1549+
)
1550+
assert FakeRunLoop.added == delegate._settled_refresh_timers
1551+
1552+
1553+
def test_settled_refresh_timer_fires_queued_refresh(
1554+
monkeypatch: pytest.MonkeyPatch,
1555+
) -> None:
1556+
class FakeTimer:
1557+
def __init__(self, valid: bool) -> None:
1558+
self._valid = valid
1559+
1560+
def isValid(self) -> bool:
1561+
return self._valid
1562+
1563+
refresh_calls = 0
1564+
queue_flags: list[bool] = []
1565+
1566+
def fake_refresh(_self: object, *, queue_if_busy: bool = False) -> None:
1567+
nonlocal refresh_calls
1568+
refresh_calls += 1
1569+
queue_flags.append(queue_if_busy)
1570+
1571+
delegate = menubar.AppDelegate.alloc().initWithMock_interval_(True, 60)
1572+
fired = FakeTimer(True)
1573+
pending = FakeTimer(True)
1574+
invalid = FakeTimer(False)
1575+
delegate._settled_refresh_timers = [fired, pending, invalid]
1576+
monkeypatch.setattr(menubar.AppDelegate, "_refresh", fake_refresh)
1577+
1578+
delegate.settledRefreshTimerFired_(fired)
1579+
1580+
assert delegate._settled_refresh_timers == [pending]
1581+
assert refresh_calls == 1
1582+
assert queue_flags == [True]
1583+
1584+
14921585
def test_apply_refresh_result_clears_busy_flag_when_ui_update_fails() -> None:
14931586
class FailingPopover:
14941587
def isShown(self) -> bool:

0 commit comments

Comments
 (0)