usage-cli 0.32.1__tar.gz → 0.32.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. {usage_cli-0.32.1 → usage_cli-0.32.3}/PKG-INFO +1 -1
  2. {usage_cli-0.32.1 → usage_cli-0.32.3}/i18n.json +70 -0
  3. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/agy.py +16 -7
  4. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/app.py +5 -5
  5. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/grok.py +8 -17
  6. usage_cli-0.32.3/menubar/manual_refresh.py +51 -0
  7. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/state.py +18 -3
  8. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/title.py +19 -13
  9. {usage_cli-0.32.1 → usage_cli-0.32.3}/panels/payload.py +1 -1
  10. {usage_cli-0.32.1 → usage_cli-0.32.3}/pyproject.toml +1 -1
  11. usage_cli-0.32.3/tests/test_manual_refresh.py +62 -0
  12. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_menubar.py +38 -4
  13. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_menubar_agy.py +19 -2
  14. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_menubar_grok.py +14 -0
  15. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_menubar_state.py +50 -0
  16. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_cli.py +36 -1
  17. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_web_panel_payload.py +10 -0
  18. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_wintray.py +67 -0
  19. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_cli.egg-info/PKG-INFO +1 -1
  20. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_cli.egg-info/SOURCES.txt +2 -0
  21. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_cli.py +11 -6
  22. {usage_cli-0.32.1 → usage_cli-0.32.3}/wintray/app.py +36 -7
  23. {usage_cli-0.32.1 → usage_cli-0.32.3}/LICENSE +0 -0
  24. {usage_cli-0.32.1 → usage_cli-0.32.3}/README.md +0 -0
  25. {usage_cli-0.32.1 → usage_cli-0.32.3}/adapters/__init__.py +0 -0
  26. {usage_cli-0.32.1 → usage_cli-0.32.3}/adapters/agy.py +0 -0
  27. {usage_cli-0.32.1 → usage_cli-0.32.3}/adapters/claude.py +0 -0
  28. {usage_cli-0.32.1 → usage_cli-0.32.3}/adapters/codex.py +0 -0
  29. {usage_cli-0.32.1 → usage_cli-0.32.3}/adapters/grok.py +0 -0
  30. {usage_cli-0.32.1 → usage_cli-0.32.3}/adapters/muse.py +0 -0
  31. {usage_cli-0.32.1 → usage_cli-0.32.3}/adapters/rate_limits.py +0 -0
  32. {usage_cli-0.32.1 → usage_cli-0.32.3}/adapters/registry.py +0 -0
  33. {usage_cli-0.32.1 → usage_cli-0.32.3}/adapters/types.py +0 -0
  34. {usage_cli-0.32.1 → usage_cli-0.32.3}/analyzer/__init__.py +0 -0
  35. {usage_cli-0.32.1 → usage_cli-0.32.3}/analyzer/aggregator.py +0 -0
  36. {usage_cli-0.32.1 → usage_cli-0.32.3}/analyzer/blocks.py +0 -0
  37. {usage_cli-0.32.1 → usage_cli-0.32.3}/analyzer/diagnoser.py +0 -0
  38. {usage_cli-0.32.1 → usage_cli-0.32.3}/analyzer/dispatch_ledger.py +0 -0
  39. {usage_cli-0.32.1 → usage_cli-0.32.3}/analyzer/insights.py +0 -0
  40. {usage_cli-0.32.1 → usage_cli-0.32.3}/analyzer/persona_loader.py +0 -0
  41. {usage_cli-0.32.1 → usage_cli-0.32.3}/analyzer/reporter.py +0 -0
  42. {usage_cli-0.32.1 → usage_cli-0.32.3}/analyzer/subscription.py +0 -0
  43. {usage_cli-0.32.1 → usage_cli-0.32.3}/analyzer/usage_snapshot.py +0 -0
  44. {usage_cli-0.32.1 → usage_cli-0.32.3}/doctor.py +0 -0
  45. {usage_cli-0.32.1 → usage_cli-0.32.3}/fsevents_watch.py +0 -0
  46. {usage_cli-0.32.1 → usage_cli-0.32.3}/i18n.py +0 -0
  47. {usage_cli-0.32.1 → usage_cli-0.32.3}/installer/__init__.py +0 -0
  48. {usage_cli-0.32.1 → usage_cli-0.32.3}/installer/claude_pane.py +0 -0
  49. {usage_cli-0.32.1 → usage_cli-0.32.3}/installer/login_item.py +0 -0
  50. {usage_cli-0.32.1 → usage_cli-0.32.3}/installer/session_hooks.py +0 -0
  51. {usage_cli-0.32.1 → usage_cli-0.32.3}/installer/setup_hook.py +0 -0
  52. {usage_cli-0.32.1 → usage_cli-0.32.3}/installer/statusline_settings.py +0 -0
  53. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/__init__.py +0 -0
  54. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/agy_disk_cache.py +0 -0
  55. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/agy_loader.py +0 -0
  56. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/agy_quota_probe.py +0 -0
  57. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/cache_quarantine.py +0 -0
  58. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/claude_paths.py +0 -0
  59. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/codex_disk_cache.py +0 -0
  60. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/codex_events.py +0 -0
  61. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/codex_fork_replay.py +0 -0
  62. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/codex_loader.py +0 -0
  63. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/codex_paths.py +0 -0
  64. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/disk_cache_common.py +0 -0
  65. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/disk_cache_lifecycle.py +0 -0
  66. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/grok_loader.py +0 -0
  67. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/grok_quota_probe.py +0 -0
  68. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/history_disk_cache.py +0 -0
  69. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/history_loader.py +0 -0
  70. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/jsonl_limits.py +0 -0
  71. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/jsonl_utils.py +0 -0
  72. {usage_cli-0.32.1 → usage_cli-0.32.3}/loaders/muse_loader.py +0 -0
  73. {usage_cli-0.32.1 → usage_cli-0.32.3}/main.py +0 -0
  74. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/__init__.py +0 -0
  75. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/actions.py +0 -0
  76. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/chrome.py +0 -0
  77. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/menu.py +0 -0
  78. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/new_badge.py +0 -0
  79. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/notify.py +0 -0
  80. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/popover.py +0 -0
  81. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/prefs.py +0 -0
  82. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/refresh.py +0 -0
  83. {usage_cli-0.32.1 → usage_cli-0.32.3}/menubar/update.py +0 -0
  84. {usage_cli-0.32.1 → usage_cli-0.32.3}/panels/__init__.py +0 -0
  85. {usage_cli-0.32.1 → usage_cli-0.32.3}/panels/base.py +0 -0
  86. {usage_cli-0.32.1 → usage_cli-0.32.3}/panels/dynamic_height.py +0 -0
  87. {usage_cli-0.32.1 → usage_cli-0.32.3}/panels/panel_scale.py +0 -0
  88. {usage_cli-0.32.1 → usage_cli-0.32.3}/panels/panel_window.py +0 -0
  89. {usage_cli-0.32.1 → usage_cli-0.32.3}/panels/panel_window_state.py +0 -0
  90. {usage_cli-0.32.1 → usage_cli-0.32.3}/panels/web_panel.py +0 -0
  91. {usage_cli-0.32.1 → usage_cli-0.32.3}/panels/window_drag.py +0 -0
  92. {usage_cli-0.32.1 → usage_cli-0.32.3}/prefs.py +0 -0
  93. {usage_cli-0.32.1 → usage_cli-0.32.3}/pricing.py +0 -0
  94. {usage_cli-0.32.1 → usage_cli-0.32.3}/project_resolver.py +0 -0
  95. {usage_cli-0.32.1 → usage_cli-0.32.3}/quota/__init__.py +0 -0
  96. {usage_cli-0.32.1 → usage_cli-0.32.3}/quota/agy_window_keeper.py +0 -0
  97. {usage_cli-0.32.1 → usage_cli-0.32.3}/quota/burn_rate.py +0 -0
  98. {usage_cli-0.32.1 → usage_cli-0.32.3}/quota/codex_window_keeper.py +0 -0
  99. {usage_cli-0.32.1 → usage_cli-0.32.3}/quota/usage_rate.py +0 -0
  100. {usage_cli-0.32.1 → usage_cli-0.32.3}/quota/window_keeper.py +0 -0
  101. {usage_cli-0.32.1 → usage_cli-0.32.3}/service_status.py +0 -0
  102. {usage_cli-0.32.1 → usage_cli-0.32.3}/setup.cfg +0 -0
  103. {usage_cli-0.32.1 → usage_cli-0.32.3}/setup_app.py +0 -0
  104. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_adapters_claude.py +0 -0
  105. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_adapters_codex.py +0 -0
  106. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_adapters_rate_limits.py +0 -0
  107. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_aggregator.py +0 -0
  108. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_agy_adapter.py +0 -0
  109. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_agy_disk_cache.py +0 -0
  110. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_agy_loader.py +0 -0
  111. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_agy_quota_probe.py +0 -0
  112. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_agy_window_keeper.py +0 -0
  113. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_analyzer_blocks.py +0 -0
  114. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_analyzer_pipeline.py +0 -0
  115. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_burn_rate.py +0 -0
  116. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_cache_quarantine.py +0 -0
  117. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_claude_pane.py +0 -0
  118. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_claude_paths.py +0 -0
  119. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_codex_consistency.py +0 -0
  120. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_codex_loader.py +0 -0
  121. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_codex_window_keeper.py +0 -0
  122. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_diagnoser.py +0 -0
  123. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_disk_cache_shards.py +0 -0
  124. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_dispatch_ledger.py +0 -0
  125. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_doctor.py +0 -0
  126. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_dynamic_height.py +0 -0
  127. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_fsevents_watch.py +0 -0
  128. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_grok_loader.py +0 -0
  129. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_grok_quota_probe.py +0 -0
  130. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_history_loader.py +0 -0
  131. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_hook_scripts_are_standalone.py +0 -0
  132. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_html_report_snapshot.py +0 -0
  133. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_i18n_format_fallback.py +0 -0
  134. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_i18n_key_parity.py +0 -0
  135. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_i18n_packaged_path.py +0 -0
  136. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_insights.py +0 -0
  137. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_jsonl_utils.py +0 -0
  138. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_login_item.py +0 -0
  139. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_main.py +0 -0
  140. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_menubar_notify.py +0 -0
  141. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_menubar_prefs.py +0 -0
  142. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_muse_loader.py +0 -0
  143. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_new_badge.py +0 -0
  144. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_packaged_resources.py +0 -0
  145. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_packaging.py +0 -0
  146. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_panel_footer_layout.py +0 -0
  147. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_panel_scale.py +0 -0
  148. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_panel_window_state.py +0 -0
  149. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_panels.py +0 -0
  150. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_panels_lazy_import.py +0 -0
  151. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_persona_loader.py +0 -0
  152. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_pricing.py +0 -0
  153. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_project_resolver.py +0 -0
  154. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_report_daily_chart.py +0 -0
  155. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_report_filter_js.py +0 -0
  156. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_reporter_build_data.py +0 -0
  157. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_service_status.py +0 -0
  158. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_session_resume.py +0 -0
  159. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_setup_app.py +0 -0
  160. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_setup_hook.py +0 -0
  161. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_setup_hook_coexistence.py +0 -0
  162. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_setup_hook_resume.py +0 -0
  163. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_setup_hook_terse.py +0 -0
  164. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_subprocess_utils.py +0 -0
  165. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_subscription.py +0 -0
  166. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_time_utils.py +0 -0
  167. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_tui.py +0 -0
  168. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_tui_sprite.py +0 -0
  169. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_ui_tables.py +0 -0
  170. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_update_checker.py +0 -0
  171. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_update_gate.py +0 -0
  172. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_update_release_notes.py +0 -0
  173. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_client.py +0 -0
  174. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_diagnosis_snapshot.py +0 -0
  175. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_dir_sweeper.py +0 -0
  176. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_lang.py +0 -0
  177. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_logging.py +0 -0
  178. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_notifications.py +0 -0
  179. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_rate.py +0 -0
  180. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_snapshot.py +0 -0
  181. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_snapshot_replay.py +0 -0
  182. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_statusline.py +0 -0
  183. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_statusline_agy.py +0 -0
  184. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_statusline_forwarder.py +0 -0
  185. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_statusline_grok.py +0 -0
  186. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_terse_mode.py +0 -0
  187. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_usage_terse_reminder.py +0 -0
  188. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_wheel_packaging.py +0 -0
  189. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_win_login_item.py +0 -0
  190. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_window_keeper.py +0 -0
  191. {usage_cli-0.32.1 → usage_cli-0.32.3}/tests/test_windows_manifest.py +0 -0
  192. {usage_cli-0.32.1 → usage_cli-0.32.3}/tui/__init__.py +0 -0
  193. {usage_cli-0.32.1 → usage_cli-0.32.3}/tui/app.py +0 -0
  194. {usage_cli-0.32.1 → usage_cli-0.32.3}/tui/sprite.py +0 -0
  195. {usage_cli-0.32.1 → usage_cli-0.32.3}/ui/__init__.py +0 -0
  196. {usage_cli-0.32.1 → usage_cli-0.32.3}/ui/html_report.py +0 -0
  197. {usage_cli-0.32.1 → usage_cli-0.32.3}/ui/report_charts.py +0 -0
  198. {usage_cli-0.32.1 → usage_cli-0.32.3}/ui/report_daily_chart.py +0 -0
  199. {usage_cli-0.32.1 → usage_cli-0.32.3}/ui/report_filter.py +0 -0
  200. {usage_cli-0.32.1 → usage_cli-0.32.3}/ui/report_ledger.py +0 -0
  201. {usage_cli-0.32.1 → usage_cli-0.32.3}/ui/report_scripts.py +0 -0
  202. {usage_cli-0.32.1 → usage_cli-0.32.3}/ui/report_styles.py +0 -0
  203. {usage_cli-0.32.1 → usage_cli-0.32.3}/ui/tables.py +0 -0
  204. {usage_cli-0.32.1 → usage_cli-0.32.3}/updates/__init__.py +0 -0
  205. {usage_cli-0.32.1 → usage_cli-0.32.3}/updates/checker.py +0 -0
  206. {usage_cli-0.32.1 → usage_cli-0.32.3}/updates/gate.py +0 -0
  207. {usage_cli-0.32.1 → usage_cli-0.32.3}/updates/release_notes.py +0 -0
  208. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_cli.egg-info/dependency_links.txt +0 -0
  209. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_cli.egg-info/entry_points.txt +0 -0
  210. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_cli.egg-info/requires.txt +0 -0
  211. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_cli.egg-info/top_level.txt +0 -0
  212. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_client.py +0 -0
  213. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_common/__init__.py +0 -0
  214. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_common/subprocess_utils.py +0 -0
  215. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_common/time_utils.py +0 -0
  216. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_common/usage_dir_sweeper.py +0 -0
  217. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_common/usage_lang.py +0 -0
  218. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_common/usage_logging.py +0 -0
  219. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_diagnosis_snapshot.py +0 -0
  220. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_notifications.py +0 -0
  221. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_session_resume.py +0 -0
  222. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_statusline.py +0 -0
  223. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_statusline_agy.py +0 -0
  224. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_statusline_forwarder.py +0 -0
  225. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_statusline_grok.py +0 -0
  226. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_terse_mode.py +0 -0
  227. {usage_cli-0.32.1 → usage_cli-0.32.3}/usage_terse_reminder.py +0 -0
  228. {usage_cli-0.32.1 → usage_cli-0.32.3}/wintray/__init__.py +0 -0
  229. {usage_cli-0.32.1 → usage_cli-0.32.3}/wintray/login_item.py +0 -0
  230. {usage_cli-0.32.1 → usage_cli-0.32.3}/wintray/menu.py +0 -0
  231. {usage_cli-0.32.1 → usage_cli-0.32.3}/wintray/watch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usage-cli
