usage-cli 0.34.9__tar.gz → 0.34.11__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 (285) hide show
  1. {usage_cli-0.34.9/usage_cli.egg-info → usage_cli-0.34.11}/PKG-INFO +1 -1
  2. {usage_cli-0.34.9 → usage_cli-0.34.11}/adapters/rate_limits.py +4 -4
  3. {usage_cli-0.34.9 → usage_cli-0.34.11}/analyzer/persona_loader.py +2 -1
  4. {usage_cli-0.34.9 → usage_cli-0.34.11}/i18n.json +75 -0
  5. {usage_cli-0.34.9 → usage_cli-0.34.11}/installer/setup_hook.py +1 -1
  6. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/claude_usage.py +20 -14
  7. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/agy.py +9 -0
  8. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/app.py +2 -3
  9. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/state.py +4 -1
  10. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/title.py +63 -8
  11. {usage_cli-0.34.9 → usage_cli-0.34.11}/pyproject.toml +1 -1
  12. {usage_cli-0.34.9 → usage_cli-0.34.11}/quota/window_keeper.py +35 -4
  13. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_adapters_rate_limits.py +41 -0
  14. usage_cli-0.34.11/tests/test_claude_expired_windows.py +86 -0
  15. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_desktop_quota_review.py +1 -1
  16. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_html_report_snapshot.py +29 -24
  17. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_menubar.py +172 -1
  18. usage_cli-0.34.11/tests/test_report_chart_redesign.py +218 -0
  19. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_report_cold_read.py +1 -1
  20. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_report_filter_js.py +40 -22
  21. usage_cli-0.34.11/tests/test_report_headings.py +373 -0
  22. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_client.py +25 -0
  23. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_statusline.py +110 -0
  24. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_window_keeper.py +76 -0
  25. {usage_cli-0.34.9 → usage_cli-0.34.11}/ui/html_report.py +276 -120
  26. usage_cli-0.34.11/ui/report_charts.py +62 -0
  27. {usage_cli-0.34.9 → usage_cli-0.34.11}/ui/report_filter.py +210 -128
  28. {usage_cli-0.34.9 → usage_cli-0.34.11}/ui/report_styles.py +100 -0
  29. {usage_cli-0.34.9 → usage_cli-0.34.11/usage_cli.egg-info}/PKG-INFO +1 -1
  30. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_cli.egg-info/SOURCES.txt +3 -0
  31. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_hooks/usage_statusline.py +57 -6
  32. usage_cli-0.34.9/ui/report_charts.py +0 -26
  33. {usage_cli-0.34.9 → usage_cli-0.34.11}/LICENSE +0 -0
  34. {usage_cli-0.34.9 → usage_cli-0.34.11}/README.md +0 -0
  35. {usage_cli-0.34.9 → usage_cli-0.34.11}/THIRD_PARTY_NOTICES.md +0 -0
  36. {usage_cli-0.34.9 → usage_cli-0.34.11}/adapters/__init__.py +0 -0
  37. {usage_cli-0.34.9 → usage_cli-0.34.11}/adapters/agy.py +0 -0
  38. {usage_cli-0.34.9 → usage_cli-0.34.11}/adapters/claude.py +0 -0
  39. {usage_cli-0.34.9 → usage_cli-0.34.11}/adapters/codex.py +0 -0
  40. {usage_cli-0.34.9 → usage_cli-0.34.11}/adapters/grok.py +0 -0
  41. {usage_cli-0.34.9 → usage_cli-0.34.11}/adapters/muse.py +0 -0
  42. {usage_cli-0.34.9 → usage_cli-0.34.11}/adapters/registry.py +0 -0
  43. {usage_cli-0.34.9 → usage_cli-0.34.11}/adapters/types.py +0 -0
  44. {usage_cli-0.34.9 → usage_cli-0.34.11}/analyzer/__init__.py +0 -0
  45. {usage_cli-0.34.9 → usage_cli-0.34.11}/analyzer/aggregator.py +0 -0
  46. {usage_cli-0.34.9 → usage_cli-0.34.11}/analyzer/blocks.py +0 -0
  47. {usage_cli-0.34.9 → usage_cli-0.34.11}/analyzer/diagnoser.py +0 -0
  48. {usage_cli-0.34.9 → usage_cli-0.34.11}/analyzer/diagnosis_snapshot.py +0 -0
  49. {usage_cli-0.34.9 → usage_cli-0.34.11}/analyzer/dispatch_ledger.py +0 -0
  50. {usage_cli-0.34.9 → usage_cli-0.34.11}/analyzer/history_persist.py +0 -0
  51. {usage_cli-0.34.9 → usage_cli-0.34.11}/analyzer/insights.py +0 -0
  52. {usage_cli-0.34.9 → usage_cli-0.34.11}/analyzer/ledger_backfill.py +0 -0
  53. {usage_cli-0.34.9 → usage_cli-0.34.11}/analyzer/reporter.py +0 -0
  54. {usage_cli-0.34.9 → usage_cli-0.34.11}/analyzer/subscription.py +0 -0
  55. {usage_cli-0.34.9 → usage_cli-0.34.11}/analyzer/usage_snapshot.py +0 -0
  56. {usage_cli-0.34.9 → usage_cli-0.34.11}/installer/__init__.py +0 -0
  57. {usage_cli-0.34.9 → usage_cli-0.34.11}/installer/claude_pane.py +0 -0
  58. {usage_cli-0.34.9 → usage_cli-0.34.11}/installer/codex_hook_trust.py +0 -0
  59. {usage_cli-0.34.9 → usage_cli-0.34.11}/installer/login_item.py +0 -0
  60. {usage_cli-0.34.9 → usage_cli-0.34.11}/installer/session_hooks.py +0 -0
  61. {usage_cli-0.34.9 → usage_cli-0.34.11}/installer/statusline_settings.py +0 -0
  62. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/__init__.py +0 -0
  63. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/_shared_cache_file.py +0 -0
  64. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/agy_disk_cache.py +0 -0
  65. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/agy_loader.py +0 -0
  66. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/agy_quota_probe.py +0 -0
  67. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/cache_quarantine.py +0 -0
  68. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/chromium_cache.py +0 -0
  69. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/claude_desktop.py +0 -0
  70. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/claude_paths.py +0 -0
  71. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/codex_disk_cache.py +0 -0
  72. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/codex_events.py +0 -0
  73. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/codex_fork_replay.py +0 -0
  74. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/codex_loader.py +0 -0
  75. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/codex_paths.py +0 -0
  76. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/disk_cache_common.py +0 -0
  77. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/disk_cache_lifecycle.py +0 -0
  78. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/grok_loader.py +0 -0
  79. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/grok_quota_probe.py +0 -0
  80. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/history_disk_cache.py +0 -0
  81. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/history_loader.py +0 -0
  82. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/jsonl_limits.py +0 -0
  83. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/jsonl_utils.py +0 -0
  84. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/muse_loader.py +0 -0
  85. {usage_cli-0.34.9 → usage_cli-0.34.11}/loaders/muse_quota_probe.py +0 -0
  86. {usage_cli-0.34.9 → usage_cli-0.34.11}/main.py +0 -0
  87. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/__init__.py +0 -0
  88. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/actions.py +0 -0
  89. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/chrome.py +0 -0
  90. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/fsevents_watch.py +0 -0
  91. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/grok.py +0 -0
  92. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/manual_refresh.py +0 -0
  93. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/menu.py +0 -0
  94. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/muse.py +0 -0
  95. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/new_badge.py +0 -0
  96. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/notify.py +0 -0
  97. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/popover.py +0 -0
  98. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/prefs.py +0 -0
  99. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/refresh.py +0 -0
  100. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/update.py +0 -0
  101. {usage_cli-0.34.9 → usage_cli-0.34.11}/menubar/whats_new.py +0 -0
  102. {usage_cli-0.34.9 → usage_cli-0.34.11}/panels/__init__.py +0 -0
  103. {usage_cli-0.34.9 → usage_cli-0.34.11}/panels/base.py +0 -0
  104. {usage_cli-0.34.9 → usage_cli-0.34.11}/panels/catalog.py +0 -0
  105. {usage_cli-0.34.9 → usage_cli-0.34.11}/panels/dynamic_height.py +0 -0
  106. {usage_cli-0.34.9 → usage_cli-0.34.11}/panels/panel_scale.py +0 -0
  107. {usage_cli-0.34.9 → usage_cli-0.34.11}/panels/panel_window.py +0 -0
  108. {usage_cli-0.34.9 → usage_cli-0.34.11}/panels/panel_window_state.py +0 -0
  109. {usage_cli-0.34.9 → usage_cli-0.34.11}/panels/payload.py +0 -0
  110. {usage_cli-0.34.9 → usage_cli-0.34.11}/panels/web_panel.py +0 -0
  111. {usage_cli-0.34.9 → usage_cli-0.34.11}/panels/window_drag.py +0 -0
  112. {usage_cli-0.34.9 → usage_cli-0.34.11}/quota/__init__.py +0 -0
  113. {usage_cli-0.34.9 → usage_cli-0.34.11}/quota/agy_window_keeper.py +0 -0
  114. {usage_cli-0.34.9 → usage_cli-0.34.11}/quota/burn_rate.py +0 -0
  115. {usage_cli-0.34.9 → usage_cli-0.34.11}/quota/codex_window_keeper.py +0 -0
  116. {usage_cli-0.34.9 → usage_cli-0.34.11}/quota/keeper_outcome.py +0 -0
  117. {usage_cli-0.34.9 → usage_cli-0.34.11}/quota/notifications.py +0 -0
  118. {usage_cli-0.34.9 → usage_cli-0.34.11}/quota/quota_snapshot.py +0 -0
  119. {usage_cli-0.34.9 → usage_cli-0.34.11}/quota/status_payload.py +0 -0
  120. {usage_cli-0.34.9 → usage_cli-0.34.11}/quota/usage_rate.py +0 -0
  121. {usage_cli-0.34.9 → usage_cli-0.34.11}/setup.cfg +0 -0
  122. {usage_cli-0.34.9 → usage_cli-0.34.11}/setup_app.py +0 -0
  123. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_adapters_claude.py +0 -0
  124. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_adapters_codex.py +0 -0
  125. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_aggregator.py +0 -0
  126. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_agy_adapter.py +0 -0
  127. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_agy_disk_cache.py +0 -0
  128. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_agy_loader.py +0 -0
  129. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_agy_quota_probe.py +0 -0
  130. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_agy_window_keeper.py +0 -0
  131. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_analyzer_blocks.py +0 -0
  132. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_analyzer_pipeline.py +0 -0
  133. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_app_version.py +0 -0
  134. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_burn_rate.py +0 -0
  135. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_cache_quarantine.py +0 -0
  136. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_check_upstream.py +0 -0
  137. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_chromium_cache.py +0 -0
  138. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_claude_desktop.py +0 -0
  139. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_claude_pane.py +0 -0
  140. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_claude_paths.py +0 -0
  141. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_codex_consistency.py +0 -0
  142. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_codex_hook_trust.py +0 -0
  143. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_codex_loader.py +0 -0
  144. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_codex_window_keeper.py +0 -0
  145. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_compact_cards.py +0 -0
  146. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_desktop_fresh_cache.py +0 -0
  147. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_desktop_quota_display.py +0 -0
  148. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_diagnoser.py +0 -0
  149. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_disk_cache_shards.py +0 -0
  150. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_dispatch_ledger.py +0 -0
  151. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_doctor.py +0 -0
  152. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_dynamic_height.py +0 -0
  153. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_fsevents_watch.py +0 -0
  154. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_grok_loader.py +0 -0
  155. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_grok_quota_probe.py +0 -0
  156. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_history_loader.py +0 -0
  157. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_history_persist.py +0 -0
  158. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_hook_scripts_are_standalone.py +0 -0
  159. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_i18n_format_fallback.py +0 -0
  160. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_i18n_key_parity.py +0 -0
  161. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_i18n_packaged_path.py +0 -0
  162. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_insights.py +0 -0
  163. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_jsonl_utils.py +0 -0
  164. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_keeper_menu.py +0 -0
  165. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_keeper_outcome.py +0 -0
  166. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_ledger_backfill.py +0 -0
  167. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_login_item.py +0 -0
  168. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_main.py +0 -0
  169. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_manual_refresh.py +0 -0
  170. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_menubar_agy.py +0 -0
  171. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_menubar_grok.py +0 -0
  172. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_menubar_muse.py +0 -0
  173. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_menubar_notify.py +0 -0
  174. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_menubar_prefs.py +0 -0
  175. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_menubar_state.py +0 -0
  176. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_menubar_update.py +0 -0
  177. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_muse_coverage.py +0 -0
  178. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_muse_loader.py +0 -0
  179. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_muse_quota_probe.py +0 -0
  180. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_muse_review_regressions.py +0 -0
  181. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_new_badge.py +0 -0
  182. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_packaged_resources.py +0 -0
  183. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_packaging.py +0 -0
  184. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_panel_footer_layout.py +0 -0
  185. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_panel_scale.py +0 -0
  186. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_panel_window_state.py +0 -0
  187. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_panels.py +0 -0
  188. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_panels_lazy_import.py +0 -0
  189. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_persona_loader.py +0 -0
  190. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_pricing.py +0 -0
  191. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_project_resolver.py +0 -0
  192. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_quota_aware.py +0 -0
  193. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_quota_label.py +0 -0
  194. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_report_daily_chart.py +0 -0
  195. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_report_share_mask.py +0 -0
  196. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_reporter_build_data.py +0 -0
  197. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_service_status.py +0 -0
  198. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_session_resume.py +0 -0
  199. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_setup_app.py +0 -0
  200. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_setup_hook.py +0 -0
  201. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_setup_hook_coexistence.py +0 -0
  202. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_setup_hook_resume.py +0 -0
  203. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_setup_hook_terse.py +0 -0
  204. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_subprocess_utils.py +0 -0
  205. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_subscription.py +0 -0
  206. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_taskbar_menu.py +0 -0
  207. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_taskbar_obstacles.py +0 -0
  208. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_time_utils.py +0 -0
  209. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_tui.py +0 -0
  210. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_tui_sprite.py +0 -0
  211. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_ui_tables.py +0 -0
  212. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_update_checker.py +0 -0
  213. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_update_gate.py +0 -0
  214. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_update_release_notes.py +0 -0
  215. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_cli.py +0 -0
  216. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_diagnosis_snapshot.py +0 -0
  217. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_dir_sweeper.py +0 -0
  218. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_lang.py +0 -0
  219. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_logging.py +0 -0
  220. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_notifications.py +0 -0
  221. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_rate.py +0 -0
  222. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_snapshot.py +0 -0
  223. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_snapshot_replay.py +0 -0
  224. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_statusline_agy.py +0 -0
  225. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_statusline_forwarder.py +0 -0
  226. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_statusline_grok.py +0 -0
  227. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_terse_mode.py +0 -0
  228. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_usage_terse_reminder.py +0 -0
  229. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_web_panel_payload.py +0 -0
  230. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_whats_new.py +0 -0
  231. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_wheel_packaging.py +0 -0
  232. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_win_login_item.py +0 -0
  233. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_windows_manifest.py +0 -0
  234. {usage_cli-0.34.9 → usage_cli-0.34.11}/tests/test_wintray.py +0 -0
  235. {usage_cli-0.34.9 → usage_cli-0.34.11}/tui/__init__.py +0 -0
  236. {usage_cli-0.34.9 → usage_cli-0.34.11}/tui/app.py +0 -0
  237. {usage_cli-0.34.9 → usage_cli-0.34.11}/tui/sprite.py +0 -0
  238. {usage_cli-0.34.9 → usage_cli-0.34.11}/ui/__init__.py +0 -0
  239. {usage_cli-0.34.9 → usage_cli-0.34.11}/ui/report_daily_chart.py +0 -0
  240. {usage_cli-0.34.9 → usage_cli-0.34.11}/ui/report_ledger.py +0 -0
  241. {usage_cli-0.34.9 → usage_cli-0.34.11}/ui/report_scripts.py +0 -0
  242. {usage_cli-0.34.9 → usage_cli-0.34.11}/ui/tables.py +0 -0
  243. {usage_cli-0.34.9 → usage_cli-0.34.11}/updates/__init__.py +0 -0
  244. {usage_cli-0.34.9 → usage_cli-0.34.11}/updates/checker.py +0 -0
  245. {usage_cli-0.34.9 → usage_cli-0.34.11}/updates/gate.py +0 -0
  246. {usage_cli-0.34.9 → usage_cli-0.34.11}/updates/release_notes.py +0 -0
  247. {usage_cli-0.34.9 → usage_cli-0.34.11}/updates/whats_new.py +0 -0
  248. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_app/__init__.py +0 -0
  249. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_app/cli.py +0 -0
  250. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_app/doctor.py +0 -0
  251. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_cli.egg-info/dependency_links.txt +0 -0
  252. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_cli.egg-info/entry_points.txt +0 -0
  253. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_cli.egg-info/requires.txt +0 -0
  254. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_cli.egg-info/top_level.txt +0 -0
  255. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_common/__init__.py +0 -0
  256. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_common/app_version.py +0 -0
  257. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_common/i18n.py +0 -0
  258. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_common/prefs.py +0 -0
  259. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_common/pricing.py +0 -0
  260. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_common/project_resolver.py +0 -0
  261. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_common/service_status.py +0 -0
  262. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_common/subprocess_utils.py +0 -0
  263. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_common/time_utils.py +0 -0
  264. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_common/usage_dir_sweeper.py +0 -0
  265. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_common/usage_lang.py +0 -0
  266. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_common/usage_logging.py +0 -0
  267. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_hooks/__init__.py +0 -0
  268. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_hooks/usage_quota_aware.py +0 -0
  269. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_hooks/usage_session_resume.py +0 -0
  270. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_hooks/usage_statusline_agy.py +0 -0
  271. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_hooks/usage_statusline_forwarder.py +0 -0
  272. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_hooks/usage_statusline_grok.py +0 -0
  273. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_hooks/usage_terse_mode.py +0 -0
  274. {usage_cli-0.34.9 → usage_cli-0.34.11}/usage_hooks/usage_terse_reminder.py +0 -0
  275. {usage_cli-0.34.9 → usage_cli-0.34.11}/wintray/__init__.py +0 -0
  276. {usage_cli-0.34.9 → usage_cli-0.34.11}/wintray/app.py +0 -0
  277. {usage_cli-0.34.9 → usage_cli-0.34.11}/wintray/login_item.py +0 -0
  278. {usage_cli-0.34.9 → usage_cli-0.34.11}/wintray/menu.py +0 -0
  279. {usage_cli-0.34.9 → usage_cli-0.34.11}/wintray/prefs.py +0 -0
  280. {usage_cli-0.34.9 → usage_cli-0.34.11}/wintray/quota_label.py +0 -0
  281. {usage_cli-0.34.9 → usage_cli-0.34.11}/wintray/taskbar_menu.py +0 -0
  282. {usage_cli-0.34.9 → usage_cli-0.34.11}/wintray/taskbar_obstacles.py +0 -0
  283. {usage_cli-0.34.9 → usage_cli-0.34.11}/wintray/taskbar_overlay.py +0 -0
  284. {usage_cli-0.34.9 → usage_cli-0.34.11}/wintray/watch.py +0 -0
  285. {usage_cli-0.34.9 → usage_cli-0.34.11}/wintray/whats_new.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usage-cli