3
- Version: 0.32.1
3
+ Version: 0.32.3
4
4
  Summary: usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)
5
5
  License-Expression: AGPL-3.0-only
6
6
  Requires-Python: >=3.13
@@ -56,6 +56,7 @@
56
56
  "data_stale_hint": "資料來自備援來源,建議檢查設定",
57
57
  "status_loading": "載入中",
58
58
  "status_synced": "✓ 已同步",
59
+ "status_refreshing": "↻ 更新中…",
59
60
  "status_no_data": "無資料",
60
61
  "awaiting_rate_limits": "請對 Claude Code 發送一句訊息以同步配額",
61
62
  "hook_broken_not_installed": "⚠ 狀態列沒在更新,請按「設定狀態列」重新安裝",
@@ -75,6 +76,7 @@
75
76
  "percent_used": "{value}% 已用",
76
77
  "reset_in": "重置 {time}",
77
78
  "reset_imminent": "即將重置",
79
+ "reset_done": "已重置",
78
80
  "burn_warning": "⚠ 照目前速度 {empty}後用完 · 重置 {reset}",
79
81
  "burn_warning_compact": "⚠ {empty}後用完",
80
82
  "reset_placeholder": "重置 --",
@@ -431,8 +433,19 @@
431
433
  "claude_pane_week": "週",
432
434
  "claude_pane_period": "期間",
433
435
  "claude_pane_sessions": "Claude 對話",
436
+ "claude_pane_session_done": "完成:{title}",
437
+ "claude_pane_session_waiting": "在等你:{title}",
434
438
  "claude_pane_busy": "忙",
435
439
  "claude_pane_waiting": "等你",
440
+ "claude_pane_unavailable_not_signed_in": "未登入 · 在它的 CLI 登入一次",
441
+ "claude_pane_unavailable_no_data": "還沒有資料 · 用過一次就會出現",
442
+ "claude_pane_unavailable_error": "讀不到 · 會自動重試",
443
+ "claude_pane_waiting_input": "要你回答",
444
+ "claude_pane_waiting_dialog": "對話框開著",
445
+ "claude_pane_waiting_sandbox": "要允許沙盒",
446
+ "claude_pane_waiting_worker": "子代理請求",
447
+ "claude_pane_waiting_goal": "確認目標",
448
+ "claude_pane_waiting_for": "等你:{reason}",
436
449
  "claude_pane_agent_running": "執行中",
437
450
  "claude_pane_agent_idle": "閒置",
438
451
  "claude_pane_agent_pending": "待執行",
@@ -479,6 +492,7 @@
479
492
  "claude_pane_invalid_sidecar": "usage-pane.json 格式錯誤",