3
- Version: 0.34.9
3
+ Version: 0.34.11
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
@@ -44,16 +44,16 @@ def _read_status() -> dict[str, Any] | None:
44
44
 
45
45
  def load_rate_limits() -> RateLimits | None:
46
46
  data = _read_status()
47
- if data is None or not usage_client._has_complete_rate_limits(data):
47
+ if data is None or not usage_client._has_rate_limits(data):
48
48
  snapshot = usage_client.read_quota_fallback()
49
49
  if snapshot is not None:
50
50
  return _snapshot_rate_limits(snapshot)
51
51
  if data is None:
52
52
  return None
53
53
 
54
- rl = data.get("rate_limits") or {}
55
- five = rl.get("five_hour") or {}
56
- seven = rl.get("seven_day") or {}
54
+ rl = usage_client._as_dict(data.get("rate_limits"))
55
+ five = usage_client._as_dict(rl.get("five_hour"))
56
+ seven = usage_client._as_dict(rl.get("seven_day"))
57
57
 
58
58
  now_ts = datetime.now(timezone.utc).timestamp()
59
59
  five_pct = _as_finite_float(five.get("used_percentage"))
@@ -22,7 +22,8 @@ from usage_common.time_utils import parse_optional_iso8601_utc
22
22
 
23
23
  logger = logging.getLogger(__name__)