480
493
  "claude_pane_elapsed": "{minutes}分{seconds}秒",
481
494
  "claude_pane_remaining": "剩",
495
+ "claude_pane_reset_done": "已重置",
482
496
  "claude_pane_hours_ago": "{count}時前"
483
497
  },
484
498
  "en": {
@@ -538,6 +552,7 @@
538
552
  "data_stale_hint": "Data is from a fallback source. Check settings.",
539
553
  "status_loading": "Loading",
540
554
  "status_synced": "✓ Synced",
555
+ "status_refreshing": "↻ Refreshing…",
541
556
  "status_no_data": "No data",
542
557
  "awaiting_rate_limits": "Send a message in Claude Code to sync your quota",
543
558
  "hook_broken_not_installed": "⚠ Status line is not updating. Click \"Set Up Status Line\" to reinstall it.",
@@ -557,6 +572,7 @@
557
572
  "percent_used": "{value}% used",
558
573
  "reset_in": "Resets in {time}",
559
574
  "reset_imminent": "Reset imminent",
575
+ "reset_done": "Reset",
560
576
  "burn_warning": "⚠ At this pace, empty in {empty} · resets in {reset}",
561
577
  "burn_warning_compact": "⚠ Empty in {empty}",
562
578
  "reset_placeholder": "Resets in --",
@@ -913,8 +929,19 @@
913
929
  "claude_pane_week": "Week",
914
930
  "claude_pane_period": "Period",
915
931
  "claude_pane_sessions": "Claude sessions",
932
+ "claude_pane_session_done": "Done: {title}",
933
+ "claude_pane_session_waiting": "Waiting for you: {title}",
916
934
  "claude_pane_busy": "Busy",
917
935
  "claude_pane_waiting": "waiting for you",
936
+ "claude_pane_unavailable_not_signed_in": "Not signed in · sign in once in its CLI",
937
+ "claude_pane_unavailable_no_data": "No data yet · shows up after first use",
938
+ "claude_pane_unavailable_error": "Couldn't read · retrying automatically",
939
+ "claude_pane_waiting_input": "needs your answer",
940
+ "claude_pane_waiting_dialog": "dialog open",
941
+ "claude_pane_waiting_sandbox": "sandbox permission",
942
+ "claude_pane_waiting_worker": "subagent request",
943
+ "claude_pane_waiting_goal": "confirm the goal",
944
+ "claude_pane_waiting_for": "waiting: {reason}",
918
945
  "claude_pane_agent_running": "running",
919
946
  "claude_pane_agent_idle": "idle",
920
947
  "claude_pane_agent_pending": "pending",
@@ -961,6 +988,7 @@
961
988
  "claude_pane_invalid_sidecar": "Invalid usage-pane.json",
962
989
  "claude_pane_elapsed": "{minutes}m{seconds}s",
963
990
  "claude_pane_remaining": "left",
991
+ "claude_pane_reset_done": "Reset",
964
992
  "claude_pane_hours_ago": "{count}h ago"
965
993
  },
966
994
  "zh-CN": {
@@ -1020,6 +1048,7 @@
1020
1048
  "data_stale_hint": "数据来自备用来源,建议检查设置",
1021
1049
  "status_loading": "加载中",
1022
1050
  "status_synced": "✓ 已同步",
1051
+ "status_refreshing": "↻ 更新中…",
1023
1052
  "status_no_data": "无数据",
1024
1053
  "awaiting_rate_limits": "请对 Claude Code 发送一句消息以同步配额",
1025
1054
  "hook_broken_not_installed": "⚠ 状态栏没有更新,请点「设置状态栏」重新安装",
@@ -1039,6 +1068,7 @@
1039
1068
  "percent_used": "{value}% 已用",
1040
1069
  "reset_in": "重置 {time}",
1041
1070
  "reset_imminent": "即将重置",
1071
+ "reset_done": "已重置",
1042
1072
  "burn_warning": "⚠ 按当前速度 {empty}后用完 · 重置 {reset}",
1043
1073
  "burn_warning_compact": "⚠ {empty}后用完",
1044
1074
  "reset_placeholder": "重置 --",
@@ -1395,8 +1425,19 @@
1395
1425
  "claude_pane_week": "周",
1396
1426
  "claude_pane_period": "期间",
1397
1427
  "claude_pane_sessions": "Claude 对话",
1428
+ "claude_pane_session_done": "完成:{title}",
1429
+ "claude_pane_session_waiting": "在等你:{title}",
1398
1430
  "claude_pane_busy": "忙",
1399
1431
  "claude_pane_waiting": "等你",
1432
+ "claude_pane_unavailable_not_signed_in": "未登录 · 在它的 CLI 登录一次",
1433
+ "claude_pane_unavailable_no_data": "还没有数据 · 用过一次就会出现",
1434
+ "claude_pane_unavailable_error": "读不到 · 会自动重试",
1435
+ "claude_pane_waiting_input": "要你回答",
1436
+ "claude_pane_waiting_dialog": "对话框开着",
1437
+ "claude_pane_waiting_sandbox": "要允许沙盒",
1438
+ "claude_pane_waiting_worker": "子代理请求",
1439
+ "claude_pane_waiting_goal": "确认目标",
1440
+ "claude_pane_waiting_for": "等你:{reason}",
1400
1441
  "claude_pane_agent_running": "运行中",
1401
1442
  "claude_pane_agent_idle": "空闲",
1402
1443
  "claude_pane_agent_pending": "待运行",
@@ -1443,6 +1484,7 @@
1443
1484
  "claude_pane_invalid_sidecar": "usage-pane.json 格式错误",
1444
1485
  "claude_pane_elapsed": "{minutes}分{seconds}秒",
1445
1486
  "claude_pane_remaining": "剩",
1487
+ "claude_pane_reset_done": "已重置",
1446
1488
  "claude_pane_hours_ago": "{count}时前"
1447
1489
  },