24
24
 
25
- _CACHE_TTL_SECONDS = 300.0
25
+ # Habits move slowly, while a rescan reads every session file (~20s on heavy users).
26
+ _CACHE_TTL_SECONDS = 3600.0
26
27
  _cache: dict[int, tuple[float, PersonaProfile]] = {}
27
28
 
28
29
 
@@ -1,5 +1,19 @@
1
1
  {
2
2
  "zh-TW": {
3
+ "report_title_tools_top": "{tool} 用得最多,佔 {pct}%",
4
+ "report_title_tools_single": "全部用量來自 {tool}",
5
+ "report_sub_tools": "各工具 token 佔比 · 共 {total} tokens",
6
+ "report_title_projects_top3": "前 3 個專案佔了 {pct}% 的用量",
7
+ "report_sub_projects": "各專案 token 佔比",
8
+ "report_title_trend_peak_week": "{date} 那週用量最高",
9
+ "report_sub_trend": "每日與每週 token 用量",
10
+ "report_title_composition_top": "{pct}% 的 token 是{kind}",
11
+ "report_sub_composition": "各類 token 佔比",
12
+ "report_title_sessions_top": "最大的一段會話用了 {tokens} tokens",
13
+ "report_title_streak": "已經連續 {days} 天每天都有用",
14
+ "report_source_note": "資料來源:本機 {tools} 使用紀錄 · {from} – {to}",
15
+ "report_source_pricing": "API 等值依公開 API 價目換算",
16
+ "report_weekly_chart_title": "每週用量",
3
17
  "panel_default_name": "預設",
4
18
  "panel_load_error": "面板載入失敗,請回報這個問題:",
5
19
  "panel_matrix": "駭客任務",
@@ -545,6 +559,7 @@
545
559
  "report_project_others": "其他專案",
546
560
  "report_ledger_lead": "Claude 派給 Codex、Antigravity 的工作,各吃掉多少週額度。",
547
561
  "report_trend_partial_week": "僅 {days} 天",
562
+ "report_trend_incomplete_week": "不完整週",
548
563
  "report_history_scope": "分工具紀錄從 {date} 起;更早的每日總量只在下方熱力圖",
549
564
  "report_composition_missing": "另有 {tokens} tokens 沒有輸入/輸出拆解",
550
565
  "report_wrapped_cost_scope": "只含有價格紀錄的日子({date} 起)",
@@ -558,6 +573,20 @@
558
573
  "report_ledger_all_reset": "最近 7 天的派工都在額度重置前,算在上週。"
559
574
  },
560
575
  "en": {
576
+ "report_title_tools_top": "{tool} leads with {pct}% of tokens",
577
+ "report_title_tools_single": "All usage came from {tool}",
578
+ "report_sub_tools": "Token share by tool · {total} tokens in total",
579
+ "report_title_projects_top3": "The top 3 projects account for {pct}% of tokens",
580
+ "report_sub_projects": "Token share by project",
581
+ "report_title_trend_peak_week": "The week of {date} was the busiest",
582
+ "report_sub_trend": "Daily and weekly token usage",
583
+ "report_title_composition_top": "{kind} makes up {pct}% of tokens",
584
+ "report_sub_composition": "Token share by type",
585
+ "report_title_sessions_top": "The largest session used {tokens} tokens",
586
+ "report_title_streak": "{days} days in a row with usage",
587
+ "report_source_note": "Source: local {tools} usage logs · {from} – {to}",
588
+ "report_source_pricing": "API-equivalent cost is based on public API prices",
589
+ "report_weekly_chart_title": "Weekly usage",
561
590
  "panel_default_name": "Default",
562
591
  "panel_load_error": "Panel failed to load. Please report this:",
563
592
  "panel_matrix": "Matrix",
@@ -1103,6 +1132,7 @@
1103
1132
  "report_project_others": "Other projects",
1104
1133
  "report_ledger_lead": "How much of each weekly quota went to work Claude handed to Codex and Antigravity.",
1105
1134
  "report_trend_partial_week": "only {days} days",
1135
+ "report_trend_incomplete_week": "Incomplete week",
1106
1136
  "report_history_scope": "Per-tool records start on {date}; earlier daily totals appear only in the heatmap below.",
1107
1137
  "report_composition_missing": "Another {tokens} tokens have no input/output breakdown.",
1108
1138
  "report_wrapped_cost_scope": "Only days with price records (from {date})",
@@ -1116,6 +1146,20 @@
1116
1146
  "report_ledger_all_reset": "All dispatched work in the last 7 days came before the quota reset, so it counts toward last week."
1117
1147
  },
1118
1148
  "zh-CN": {
1149
+ "report_title_tools_top": "{tool} 用得最多,占 {pct}%",
1150
+ "report_title_tools_single": "全部用量来自 {tool}",
1151
+ "report_sub_tools": "各工具 token 占比 · 共 {total} tokens",
1152
+ "report_title_projects_top3": "前 3 个项目占了 {pct}% 的用量",
1153
+ "report_sub_projects": "各项目 token 占比",
1154
+ "report_title_trend_peak_week": "{date} 那周用量最高",
1155
+ "report_sub_trend": "每日与每周 token 用量",
1156
+ "report_title_composition_top": "{pct}% 的 token 是{kind}",
1157
+ "report_sub_composition": "各类 token 占比",
1158
+ "report_title_sessions_top": "最大的一段会话用了 {tokens} tokens",
1159
+ "report_title_streak": "已经连续 {days} 天每天都有使用",
1160
+ "report_source_note": "数据来源:本机 {tools} 使用记录 · {from} – {to}",
1161
+ "report_source_pricing": "API 等值按公开 API 价格换算",
1162
+ "report_weekly_chart_title": "每周用量",
1119
1163
  "panel_default_name": "默认",
1120
1164
  "panel_load_error": "面板加载失败,请反馈这个问题:",
1121
1165
  "panel_matrix": "黑客任务",
@@ -1661,6 +1705,7 @@
1661
1705
  "report_project_others": "其他项目",
1662
1706
  "report_ledger_lead": "Claude 派给 Codex、Antigravity 的工作,各吃掉多少周额度。",
1663
1707
  "report_trend_partial_week": "仅 {days} 天",
1708
+ "report_trend_incomplete_week": "不完整周",
1664
1709
  "report_history_scope": "分工具记录从 {date} 起;更早的每日总量只在下方热力图",
1665
1710
  "report_composition_missing": "另有 {tokens} tokens 没有输入/输出拆解",
1666
1711
  "report_wrapped_cost_scope": "只含有价格记录的日子({date} 起)",
@@ -1674,6 +1719,20 @@
1674
1719
  "report_ledger_all_reset": "最近 7 天的派工都在额度重置前,算在上周。"
1675
1720
  },
1676
1721
  "ja": {
1722
+ "report_title_tools_top": "{tool} が最多で {pct}%",
1723
+ "report_title_tools_single": "すべての使用量は {tool} から",
1724
+ "report_sub_tools": "ツール別 token 比率 · 合計 {total} tokens",
1725
+ "report_title_projects_top3": "上位 3 プロジェクトが使用量の {pct}%",
1726
+ "report_sub_projects": "プロジェクト別 token 比率",
1727
+ "report_title_trend_peak_week": "{date} の週が使用量最多",
1728
+ "report_sub_trend": "日別・週別 token 使用量",
1729
+ "report_title_composition_top": "token の {pct}% は{kind}",
1730
+ "report_sub_composition": "種類別 token 比率",
1731
+ "report_title_sessions_top": "最大のセッションは {tokens} tokens",
1732
+ "report_title_streak": "{days} 日連続で毎日使用中",
1733
+ "report_source_note": "データ元:ローカルの {tools} 使用記録 · {from} – {to}",
1734
+ "report_source_pricing": "API 相当額は公開 API 料金で換算",
1735
+ "report_weekly_chart_title": "週別使用量",
1677
1736
  "panel_default_name": "デフォルト",
1678
1737
  "panel_load_error": "パネルの読み込みに失敗しました。報告してください:",
1679
1738
  "panel_matrix": "マトリックス",
@@ -2219,6 +2278,7 @@
2219
2278
  "report_project_others": "その他のプロジェクト",
2220
2279
  "report_ledger_lead": "Claude が Codex・Antigravity に任せた作業が、それぞれの週間クォータをどれだけ使ったか。",
2221
2280
  "report_trend_partial_week": "{days} 日分のみ",
2281
+ "report_trend_incomplete_week": "不完全な週",
2222
2282
  "report_history_scope": "ツール別記録は {date} から。それ以前の日別合計は下のヒートマップにのみ表示します。",
2223
2283
  "report_composition_missing": "ほかに {tokens} tokens は入力/出力の内訳がありません。",
2224
2284
  "report_wrapped_cost_scope": "価格記録のある日のみ({date} 以降)",
@@ -2232,6 +2292,20 @@
2232
2292
  "report_ledger_all_reset": "直近 7 日間に委任した作業はすべてクォータのリセット前のため、先週分に含まれます。"
2233
2293
  },
2234
2294
  "ko": {
2295
+ "report_title_tools_top": "{tool} 사용량이 {pct}%로 가장 많음",
2296
+ "report_title_tools_single": "모든 사용량은 {tool}에서 발생",
2297
+ "report_sub_tools": "도구별 token 비율 · 총 {total} tokens",
2298
+ "report_title_projects_top3": "상위 3개 프로젝트가 사용량의 {pct}%",
2299
+ "report_sub_projects": "프로젝트별 token 비율",
2300
+ "report_title_trend_peak_week": "{date} 주의 사용량이 가장 많음",
2301
+ "report_sub_trend": "일별 및 주별 token 사용량",
2302
+ "report_title_composition_top": "token의 {pct}%는 {kind}",
2303
+ "report_sub_composition": "유형별 token 비율",
2304
+ "report_title_sessions_top": "가장 큰 세션에서 {tokens} tokens 사용",
2305
+ "report_title_streak": "{days}일 연속 매일 사용 중",
2306
+ "report_source_note": "데이터 출처: 로컬 {tools} 사용 기록 · {from} – {to}",
2307
+ "report_source_pricing": "API 환산 비용은 공개 API 요금 기준",
2308
+ "report_weekly_chart_title": "주별 사용량",
2235
2309
  "panel_default_name": "기본",
2236
2310
  "panel_load_error": "패널을 불러오지 못했습니다. 제보해 주세요:",
2237
2311
  "panel_matrix": "매트릭스",
@@ -2777,6 +2851,7 @@
2777
2851
  "report_project_others": "기타 프로젝트",
2778
2852
  "report_ledger_lead": "Claude가 Codex와 Antigravity에 맡긴 작업이 각 주간 한도를 얼마나 썼는지 보여 줍니다.",
2779
2853
  "report_trend_partial_week": "{days}일만 포함",
2854
+ "report_trend_incomplete_week": "불완전한 주",
2780
2855
  "report_history_scope": "도구별 기록은 {date}부터입니다. 이전 일별 합계는 아래 히트맵에만 표시됩니다.",
2781
2856
  "report_composition_missing": "추가 {tokens} tokens에는 입력/출력 내역이 없습니다.",
2782
2857
  "report_wrapped_cost_scope": "가격 기록이 있는 날만 포함 ({date}부터)",
@@ -86,7 +86,7 @@ PREV_SL_KEY = "previousStatusLine"
86
86
  # Where the status-line switch parks the live statusLine while it is off, so it
87
87
  # never overwrites the pre-install backup under PREV_SL_KEY.
88
88
  DISABLED_SL_KEY = "disabledStatusLine"
89
- HOOK_VERSION = "1.14"
89
+ HOOK_VERSION = "1.17"
90
90
  FORWARDER_VERSION = "1.1"
91
91
  # Antigravity's Go runner hands its status-line command to cmd.exe unquoted, so
92
92
  # every character cmd.exe treats specially has to be kept out of the path.
@@ -293,17 +293,20 @@ def _hook_broken_message(now: float, polled_at: float) -> str | None:
293
293
  return HOOK_BROKEN_NOT_INSTALLED
294
294
 
295
295
 
296
- def _has_complete_rate_limits(data: dict[str, Any]) -> bool:
296
+ def _has_rate_limits(data: dict[str, Any]) -> bool:
297
+ """True when every window the hook sent is valid and at least one was sent.
298
+
299
+ Plans without a 5-hour limit never send five_hour, so requiring both windows
300
+ would leave them on the cache or the loading state forever. A window that is
301
+ present but unreadable still falls back to the cache.
302
+ """
297
303
  rl = data.get("rate_limits")
298
304
  if not isinstance(rl, dict):
299
305
  return False
300
- five = rl.get("five_hour")
301
- seven = rl.get("seven_day")
302
- if not isinstance(five, dict) or not isinstance(seven, dict):
303
- return False
304
- return (
305
- _as_finite_float(five.get("used_percentage")) is not None
306
- and _as_finite_float(seven.get("used_percentage")) is not None
306
+ windows = [w for w in (rl.get("five_hour"), rl.get("seven_day")) if w is not None]
307
+ return bool(windows) and all(
308
+ isinstance(w, dict) and _as_finite_float(w.get("used_percentage")) is not None
309
+ for w in windows
307
310
  )
308
311
 
309
312
 
@@ -345,7 +348,10 @@ def _build_snapshot(data: dict[str, Any], *, data_source: str = "hook") -> Usage
345
348
 
346
349
  return UsageSnapshot(
347
350
  current_percent=five_pct,
348
- current_reset_at=five_reset,
351
+ # Keep an unknown hook reset unknown so the keeper cannot remember a
352
+ # parse-time default as a real boundary. Display expiry logic above
353
+ # still uses the default to preserve the reported percentage.
354
+ current_reset_at=_as_finite_float(five.get("resets_at")),
349
355
  weekly_percent=seven_pct,
350
356
  weekly_reset_at=seven_reset,
351
357
  current_status=status,
@@ -440,11 +446,11 @@ class ClaudeUsageClient:
440
446
 
441
447
  # ``.claude.json`` is Claude Code's cache, not a competing live source.
442
448
  # In particular its fetchedAtMs can be newer than the hook timestamp while
443
- # still describing a different/expired session. A complete statusLine
444
- # payload must therefore always win; use the cache only when the hook has
445
- # not provided both quota windows yet. Desktop history is also a fallback;
446
- # it never replaces a complete hook payload or supplies invented resets.
447
- if not _has_complete_rate_limits(data):
449
+ # still describing a different/expired session. A statusLine payload
450
+ # with quota must therefore always win; use the cache only when the hook
451
+ # has not provided any quota window yet. Desktop history is also a fallback;
452
+ # it never replaces a hook payload or supplies invented resets.
453
+ if not _has_rate_limits(data):
448
454
  fallback_snapshot = read_quota_fallback(self._read_claude_json_snapshot_cached)
449
455
  if fallback_snapshot is not None:
450
456
  return self._success_outcome(fallback_snapshot)
@@ -124,6 +124,7 @@ def project_quota(
124
124
  language,
125
125
  age_minutes,
126
126
  forecast_seconds=session_forecast,
127
+ now=current_time,
127
128
  )
128
129
  weekly = _window_row(
129
130
  _t(language, "weekly_label"),
@@ -133,6 +134,7 @@ def project_quota(
133
134
  forecast_seconds=weekly_forecast,
134
135
  warning_max_seconds=AGY_WEEKLY_WARNING_MAX_SECONDS,
135
136
  window_seconds=WEEKLY_WINDOW_SECONDS,
137
+ now=current_time,
136
138
  )
137
139
  # Google pauses the 5-hour bucket once the weekly one is spent; showing it
138
140
  # as "full" reads as usable, so say it is paused instead.
@@ -278,6 +280,7 @@ def _window_row(
278
280
  warning_max_seconds: float | None = None,
279
281
  *,
280
282
  window_seconds: float | None = None,
283
+ now: float | None = None,
281
284
  ) -> QuotaRowState:
282
285
  if window is None:
283
286
  return QuotaRowState(
@@ -346,6 +349,12 @@ def _window_row(
346
349
  warning=warning,
347
350
  available=True,
348
351
  reset_done=reset_done,
352
+ resets_at=(
353
+ (time.time() if now is None else now)
354
+ + (window.resets_in_minutes - max(0, age_minutes)) * 60
355
+ if window.resets_in_minutes is not None
356
+ else None
357
+ ),
349
358
  )
350
359
 
351
360
 
@@ -189,7 +189,6 @@ __all__ = [
189
189
  ]
190
190
 
191
191
  UPDATE_ALERT_BODY_LIMIT = 2000
192
- SLOW_POLL_INTERVAL_S = 300.0
193
192
 
194
193
  logger = logging.getLogger(__name__)
195
194
 
@@ -317,7 +316,7 @@ class AppDelegate(NSObject):
317
316
 
318
317
  self._request_notification_authorization()
319
318
  self._refresh()
320
- self._reschedule_poll_timer(max(self.interval, SLOW_POLL_INTERVAL_S))
319
+ self._reschedule_poll_timer(menubar_title.menubar_poll_interval(self, self.latest_state))
321
320
  self._fs_stream = setup_fsevents(self)
322
321
  self._history_source_tracker.set_incremental_enabled(self._fs_stream is not None)
323
322
  warm_up_pricing(self._refresh_after_pricing_warm_up)
@@ -378,7 +377,7 @@ class AppDelegate(NSObject):
378
377
 
379
378
  def _panel_window_did_hide(self) -> None:
380
379
  self.popover_controller.evictInactivePanelViews()
381
- self._reschedule_poll_timer(max(self.interval, SLOW_POLL_INTERVAL_S))
380
+ self._reschedule_poll_timer(menubar_title.menubar_poll_interval(self, self.latest_state))
382
381
 
383
382
  def windowDidMove_(self, notification: Any) -> None:
384
383
  if notification.object() is self.popover and self._panel_window_is_visible():
@@ -106,6 +106,7 @@ class QuotaRowState:
106
106
  available: bool = True
107
107
  reset_text_compact: str = ""
108
108
  reset_done: bool = False
109
+ resets_at: float | None = None
109
110
 
110
111
  # Keep percent unchanged for quota notifications; renderers use display_percent.
111
112
 
@@ -902,7 +903,8 @@ def build_popover_state(
902
903
  CLAUDE_COLOR,
903
904
  language,
904
905
  forecast_seconds=burn_rate_trackers["claude_session"].forecast_seconds(),
905
- allow_unknown_reset=snapshot.data_source == "claude-desktop",
906
+ # The hook reader leaves a missing five_hour resets_at unknown.
907
+ allow_unknown_reset=True,
906
908
  )
907
909
  claude_weekly = _quota_row(
908
910
  _t(language, "weekly_label"),
@@ -1083,6 +1085,7 @@ def _quota_row(
1083
1085
  warning=warning,
1084
1086
  available=True,
1085
1087
  reset_done=time_to_reset <= 0,
1088
+ resets_at=resets_at,
1086
1089
  )
1087
1090
  if warning_seconds is not None:
1088
1091
  row.reset_text_compact = _t(
@@ -8,6 +8,8 @@
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import math
12
+ import time
11
13
  from collections.abc import Callable
12
14
  from typing import Any, Protocol
13
15
 
@@ -27,7 +29,7 @@ from menubar.chrome import (
27
29
  _menubar_icon_attachment_string,
28
30
  _muse_menubar_icon,
29
31
  )
30
- from menubar.state import PopoverState, _format_percent
32
+ from menubar.state import PopoverState, QuotaRowState, _format_percent
31
33
 
32
34
 
33
35
  class _TitleApp(Protocol):
@@ -36,6 +38,11 @@ class _TitleApp(Protocol):
36
38
  _last_button_title_key: tuple[str] | None
37
39
  _last_plain_title_key: tuple[str] | None
38
40
  status_item: Any
41
+ interval: float
42
+
43
+ def _reschedule_poll_timer(self, interval: float) -> None: ...
44
+
45
+ def _panel_window_is_visible(self) -> bool: ...
39
46
 
40
47
 
41
48
  _MENUBAR_FONT: Any = None
@@ -65,10 +72,43 @@ def _menubar_text_string(app: _TitleApp, text: str) -> Any:
65
72
 
66
73
  # A wider strip slides behind the notch; the panel still lists every source.
67
74
  _MENUBAR_MAX_ITEMS = 3
75
+ SLOW_POLL_INTERVAL_S = 300.0
76
+
77
+
78
+ def _format_countdown(seconds: float) -> str:
79
+ minutes = math.ceil(seconds / 60)
80
+ if minutes >= 1440:
81
+ return f"{minutes // 1440}d{minutes % 1440 // 60}h"
82
+ if minutes >= 60:
83
+ return f"{minutes // 60}h{minutes % 60}m"
84
+ return f"{minutes}m"
85
+
86
+
87
+ def _quota_countdown(rows: tuple[QuotaRowState, ...], now: float) -> str | None:
88
+ full = [row for row in rows if row.display_percent is not None and row.display_percent >= 100]
89
+ if not full:
90
+ return None
91
+ resets = []
92
+ for row in full:
93
+ if row.resets_at is None or row.resets_at <= now:
94
+ return None
95
+ resets.append(row.resets_at)
96
+ return _format_countdown(max(resets) - now)
97
+
98
+
99
+ def menubar_poll_interval(app: _TitleApp, state: PopoverState) -> float:
100
+ if app._panel_window_is_visible() or any(
101
+ text != "--" and not text.endswith("%") for _icon, text in _menubar_items(app, state)
102
+ ):
103
+ return app.interval
104
+ return max(app.interval, SLOW_POLL_INTERVAL_S)
68
105
 
69
106
 
70
- def _menubar_items(app: _TitleApp, state: PopoverState) -> list[tuple[Callable[[], Any], str]]:
71
- """Visible (icon loader, percent text) pairs, keeping the highest-usage sources in order."""
107
+ def _menubar_items(
108
+ app: _TitleApp, state: PopoverState, now: float | None = None
109
+ ) -> list[tuple[Callable[[], Any], str]]:
110
+ """Visible icon/text pairs, keeping the highest-usage sources in order."""
111
+ now = time.time() if now is None else now
72
112
  items: list[tuple[Callable[[], Any], float | None]] = []
73
113
  if not state.hide_claude:
74
114
  items.append((_claude_menubar_icon, state.claude_session.display_percent))
@@ -85,15 +125,29 @@ def _menubar_items(app: _TitleApp, state: PopoverState) -> list[tuple[Callable[[
85
125
  ):
86
126
  if not hidden and percent is not None:
87
127
  items.append((icon, percent))
128
+ rows_by_icon = {
129
+ _claude_menubar_icon: (state.claude_session, state.claude_weekly),
130
+ _codex_menubar_icon: (state.codex_session, state.codex_weekly),
131
+ _agy_menubar_icon: (state.agy_session, state.agy_weekly),
132
+ _grok_menubar_icon: (state.grok_weekly,),
133
+ _muse_menubar_icon: (state.muse_session, state.muse_weekly),
134
+ }
135
+ countdowns = [_quota_countdown(rows_by_icon[icon], now) for icon, _percent in items]
88
136
  if len(items) > _MENUBAR_MAX_ITEMS:
89
137
  # Stable sort, so ties keep the fixed order; an unknown "--" ranks last.
90
- ranks = [-1.0 if percent is None else percent for _icon, percent in items]
138
+ ranks = [
139
+ 100.0 if countdowns[i] is not None else -1.0 if percent is None else percent
140
+ for i, (_icon, percent) in enumerate(items)
141
+ ]
91
142
  ranked = sorted(range(len(items)), key=ranks.__getitem__, reverse=True)
92
- items = [items[i] for i in sorted(ranked[:_MENUBAR_MAX_ITEMS])]
93
- return [
94
- (icon, "--" if percent is None else f"{_format_percent(percent)}%")
95
- for icon, percent in items
143
+ selected = sorted(ranked[:_MENUBAR_MAX_ITEMS])
144
+ else:
145
+ selected = list(range(len(items)))
146
+ rendered = [
147
+ (icon, countdown or ("--" if percent is None else f"{_format_percent(percent)}%"))
148
+ for (icon, percent), countdown in zip(items, countdowns, strict=True)
96
149
  ]
150
+ return [rendered[i] for i in selected]
97
151
 
98
152
 
99
153
  def _menubar_attributed_title(app: _TitleApp, state: PopoverState) -> Any:
@@ -110,6 +164,7 @@ def _menubar_attributed_title(app: _TitleApp, state: PopoverState) -> Any:
110
164
 
111
165
 
112
166
  def _set_button_title(app: _TitleApp, state: PopoverState) -> None:
167
+ app._reschedule_poll_timer(menubar_poll_interval(app, state))
113
168
  title = _compose_title(app, state)
114
169
  title_key = (title,)
115
170
  if app._last_button_title_key == title_key:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usage-cli"
3
- version = "0.34.9"
3
+ version = "0.34.11"
4
4
  description = "usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -33,6 +33,7 @@ dispatches a one-line call into :func:`maybe_ping`.
33
33
  from __future__ import annotations
34
34
 
35
35
  import logging
36
+ import math
36
37
  import os
37
38
  import shutil
38
39
  import subprocess
@@ -60,10 +61,8 @@ PING_TIMEOUT_SECONDS = 180
60
61
  # window the previous ping opened.
61
62
  PING_COOLDOWN_SECONDS = 5 * 3600 + 300
62
63
 
63
- # loaders.claude_usage defaults a missing ``resets_at`` to parse-time "now", which one
64
- # refresh later reads as "expired seconds ago". Requiring the expiry to be at
65
- # least this old filters those synthetic timestamps without delaying a real
66
- # expired-while-away ping by more than two minutes.
64
+ # Wait briefly after a real reset before opening the next window, allowing
65
+ # quota observations around the reset boundary to settle.
67
66
  PING_EXPIRY_GRACE_SECONDS = 120
68
67
 
69
68
  # ``claude`` binary resolution order: PATH first, then the well-known install
@@ -203,6 +202,35 @@ def _ping_worker(started_at: float) -> None:
203
202
  _release()
204
203
 
205
204
 
205
+ def _recall_expired_window(
206
+ state: dict[str, object],
207
+ current_reset_at: float | None,
208
+ current_percent: float | None,
209
+ data_source: str,
210
+ ) -> tuple[float | None, float | None]:
211
+ """Stand in the last reported five-hour reset once the payload drops the window.
212
+
213
+ Claude Code omits ``five_hour`` once its ``resets_at`` has passed, and the
214
+ statusLine hook drops the cached copy too (plans without a 5-hour limit never
215
+ send one), so an expired window reaches us as no window at all. The reader
216
+ then reports no percent and no reset, which is not worth keeping.
217
+ Remember every real reset the hook reports; a plan that never reported one
218
+ stays unarmed.
219
+ """
220
+ if data_source != "hook":
221
+ return current_reset_at, current_percent
222
+ remembered = keeper_outcome.numeric_value(state, "last_seen_reset_at")
223
+ if current_percent is None:
224
+ return (current_reset_at, None) if remembered is None else (remembered, 0.0)
225
+ if current_reset_at is None or not math.isfinite(current_reset_at):
226
+ return None, current_percent
227
+ if current_reset_at is not None and current_reset_at != remembered:
228
+ keeper_outcome.update_state(
229
+ WINDOW_KEEPER_STATE_PATH, {"last_seen_reset_at": current_reset_at}
230
+ )
231
+ return current_reset_at, current_percent
232
+
233
+
206
234
  def maybe_ping(
207
235
  current_reset_at: float | None,
208
236
  current_percent: float | None,
@@ -224,6 +252,9 @@ def maybe_ping(
224
252
  now = time.time()
225
253
  last_pinged_reset_at, last_ping_at = _load_ping_state()
226
254
  state = keeper_outcome.read_state(WINDOW_KEEPER_STATE_PATH)
255
+ current_reset_at, current_percent = _recall_expired_window(
256
+ state, current_reset_at, current_percent, data_source
257
+ )
227
258
  idle = should_ping(now, current_reset_at, enabled, None, None, current_percent, data_source)
228
259
  fresh = should_ping(
229
260
  now,
@@ -13,6 +13,7 @@ from pathlib import Path
13
13
  import pytest
14
14
 
15
15
  from adapters import rate_limits
16
+ from loaders import claude_usage
16
17
 
17
18
 
18
19
  def _write_status(path: Path, body: dict[str, object]) -> None:
@@ -20,6 +21,46 @@ def _write_status(path: Path, body: dict[str, object]) -> None:
20
21
  path.write_text(json.dumps(body), encoding="utf-8")
21
22
 
22
23
 
24
+ @pytest.mark.parametrize("window", [None, ["invalid"], {}, {"used_percentage": "nan"}])
25
+ def test_malformed_window_uses_fallback(monkeypatch: pytest.MonkeyPatch, window: object) -> None:
26
+ data = {
27
+ "rate_limits": {
28
+ "five_hour": window,
29
+ "seven_day": {"used_percentage": 42, "resets_at": 9_999_999_999},
30
+ }
31
+ }
32
+ assert claude_usage._has_rate_limits(data) is (window is None)
33
+ monkeypatch.setattr(rate_limits, "_read_status", lambda: data)
34
+ calls = []
35
+ fallback = claude_usage.UsageSnapshot(10, None, 20, None, "", 1_800_000_000)
36
+
37
+ def read_fallback() -> claude_usage.UsageSnapshot:
38
+ calls.append(True)
39
+ return fallback
40
+
41
+ monkeypatch.setattr(claude_usage, "read_quota_fallback", read_fallback)
42
+ result = rate_limits.load_rate_limits()
43
+ assert result is not None
44
+ assert result.five_hour_pct == (None if window is None else 10)
45
+ assert calls == ([] if window is None else [True])
46
+ # Missing fallback must also tolerate malformed windows without .get errors.
47
+ monkeypatch.setattr(claude_usage, "read_quota_fallback", lambda: None)
48
+ result = rate_limits.load_rate_limits()
49
+ assert result is not None
50
+ assert result.five_hour_pct is None
51
+ assert result.seven_day_pct == 42
52
+
53
+
54
+ def test_weekly_only_ultra_remains_valid(monkeypatch: pytest.MonkeyPatch) -> None:
55
+ data = {"rate_limits": {"seven_day": {"used_percentage": 42}}}
56
+ assert claude_usage._has_rate_limits(data) is True
57
+ monkeypatch.setattr(rate_limits, "_read_status", lambda: data)
58
+ result = rate_limits.load_rate_limits()
59
+ assert result is not None
60
+ assert result.five_hour_pct is None
61
+ assert result.seven_day_pct == 42
62
+
63
+
23
64
  def test_load_rate_limits_skips_bad_utf8_status_without_crashing(
24
65
  monkeypatch: pytest.MonkeyPatch,
25
66
  tmp_path: Path,