1448
1490
  "ja": {
@@ -1502,6 +1544,7 @@
1502
1544
  "data_stale_hint": "データはフォールバック元から取得しています。設定を確認してください。",
1503
1545
  "status_loading": "読み込み中",
1504
1546
  "status_synced": "✓ 同期済み",
1547
+ "status_refreshing": "↻ 更新中…",
1505
1548
  "status_no_data": "データなし",
1506
1549
  "awaiting_rate_limits": "Claude Code にメッセージを送信してクォータを同期してください",
1507
1550
  "hook_broken_not_installed": "⚠ ステータスラインが更新されていません。「ステータスラインを設定」を押して再インストールしてください。",
@@ -1521,6 +1564,7 @@
1521
1564
  "percent_used": "{value}% 使用済み",
1522
1565
  "reset_in": "{time}後にリセット",
1523
1566
  "reset_imminent": "まもなくリセット",
1567
+ "reset_done": "リセット済み",
1524
1568
  "burn_warning": "⚠ このペースで{empty}で使い切り·リセット{reset}",
1525
1569
  "burn_warning_compact": "⚠ {empty}で使い切り",
1526
1570
  "reset_placeholder": "リセット --",
@@ -1877,8 +1921,19 @@
1877
1921
  "claude_pane_week": "週",
1878
1922
  "claude_pane_period": "期間",
1879
1923
  "claude_pane_sessions": "Claude セッション",
1924
+ "claude_pane_session_done": "完了:{title}",
1925
+ "claude_pane_session_waiting": "入力待ち:{title}",
1880
1926
  "claude_pane_busy": "実行中",
1881
1927
  "claude_pane_waiting": "入力待ち",
1928
+ "claude_pane_unavailable_not_signed_in": "未ログイン · CLI で一度ログイン",
1929
+ "claude_pane_unavailable_no_data": "まだデータなし · 一度使うと表示",
1930
+ "claude_pane_unavailable_error": "読み取れません · 自動で再試行",
1931
+ "claude_pane_waiting_input": "回答待ち",
1932
+ "claude_pane_waiting_dialog": "ダイアログ表示中",
1933
+ "claude_pane_waiting_sandbox": "サンドボックス許可",
1934
+ "claude_pane_waiting_worker": "サブエージェント要求",
1935
+ "claude_pane_waiting_goal": "目標の確認",
1936
+ "claude_pane_waiting_for": "入力待ち:{reason}",
1882
1937
  "claude_pane_agent_running": "実行中",
1883
1938
  "claude_pane_agent_idle": "待機中",
1884
1939
  "claude_pane_agent_pending": "実行待ち",
@@ -1925,6 +1980,7 @@
1925
1980
  "claude_pane_invalid_sidecar": "usage-pane.json の形式が不正です",
1926
1981
  "claude_pane_elapsed": "{minutes}分{seconds}秒",
1927
1982
  "claude_pane_remaining": "残り",
1983
+ "claude_pane_reset_done": "リセット済み",
1928
1984
  "claude_pane_hours_ago": "{count}時間前"
1929
1985
  },
1930
1986
  "ko": {
@@ -1984,6 +2040,7 @@
1984
2040
  "data_stale_hint": "데이터가 대체 소스에서 왔습니다. 설정을 확인하세요.",
1985
2041
  "status_loading": "불러오는 중",
1986
2042
  "status_synced": "✓ 동기화됨",
2043
+ "status_refreshing": "↻ 새로 고치는 중…",
1987
2044
  "status_no_data": "데이터 없음",
1988
2045
  "awaiting_rate_limits": "Claude Code에 메시지를 보내 할당량을 동기화하세요",
1989
2046
  "hook_broken_not_installed": "⚠ 상태 표시줄이 업데이트되지 않습니다. \"상태 표시줄 설정\"을 눌러 다시 설치하세요.",
@@ -2003,6 +2060,7 @@
2003
2060
  "percent_used": "{value}% 사용됨",
2004
2061
  "reset_in": "{time} 후 초기화",
2005
2062
  "reset_imminent": "곧 초기화",
2063
+ "reset_done": "리셋됨",
2006
2064
  "burn_warning": "⚠ 현재 속도면 {empty} 후 소진 · 초기화까지 {reset}",
2007
2065
  "burn_warning_compact": "⚠ {empty} 후 소진",
2008
2066
  "reset_placeholder": "초기화까지 --",
@@ -2359,8 +2417,19 @@
2359
2417
  "claude_pane_week": "주간",
2360
2418
  "claude_pane_period": "기간",
2361
2419
  "claude_pane_sessions": "Claude 대화",
2420
+ "claude_pane_session_done": "완료: {title}",
2421
+ "claude_pane_session_waiting": "입력 대기: {title}",
2362
2422
  "claude_pane_busy": "작업 중",
2363
2423
  "claude_pane_waiting": "입력 대기",
2424
+ "claude_pane_unavailable_not_signed_in": "로그인 안 됨 · CLI에서 한 번 로그인",
2425
+ "claude_pane_unavailable_no_data": "아직 데이터 없음 · 한 번 사용하면 표시",
2426
+ "claude_pane_unavailable_error": "읽을 수 없음 · 자동으로 재시도",
2427
+ "claude_pane_waiting_input": "답변 필요",
2428
+ "claude_pane_waiting_dialog": "대화 상자 열림",
2429
+ "claude_pane_waiting_sandbox": "샌드박스 권한",
2430
+ "claude_pane_waiting_worker": "하위 에이전트 요청",
2431
+ "claude_pane_waiting_goal": "목표 확인",
2432
+ "claude_pane_waiting_for": "입력 대기: {reason}",
2364
2433
  "claude_pane_agent_running": "실행 중",
2365
2434
  "claude_pane_agent_idle": "유휴",
2366
2435
  "claude_pane_agent_pending": "실행 대기",
@@ -2407,6 +2476,7 @@
2407
2476
  "claude_pane_invalid_sidecar": "usage-pane.json 형식이 잘못되었습니다",
2408
2477
  "claude_pane_elapsed": "{minutes}분{seconds}초",
2409
2478
  "claude_pane_remaining": "남음",
2479
+ "claude_pane_reset_done": "리셋됨",
2410
2480
  "claude_pane_hours_ago": "{count}시간 전"
2411
2481
  }
2412
2482
  }
@@ -213,20 +213,20 @@ def _remaining_percent(window: AgyQuotaWindow) -> float:
213
213
  return max(0.0, min(100.0, float(window.remaining_percent)))
214
214
 
215
215
 
216
- def _cache_age_minutes(fetched_at: str, now: float) -> int:
217
- """Whole minutes since the cached snapshot was taken (never negative)."""
216
+ def _cache_age_minutes(fetched_at: str, now: float) -> float:
217
+ """Minutes since the cached snapshot was taken (never negative)."""
218
218
  try:
219
219
  age_seconds = now - parse_iso8601_utc_or_raise(fetched_at).timestamp()
220
220
  except (TypeError, ValueError):
221
221
  return 0
222
- return max(0, int(age_seconds // 60))
222
+ return max(0.0, age_seconds / 60)
223
223
 
224
224
 
225
225
  def _window_row(
226
226
  title: str,
227
227
  window: AgyQuotaWindow,
228
228
  language: str,
229
- age_minutes: int = 0,
229
+ age_minutes: float = 0,
230
230
  forecast_seconds: float | None = None,
231
231
  warning_max_seconds: float | None = None,
232
232
  *,
@@ -236,12 +236,20 @@ def _window_row(
236
236
  used = 100.0 - remaining
237
237
  warning = False
238
238
  warning_seconds: float | None = None
239
- if remaining == 100.0:
239
+ reset_done = window.resets_in_minutes is not None and window.resets_in_minutes <= max(
240
+ 0, age_minutes
241
+ )
242
+ if reset_done:
243
+ used = 0.0
244
+ reset_text = _t(language, "reset_done")
245
+ elif remaining == 100.0:
240
246
  reset_text = _t(language, "agy_quota_full")
241
247
  elif window.resets_in_minutes is None:
242
248
  reset_text = _t(language, "reset_placeholder")
249
+ elif window.resets_in_minutes - max(0, age_minutes) < 1:
250
+ reset_text = _t(language, "reset_imminent")
243
251
  else:
244
- minutes_left = max(1, window.resets_in_minutes - max(0, age_minutes))
252
+ minutes_left = window.resets_in_minutes - int(max(0, age_minutes))
245
253
  time_to_reset = minutes_left * 60
246
254
  if window_seconds is None:
247
255
  if (
@@ -275,12 +283,13 @@ def _window_row(
275
283
  )
276
284
  return QuotaRowState(
277
285
  title=title,
278
- percent=used,
286
+ percent=100.0 - remaining,
279
287
  percent_text=_t(language, "percent_used", value=_format_percent(used)),
280
288
  reset_text=reset_text,
281
289
  color=_bar_color(used, AGY_COLOR),
282
290
  warning=warning,
283
291
  available=True,
292
+ reset_done=reset_done,
284
293
  )
285
294
 
286
295
 
@@ -70,6 +70,7 @@ from loaders.history_loader import (
70
70
  flush_caches_on_terminate as flush_history_cache,
71
71
  )
72
72
  from menubar import actions as menubar_actions
73
+ from menubar import manual_refresh
73
74
  from menubar import menu as menubar_menu
74
75
  from menubar import notify as menubar_notify
75
76
  from menubar import refresh as menubar_refresh
@@ -419,7 +420,7 @@ class AppDelegate(NSObject):
419
420
  self._panel_window_did_hide()
420
421
 
421
422
  def refreshNow_(self, sender: Any) -> None:
422
- self._refresh(queue_if_busy=True)
423
+ manual_refresh.start_mac(self)
423
424
 
424
425
  def installHook_(self, sender: Any) -> None:
425
426
  thread = threading.Thread(target=self._install_hook_in_background, daemon=True)
@@ -822,7 +823,7 @@ class AppDelegate(NSObject):
822
823
  codex_model = result.get("codex_model", "unknown")
823
824
  self.codex_5h_pct = codex_5h_pct
824
825
  self.codex_model = codex_model
825
- self.latest_state = state
826
+ self.latest_state = manual_refresh.finish_mac(self, state)
826
827
  self._process_quota_notifications(state)
827
828
  if self._panel_window_is_visible():
828
829
  self.popover_controller.setState_(self.latest_state)
@@ -831,8 +832,7 @@ class AppDelegate(NSObject):
831
832
  menubar_title._set_button_title(self, state)
832
833
  finally:
833
834
  should_refresh_again = bool(self._refresh_queued)
834
- self._refresh_queued = False
835
- self._refresh_in_flight = False
835
+ self._refresh_queued = self._refresh_in_flight = False
836
836
  if should_refresh_again:
837
837
  self._refresh()
838
838
  if started_at:
@@ -842,7 +842,7 @@ class AppDelegate(NSObject):
842
842
  )
843
843
 
844
844
  def _clearRefreshInFlight_(self, _sender: Any) -> None:
845
- self._refresh_in_flight = False
845
+ manual_refresh.fail_mac(self)
846
846
 
847
847
  def _load_codex_refresh_result(self) -> dict[str, Any]:
848
848
  history_scan = None if self.mock else self._history_source_scan()
@@ -18,9 +18,7 @@ from menubar.state import (
18
18
  GROK_COLOR,
19
19
  GrokStaleState,
20
20
  QuotaRowState,
21
- _bar_color,
22
- _format_percent,
23
- format_human_time,
21
+ _quota_row,
24
22
  )
25
23
  from usage_common.time_utils import parse_iso8601_utc_or_raise
26
24
 
@@ -56,22 +54,15 @@ def project_quota(
56
54
  period_end = parse_iso8601_utc_or_raise(quota.period_end).timestamp()
57
55
  except (TypeError, ValueError):
58
56
  return None
59
- if period_end < current_time:
60
- return None
61
57
  stale = _stale_state(quota.fetched_at, current_time, language)
62
- used = max(0.0, min(100.0, quota.used_percent))
63
58
  return GrokQuotaProjection(
64
- weekly=QuotaRowState(
65
- title=_t(language, "weekly_label"),
66
- percent=used,
67
- percent_text=_t(language, "percent_used", value=_format_percent(used)),
68
- reset_text=_t(
69
- language,
70
- "reset_in",
71
- time=format_human_time(max(0.0, period_end - current_time), language),
72
- ),
73
- color=_bar_color(used, GROK_COLOR),
74
- available=True,
59
+ weekly=_quota_row(
60
+ _t(language, "weekly_label"),
61
+ quota.used_percent,
62
+ period_end,
63
+ current_time,
64
+ GROK_COLOR,
65
+ language,
75
66
  ),
76
67
  stale=stale,
77
68
  )
@@ -0,0 +1,51 @@
1
+ """Status-chip feedback for user-requested refreshes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from i18n import _t
8
+ from menubar.state import PopoverState
9
+
10
+
11
+ def begin(state: PopoverState, *, queued: bool = False) -> None:
12
+ if state.refresh_status is None:
13
+ state.refresh_status = (state.status_text, state.status_long)
14
+ state.refresh_queued = state.refresh_queued or queued
15
+ state.status_text = _t(
16
+ state.language, "status_text", value=_t(state.language, "status_refreshing")
17
+ )
18
+ state.status_long = False
19
+
20
+
21
+ def finish(
22
+ previous: PopoverState | None, state: PopoverState, *, queued: bool = False
23
+ ) -> PopoverState:
24
+ if previous is not None and previous.refresh_status is not None:
25
+ keep = previous.refresh_queued and queued
26
+ previous.status_text, previous.status_long = previous.refresh_status
27
+ previous.refresh_status = None
28
+ previous.refresh_queued = False
29
+ if keep:
30
+ begin(state)
31
+ return state
32
+
33
+
34
+ def finish_mac(app: Any, state: PopoverState) -> PopoverState:
35
+ return finish(app.latest_state, state, queued=bool(app._refresh_queued))
36
+
37
+
38
+ def start_mac(app: Any) -> None:
39
+ begin(app.latest_state, queued=bool(app._refresh_in_flight))
40
+ app._refresh(queue_if_busy=True)
41
+ if app._panel_window_is_visible():
42
+ app.popover_controller.setState_(app.latest_state)
43
+
44
+
45
+ def fail_mac(app: Any) -> None:
46
+ state = getattr(app, "latest_state", None)
47
+ if state is not None:
48
+ finish(state, state)
49
+ app._refresh_in_flight = False
50
+ if state is not None and app._panel_window_is_visible():
51
+ app.popover_controller.setState_(state)
@@ -99,6 +99,13 @@ class QuotaRowState:
99
99
  warning: bool = False
100
100
  available: bool = True
101
101
  reset_text_compact: str = ""
102
+ reset_done: bool = False
103
+
104
+ # Keep percent unchanged for quota notifications; renderers use display_percent.
105
+
106
+ @property
107
+ def display_percent(self) -> float | None:
108
+ return 0.0 if self.reset_done else self.percent
102
109
 
103
110
 
104
111
  class CodexStaleState(TypedDict):
@@ -156,6 +163,8 @@ class PopoverState:
156
163
  grok_stale: GrokStaleState | None = None
157
164
  card_order: tuple[str, ...] = ("claude", "codex", "agy", "grok")
158
165
  history_error: HistoryLoadErrorState | None = None
166
+ refresh_status: tuple[str, bool] | None = None
167
+ refresh_queued: bool = False
159
168
 
160
169
 
161
170
  @dataclass(frozen=True, slots=True)
@@ -970,7 +979,10 @@ def _quota_row(
970
979
  pct = max(0.0, min(100.0, float(pct)))
971
980
  time_to_reset = resets_at - now
972
981
  warning_seconds: float | None = None
973
- if time_to_reset < 60:
982
+ if time_to_reset <= 0:
983
+ reset_text = _t(language, "reset_done")
984
+ warning = False
985
+ elif time_to_reset < 60:
974
986
  reset_text = _t(language, "reset_imminent")
975
987
  warning = False
976
988
  else:
@@ -1007,11 +1019,14 @@ def _quota_row(
1007
1019
  row = QuotaRowState(
1008
1020
  title=title,
1009
1021
  percent=pct,
1010
- percent_text=_t(language, "percent_used", value=_format_percent(pct)),
1022
+ percent_text=_t(
1023
+ language, "percent_used", value=_format_percent(0.0 if time_to_reset <= 0 else pct)
1024
+ ),
1011
1025
  reset_text=reset_text,
1012
- color=_bar_color(pct, color),
1026
+ color=_bar_color(0.0 if time_to_reset <= 0 else pct, color),
1013
1027
  warning=warning,
1014
1028
  available=True,
1029
+ reset_done=time_to_reset <= 0,
1015
1030
  )
1016
1031
  if warning_seconds is not None:
1017
1032
  row.reset_text_compact = _t(
@@ -66,20 +66,22 @@ def _menubar_attributed_title(app: _TitleApp, state: PopoverState) -> Any:
66
66
  if not state.hide_claude:
67
67
  claude_percent = (
68
68
  "--"
69
- if state.claude_session.percent is None
70
- else f"{_format_percent(state.claude_session.percent)}%"
69
+ if state.claude_session.display_percent is None
70
+ else f"{_format_percent(state.claude_session.display_percent)}%"
71
71
  )
72
72
  title.appendAttributedString_(_menubar_icon_attachment_string(_claude_menubar_icon()))
73
73
  title.appendAttributedString_(_menubar_text_string(app, f" {claude_percent}"))
74
74
  if not state.hide_codex and (app.codex_5h_pct is not None or state.hide_claude):
75
- codex_percent = (
76
- "--" if app.codex_5h_pct is None else f"{_format_percent(float(app.codex_5h_pct))}%"
75
+ codex_row = (
76
+ state.codex_session if state.codex_session.percent is not None else state.codex_weekly
77
77
  )
78
+ codex_used = 0.0 if codex_row.reset_done else app.codex_5h_pct
79
+ codex_percent = "--" if codex_used is None else f"{_format_percent(float(codex_used))}%"
78
80
  if not state.hide_claude:
79
81
  title.appendAttributedString_(_menubar_text_string(app, " "))
80
82
  title.appendAttributedString_(_menubar_icon_attachment_string(_codex_menubar_icon()))
81
83
  title.appendAttributedString_(_menubar_text_string(app, f" {codex_percent}"))
82
- agy_session_percent = state.agy_session.percent
84
+ agy_session_percent = state.agy_session.display_percent
83
85
  agy_visible = not state.hide_agy and agy_session_percent is not None
84
86
  if agy_visible:
85
87
  assert agy_session_percent is not None
@@ -88,7 +90,7 @@ def _menubar_attributed_title(app: _TitleApp, state: PopoverState) -> Any:
88
90
  title.appendAttributedString_(_menubar_text_string(app, " "))
89
91
  title.appendAttributedString_(_menubar_icon_attachment_string(_agy_menubar_icon()))
90
92
  title.appendAttributedString_(_menubar_text_string(app, f" {agy_percent}"))
91
- grok_weekly_percent = state.grok_weekly.percent
93
+ grok_weekly_percent = state.grok_weekly.display_percent
92
94
  grok_visible = not state.hide_grok and grok_weekly_percent is not None
93
95
  if grok_visible:
94
96
  assert grok_weekly_percent is not None
@@ -123,18 +125,22 @@ def _compose_title(app: _TitleApp, state: PopoverState) -> str:
123
125
  if not state.hide_claude:
124
126
  claude = (
125
127
  "--"
126
- if state.claude_session.percent is None
127
- else f"{_format_percent(state.claude_session.percent)}%"
128
+ if state.claude_session.display_percent is None
129
+ else f"{_format_percent(state.claude_session.display_percent)}%"
128
130
  )
129
131
  parts.append(claude)
130
132
  if not state.hide_codex and (app.codex_5h_pct is not None or state.hide_claude):
131
- codex = "--" if app.codex_5h_pct is None else f"{_format_percent(float(app.codex_5h_pct))}%"
133
+ codex_row = (
134
+ state.codex_session if state.codex_session.percent is not None else state.codex_weekly
135
+ )
136
+ codex_used = 0.0 if codex_row.reset_done else app.codex_5h_pct
137
+ codex = "--" if codex_used is None else f"{_format_percent(float(codex_used))}%"
132
138
  parts.append(codex)
133
- if not state.hide_agy and state.agy_session.percent is not None:
134
- agy = f"{_format_percent(state.agy_session.percent)}%"
139
+ if not state.hide_agy and state.agy_session.display_percent is not None:
140
+ agy = f"{_format_percent(state.agy_session.display_percent)}%"
135
141
  parts.append(agy)
136
- if not state.hide_grok and state.grok_weekly.percent is not None:
137
- grok = f"{_format_percent(state.grok_weekly.percent)}%"
142
+ if not state.hide_grok and state.grok_weekly.display_percent is not None:
143
+ grok = f"{_format_percent(state.grok_weekly.display_percent)}%"
138
144
  parts.append(grok)
139
145
  # Both providers hidden: keep a recognizable, clickable status item.
140
146
  return " · ".join(parts) if parts else "usage"
@@ -91,7 +91,7 @@ def _data_uri(asset_name: str) -> str:
91
91
 
92
92
  def _row_payload(row: QuotaRowState) -> dict[str, object]:
93
93
  return {
94
- "percent": row.percent,
94
+ "percent": row.display_percent,
95
95
  "percentText": row.percent_text,
96
96
  "resetText": row.reset_text,
97
97
  "resetTextCompact": row.reset_text_compact or row.reset_text,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usage-cli"
3
- version = "0.32.1"
3
+ version = "0.32.3"
4
4
  description = "usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -0,0 +1,62 @@
1
+ from types import SimpleNamespace
2
+
3
+ import pytest
4
+
5
+ from i18n import _t
6
+ from menubar import manual_refresh
7
+ from menubar.state import _empty_state
8
+
9
+
10
+ @pytest.mark.parametrize("normal", ["status_synced", "data_stale_hint", "status_no_data"])
11
+ def test_manual_refresh_restores_new_status(normal: str) -> None:
12
+ previous = _empty_state()
13
+ manual_refresh.begin(previous)
14
+ assert previous.status_text == "Status: ↻ Refreshing…"
15
+ assert previous.status_long is False
16
+ state = _empty_state()
17
+ state.status_text = _t("en", "status_text", value=_t("en", normal))
18
+ state.status_long = True
19
+ assert manual_refresh.finish(previous, state) is state
20
+ assert state.status_text == _t("en", "status_text", value=_t("en", normal))
21
+ assert state.status_long is True
22
+ assert state.refresh_status is None
23
+
24
+
25
+ def test_auto_refresh_never_changes_status() -> None:
26
+ previous, state = _empty_state(), _empty_state()
27
+ expected = state.status_text
28
+ manual_refresh.finish(previous, state, queued=True)
29
+ assert state.status_text == expected
30
+ assert state.refresh_status is None
31
+
32
+
33
+ def test_manual_refresh_queued_behind_auto_waits_for_its_result() -> None:
34
+ previous = _empty_state()
35
+ manual_refresh.begin(previous, queued=True)
36
+ state = manual_refresh.finish(previous, _empty_state(), queued=True)
37
+ assert state.status_text == "Status: ↻ Refreshing…"
38
+ result = manual_refresh.finish(state, _empty_state())
39
+ assert result.status_text == _empty_state().status_text
40
+ assert result.refresh_status is None
41
+
42
+
43
+ def test_mac_manual_refresh_pushes_status_and_failure_restores_it() -> None:
44
+ state = _empty_state()
45
+ expected = state.status_text
46
+ pushed: list[str] = []
47
+ calls: list[bool] = []
48
+ app = SimpleNamespace(
49
+ latest_state=state,
50
+ _refresh_in_flight=False,
51
+ _refresh=lambda *, queue_if_busy: calls.append(queue_if_busy),
52
+ _panel_window_is_visible=lambda: True,
53
+ popover_controller=SimpleNamespace(
54
+ setState_=lambda state: pushed.append(state.status_text)
55
+ ),
56
+ )
57
+ manual_refresh.start_mac(app)
58
+ assert calls == [True]
59
+ assert pushed == ["Status: ↻ Refreshing…"]
60
+ manual_refresh.fail_mac(app)
61
+ assert pushed == ["Status: ↻ Refreshing…", expected]
62
+ assert app._refresh_in_flight is False