usage-cli 0.34.10__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.10/usage_cli.egg-info → usage_cli-0.34.11}/PKG-INFO +1 -1
  2. {usage_cli-0.34.10 → usage_cli-0.34.11}/adapters/rate_limits.py +3 -3
  3. {usage_cli-0.34.10 → usage_cli-0.34.11}/analyzer/persona_loader.py +2 -1
  4. {usage_cli-0.34.10 → usage_cli-0.34.11}/i18n.json +75 -0
  5. {usage_cli-0.34.10 → usage_cli-0.34.11}/installer/setup_hook.py +1 -1
  6. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/claude_usage.py +7 -3
  7. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/agy.py +9 -0
  8. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/state.py +2 -1
  9. {usage_cli-0.34.10 → usage_cli-0.34.11}/pyproject.toml +1 -1
  10. {usage_cli-0.34.10 → usage_cli-0.34.11}/quota/window_keeper.py +35 -4
  11. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_adapters_rate_limits.py +41 -0
  12. usage_cli-0.34.11/tests/test_claude_expired_windows.py +86 -0
  13. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_html_report_snapshot.py +29 -24
  14. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_menubar.py +37 -0
  15. usage_cli-0.34.11/tests/test_report_chart_redesign.py +218 -0
  16. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_report_cold_read.py +1 -1
  17. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_report_filter_js.py +40 -22
  18. usage_cli-0.34.11/tests/test_report_headings.py +373 -0
  19. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_statusline.py +14 -11
  20. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_window_keeper.py +76 -0
  21. {usage_cli-0.34.10 → usage_cli-0.34.11}/ui/html_report.py +276 -120
  22. usage_cli-0.34.11/ui/report_charts.py +62 -0
  23. {usage_cli-0.34.10 → usage_cli-0.34.11}/ui/report_filter.py +210 -128
  24. {usage_cli-0.34.10 → usage_cli-0.34.11}/ui/report_styles.py +100 -0
  25. {usage_cli-0.34.10 → usage_cli-0.34.11/usage_cli.egg-info}/PKG-INFO +1 -1
  26. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_cli.egg-info/SOURCES.txt +3 -0
  27. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_hooks/usage_statusline.py +11 -8
  28. usage_cli-0.34.10/ui/report_charts.py +0 -26
  29. {usage_cli-0.34.10 → usage_cli-0.34.11}/LICENSE +0 -0
  30. {usage_cli-0.34.10 → usage_cli-0.34.11}/README.md +0 -0
  31. {usage_cli-0.34.10 → usage_cli-0.34.11}/THIRD_PARTY_NOTICES.md +0 -0
  32. {usage_cli-0.34.10 → usage_cli-0.34.11}/adapters/__init__.py +0 -0
  33. {usage_cli-0.34.10 → usage_cli-0.34.11}/adapters/agy.py +0 -0
  34. {usage_cli-0.34.10 → usage_cli-0.34.11}/adapters/claude.py +0 -0
  35. {usage_cli-0.34.10 → usage_cli-0.34.11}/adapters/codex.py +0 -0
  36. {usage_cli-0.34.10 → usage_cli-0.34.11}/adapters/grok.py +0 -0
  37. {usage_cli-0.34.10 → usage_cli-0.34.11}/adapters/muse.py +0 -0
  38. {usage_cli-0.34.10 → usage_cli-0.34.11}/adapters/registry.py +0 -0
  39. {usage_cli-0.34.10 → usage_cli-0.34.11}/adapters/types.py +0 -0
  40. {usage_cli-0.34.10 → usage_cli-0.34.11}/analyzer/__init__.py +0 -0
  41. {usage_cli-0.34.10 → usage_cli-0.34.11}/analyzer/aggregator.py +0 -0
  42. {usage_cli-0.34.10 → usage_cli-0.34.11}/analyzer/blocks.py +0 -0
  43. {usage_cli-0.34.10 → usage_cli-0.34.11}/analyzer/diagnoser.py +0 -0
  44. {usage_cli-0.34.10 → usage_cli-0.34.11}/analyzer/diagnosis_snapshot.py +0 -0
  45. {usage_cli-0.34.10 → usage_cli-0.34.11}/analyzer/dispatch_ledger.py +0 -0
  46. {usage_cli-0.34.10 → usage_cli-0.34.11}/analyzer/history_persist.py +0 -0
  47. {usage_cli-0.34.10 → usage_cli-0.34.11}/analyzer/insights.py +0 -0
  48. {usage_cli-0.34.10 → usage_cli-0.34.11}/analyzer/ledger_backfill.py +0 -0
  49. {usage_cli-0.34.10 → usage_cli-0.34.11}/analyzer/reporter.py +0 -0
  50. {usage_cli-0.34.10 → usage_cli-0.34.11}/analyzer/subscription.py +0 -0
  51. {usage_cli-0.34.10 → usage_cli-0.34.11}/analyzer/usage_snapshot.py +0 -0
  52. {usage_cli-0.34.10 → usage_cli-0.34.11}/installer/__init__.py +0 -0
  53. {usage_cli-0.34.10 → usage_cli-0.34.11}/installer/claude_pane.py +0 -0
  54. {usage_cli-0.34.10 → usage_cli-0.34.11}/installer/codex_hook_trust.py +0 -0
  55. {usage_cli-0.34.10 → usage_cli-0.34.11}/installer/login_item.py +0 -0
  56. {usage_cli-0.34.10 → usage_cli-0.34.11}/installer/session_hooks.py +0 -0
  57. {usage_cli-0.34.10 → usage_cli-0.34.11}/installer/statusline_settings.py +0 -0
  58. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/__init__.py +0 -0
  59. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/_shared_cache_file.py +0 -0
  60. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/agy_disk_cache.py +0 -0
  61. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/agy_loader.py +0 -0
  62. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/agy_quota_probe.py +0 -0
  63. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/cache_quarantine.py +0 -0
  64. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/chromium_cache.py +0 -0
  65. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/claude_desktop.py +0 -0
  66. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/claude_paths.py +0 -0
  67. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/codex_disk_cache.py +0 -0
  68. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/codex_events.py +0 -0
  69. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/codex_fork_replay.py +0 -0
  70. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/codex_loader.py +0 -0
  71. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/codex_paths.py +0 -0
  72. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/disk_cache_common.py +0 -0
  73. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/disk_cache_lifecycle.py +0 -0
  74. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/grok_loader.py +0 -0
  75. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/grok_quota_probe.py +0 -0
  76. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/history_disk_cache.py +0 -0
  77. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/history_loader.py +0 -0
  78. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/jsonl_limits.py +0 -0
  79. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/jsonl_utils.py +0 -0
  80. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/muse_loader.py +0 -0
  81. {usage_cli-0.34.10 → usage_cli-0.34.11}/loaders/muse_quota_probe.py +0 -0
  82. {usage_cli-0.34.10 → usage_cli-0.34.11}/main.py +0 -0
  83. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/__init__.py +0 -0
  84. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/actions.py +0 -0
  85. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/app.py +0 -0
  86. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/chrome.py +0 -0
  87. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/fsevents_watch.py +0 -0
  88. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/grok.py +0 -0
  89. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/manual_refresh.py +0 -0
  90. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/menu.py +0 -0
  91. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/muse.py +0 -0
  92. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/new_badge.py +0 -0
  93. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/notify.py +0 -0
  94. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/popover.py +0 -0
  95. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/prefs.py +0 -0
  96. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/refresh.py +0 -0
  97. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/title.py +0 -0
  98. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/update.py +0 -0
  99. {usage_cli-0.34.10 → usage_cli-0.34.11}/menubar/whats_new.py +0 -0
  100. {usage_cli-0.34.10 → usage_cli-0.34.11}/panels/__init__.py +0 -0
  101. {usage_cli-0.34.10 → usage_cli-0.34.11}/panels/base.py +0 -0
  102. {usage_cli-0.34.10 → usage_cli-0.34.11}/panels/catalog.py +0 -0
  103. {usage_cli-0.34.10 → usage_cli-0.34.11}/panels/dynamic_height.py +0 -0
  104. {usage_cli-0.34.10 → usage_cli-0.34.11}/panels/panel_scale.py +0 -0
  105. {usage_cli-0.34.10 → usage_cli-0.34.11}/panels/panel_window.py +0 -0
  106. {usage_cli-0.34.10 → usage_cli-0.34.11}/panels/panel_window_state.py +0 -0
  107. {usage_cli-0.34.10 → usage_cli-0.34.11}/panels/payload.py +0 -0
  108. {usage_cli-0.34.10 → usage_cli-0.34.11}/panels/web_panel.py +0 -0
  109. {usage_cli-0.34.10 → usage_cli-0.34.11}/panels/window_drag.py +0 -0
  110. {usage_cli-0.34.10 → usage_cli-0.34.11}/quota/__init__.py +0 -0
  111. {usage_cli-0.34.10 → usage_cli-0.34.11}/quota/agy_window_keeper.py +0 -0
  112. {usage_cli-0.34.10 → usage_cli-0.34.11}/quota/burn_rate.py +0 -0
  113. {usage_cli-0.34.10 → usage_cli-0.34.11}/quota/codex_window_keeper.py +0 -0
  114. {usage_cli-0.34.10 → usage_cli-0.34.11}/quota/keeper_outcome.py +0 -0
  115. {usage_cli-0.34.10 → usage_cli-0.34.11}/quota/notifications.py +0 -0
  116. {usage_cli-0.34.10 → usage_cli-0.34.11}/quota/quota_snapshot.py +0 -0
  117. {usage_cli-0.34.10 → usage_cli-0.34.11}/quota/status_payload.py +0 -0
  118. {usage_cli-0.34.10 → usage_cli-0.34.11}/quota/usage_rate.py +0 -0
  119. {usage_cli-0.34.10 → usage_cli-0.34.11}/setup.cfg +0 -0
  120. {usage_cli-0.34.10 → usage_cli-0.34.11}/setup_app.py +0 -0
  121. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_adapters_claude.py +0 -0
  122. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_adapters_codex.py +0 -0
  123. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_aggregator.py +0 -0
  124. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_agy_adapter.py +0 -0
  125. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_agy_disk_cache.py +0 -0
  126. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_agy_loader.py +0 -0
  127. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_agy_quota_probe.py +0 -0
  128. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_agy_window_keeper.py +0 -0
  129. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_analyzer_blocks.py +0 -0
  130. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_analyzer_pipeline.py +0 -0
  131. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_app_version.py +0 -0
  132. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_burn_rate.py +0 -0
  133. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_cache_quarantine.py +0 -0
  134. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_check_upstream.py +0 -0
  135. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_chromium_cache.py +0 -0
  136. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_claude_desktop.py +0 -0
  137. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_claude_pane.py +0 -0
  138. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_claude_paths.py +0 -0
  139. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_codex_consistency.py +0 -0
  140. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_codex_hook_trust.py +0 -0
  141. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_codex_loader.py +0 -0
  142. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_codex_window_keeper.py +0 -0
  143. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_compact_cards.py +0 -0
  144. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_desktop_fresh_cache.py +0 -0
  145. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_desktop_quota_display.py +0 -0
  146. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_desktop_quota_review.py +0 -0
  147. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_diagnoser.py +0 -0
  148. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_disk_cache_shards.py +0 -0
  149. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_dispatch_ledger.py +0 -0
  150. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_doctor.py +0 -0
  151. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_dynamic_height.py +0 -0
  152. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_fsevents_watch.py +0 -0
  153. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_grok_loader.py +0 -0
  154. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_grok_quota_probe.py +0 -0
  155. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_history_loader.py +0 -0
  156. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_history_persist.py +0 -0
  157. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_hook_scripts_are_standalone.py +0 -0
  158. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_i18n_format_fallback.py +0 -0
  159. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_i18n_key_parity.py +0 -0
  160. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_i18n_packaged_path.py +0 -0
  161. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_insights.py +0 -0
  162. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_jsonl_utils.py +0 -0
  163. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_keeper_menu.py +0 -0
  164. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_keeper_outcome.py +0 -0
  165. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_ledger_backfill.py +0 -0
  166. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_login_item.py +0 -0
  167. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_main.py +0 -0
  168. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_manual_refresh.py +0 -0
  169. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_menubar_agy.py +0 -0
  170. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_menubar_grok.py +0 -0
  171. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_menubar_muse.py +0 -0
  172. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_menubar_notify.py +0 -0
  173. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_menubar_prefs.py +0 -0
  174. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_menubar_state.py +0 -0
  175. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_menubar_update.py +0 -0
  176. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_muse_coverage.py +0 -0
  177. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_muse_loader.py +0 -0
  178. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_muse_quota_probe.py +0 -0
  179. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_muse_review_regressions.py +0 -0
  180. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_new_badge.py +0 -0
  181. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_packaged_resources.py +0 -0
  182. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_packaging.py +0 -0
  183. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_panel_footer_layout.py +0 -0
  184. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_panel_scale.py +0 -0
  185. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_panel_window_state.py +0 -0
  186. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_panels.py +0 -0
  187. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_panels_lazy_import.py +0 -0
  188. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_persona_loader.py +0 -0
  189. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_pricing.py +0 -0
  190. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_project_resolver.py +0 -0
  191. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_quota_aware.py +0 -0
  192. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_quota_label.py +0 -0
  193. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_report_daily_chart.py +0 -0
  194. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_report_share_mask.py +0 -0
  195. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_reporter_build_data.py +0 -0
  196. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_service_status.py +0 -0
  197. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_session_resume.py +0 -0
  198. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_setup_app.py +0 -0
  199. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_setup_hook.py +0 -0
  200. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_setup_hook_coexistence.py +0 -0
  201. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_setup_hook_resume.py +0 -0
  202. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_setup_hook_terse.py +0 -0
  203. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_subprocess_utils.py +0 -0
  204. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_subscription.py +0 -0
  205. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_taskbar_menu.py +0 -0
  206. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_taskbar_obstacles.py +0 -0
  207. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_time_utils.py +0 -0
  208. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_tui.py +0 -0
  209. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_tui_sprite.py +0 -0
  210. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_ui_tables.py +0 -0
  211. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_update_checker.py +0 -0
  212. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_update_gate.py +0 -0
  213. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_update_release_notes.py +0 -0
  214. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_cli.py +0 -0
  215. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_client.py +0 -0
  216. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_diagnosis_snapshot.py +0 -0
  217. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_dir_sweeper.py +0 -0
  218. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_lang.py +0 -0
  219. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_logging.py +0 -0
  220. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_notifications.py +0 -0
  221. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_rate.py +0 -0
  222. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_snapshot.py +0 -0
  223. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_snapshot_replay.py +0 -0
  224. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_statusline_agy.py +0 -0
  225. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_statusline_forwarder.py +0 -0
  226. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_statusline_grok.py +0 -0
  227. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_terse_mode.py +0 -0
  228. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_usage_terse_reminder.py +0 -0
  229. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_web_panel_payload.py +0 -0
  230. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_whats_new.py +0 -0
  231. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_wheel_packaging.py +0 -0
  232. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_win_login_item.py +0 -0
  233. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_windows_manifest.py +0 -0
  234. {usage_cli-0.34.10 → usage_cli-0.34.11}/tests/test_wintray.py +0 -0
  235. {usage_cli-0.34.10 → usage_cli-0.34.11}/tui/__init__.py +0 -0
  236. {usage_cli-0.34.10 → usage_cli-0.34.11}/tui/app.py +0 -0
  237. {usage_cli-0.34.10 → usage_cli-0.34.11}/tui/sprite.py +0 -0
  238. {usage_cli-0.34.10 → usage_cli-0.34.11}/ui/__init__.py +0 -0
  239. {usage_cli-0.34.10 → usage_cli-0.34.11}/ui/report_daily_chart.py +0 -0
  240. {usage_cli-0.34.10 → usage_cli-0.34.11}/ui/report_ledger.py +0 -0
  241. {usage_cli-0.34.10 → usage_cli-0.34.11}/ui/report_scripts.py +0 -0
  242. {usage_cli-0.34.10 → usage_cli-0.34.11}/ui/tables.py +0 -0
  243. {usage_cli-0.34.10 → usage_cli-0.34.11}/updates/__init__.py +0 -0
  244. {usage_cli-0.34.10 → usage_cli-0.34.11}/updates/checker.py +0 -0
  245. {usage_cli-0.34.10 → usage_cli-0.34.11}/updates/gate.py +0 -0
  246. {usage_cli-0.34.10 → usage_cli-0.34.11}/updates/release_notes.py +0 -0
  247. {usage_cli-0.34.10 → usage_cli-0.34.11}/updates/whats_new.py +0 -0
  248. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_app/__init__.py +0 -0
  249. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_app/cli.py +0 -0
  250. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_app/doctor.py +0 -0
  251. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_cli.egg-info/dependency_links.txt +0 -0
  252. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_cli.egg-info/entry_points.txt +0 -0
  253. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_cli.egg-info/requires.txt +0 -0
  254. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_cli.egg-info/top_level.txt +0 -0
  255. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_common/__init__.py +0 -0
  256. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_common/app_version.py +0 -0
  257. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_common/i18n.py +0 -0
  258. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_common/prefs.py +0 -0
  259. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_common/pricing.py +0 -0
  260. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_common/project_resolver.py +0 -0
  261. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_common/service_status.py +0 -0
  262. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_common/subprocess_utils.py +0 -0
  263. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_common/time_utils.py +0 -0
  264. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_common/usage_dir_sweeper.py +0 -0
  265. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_common/usage_lang.py +0 -0
  266. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_common/usage_logging.py +0 -0
  267. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_hooks/__init__.py +0 -0
  268. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_hooks/usage_quota_aware.py +0 -0
  269. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_hooks/usage_session_resume.py +0 -0
  270. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_hooks/usage_statusline_agy.py +0 -0
  271. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_hooks/usage_statusline_forwarder.py +0 -0
  272. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_hooks/usage_statusline_grok.py +0 -0
  273. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_hooks/usage_terse_mode.py +0 -0
  274. {usage_cli-0.34.10 → usage_cli-0.34.11}/usage_hooks/usage_terse_reminder.py +0 -0
  275. {usage_cli-0.34.10 → usage_cli-0.34.11}/wintray/__init__.py +0 -0
  276. {usage_cli-0.34.10 → usage_cli-0.34.11}/wintray/app.py +0 -0
  277. {usage_cli-0.34.10 → usage_cli-0.34.11}/wintray/login_item.py +0 -0
  278. {usage_cli-0.34.10 → usage_cli-0.34.11}/wintray/menu.py +0 -0
  279. {usage_cli-0.34.10 → usage_cli-0.34.11}/wintray/prefs.py +0 -0
  280. {usage_cli-0.34.10 → usage_cli-0.34.11}/wintray/quota_label.py +0 -0
  281. {usage_cli-0.34.10 → usage_cli-0.34.11}/wintray/taskbar_menu.py +0 -0
  282. {usage_cli-0.34.10 → usage_cli-0.34.11}/wintray/taskbar_obstacles.py +0 -0
  283. {usage_cli-0.34.10 → usage_cli-0.34.11}/wintray/taskbar_overlay.py +0 -0
  284. {usage_cli-0.34.10 → usage_cli-0.34.11}/wintray/watch.py +0 -0
  285. {usage_cli-0.34.10 → 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.10
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
@@ -51,9 +51,9 @@ def load_rate_limits() -> RateLimits | None:
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.16"
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.
@@ -303,9 +303,10 @@ def _has_rate_limits(data: dict[str, Any]) -> bool:
303
303
  rl = data.get("rate_limits")
304
304
  if not isinstance(rl, dict):
305
305
  return False
306
- windows = [w for w in (rl.get("five_hour"), rl.get("seven_day")) if isinstance(w, dict)]
306
+ windows = [w for w in (rl.get("five_hour"), rl.get("seven_day")) if w is not None]
307
307
  return bool(windows) and all(
308
- _as_finite_float(w.get("used_percentage")) is not None for w in windows
308
+ isinstance(w, dict) and _as_finite_float(w.get("used_percentage")) is not None
309
+ for w in windows
309
310
  )
310
311
 
311
312
 
@@ -347,7 +348,10 @@ def _build_snapshot(data: dict[str, Any], *, data_source: str = "hook") -> Usage
347
348
 
348
349
  return UsageSnapshot(
349
350
  current_percent=five_pct,
350
- 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")),
351
355
  weekly_percent=seven_pct,
352
356
  weekly_reset_at=seven_reset,
353
357
  current_status=status,
@@ -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
 
@@ -903,7 +903,8 @@ def build_popover_state(
903
903
  CLAUDE_COLOR,
904
904
  language,
905
905
  forecast_seconds=burn_rate_trackers["claude_session"].forecast_seconds(),
906
- 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,
907
908
  )
908
909
  claude_weekly = _quota_row(
909
910
  _t(language, "weekly_label"),
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usage-cli"
3
- version = "0.34.10"
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,
@@ -0,0 +1,86 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only
2
+ # Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
3
+ #
4
+ # Part of "usage". Free software licensed under the GNU Affero General Public
5
+ # License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import re
11
+ import time
12
+ from datetime import UTC, datetime
13
+ from pathlib import Path
14
+
15
+ import pytest
16
+
17
+ from adapters import rate_limits
18
+ from loaders import claude_usage
19
+ from menubar.state import _quota_row
20
+ from quota.notifications import QuotaNotifier
21
+ from usage_common.i18n import _t
22
+ from usage_hooks import usage_statusline
23
+
24
+
25
+ @pytest.mark.parametrize("window", ["five_hour", "seven_day"])
26
+ def test_save_expired_window_reaches_readers_and_restored_notification(
27
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, window: str
28
+ ) -> None:
29
+ now = datetime(2026, 11, 1, 12, tzinfo=UTC)
30
+ now_ts = now.timestamp()
31
+ reset_at = now_ts - 3600
32
+ other = "seven_day" if window == "five_hour" else "five_hour"
33
+ status_file = tmp_path / "usage-status.json"
34
+ status_file.write_text(
35
+ json.dumps({"rate_limits": {window: {"used_percentage": 100, "resets_at": reset_at}}}),
36
+ encoding="utf-8",
37
+ )
38
+ monkeypatch.setattr(usage_statusline, "STATUS_FILE", str(status_file))
39
+ monkeypatch.setattr(rate_limits, "STATUS_FILE", str(status_file))
40
+ monkeypatch.setattr(time, "time", lambda: now_ts)
41
+ channel = "claude_session" if window == "five_hour" else "claude_weekly"
42
+ notifier = QuotaNotifier()
43
+ notifier.update({channel: (100.0, True)})
44
+
45
+ usage_statusline.save(
46
+ {"rate_limits": {other: {"used_percentage": 46, "resets_at": now_ts + 7200}}}, now
47
+ )
48
+
49
+ data = json.loads(status_file.read_text(encoding="utf-8"))
50
+ assert claude_usage._has_rate_limits(data)
51
+ snapshot = claude_usage._build_snapshot(data)
52
+ assert snapshot is not None
53
+ percent = snapshot.current_percent if window == "five_hour" else snapshot.weekly_percent
54
+ reset = snapshot.current_reset_at if window == "five_hour" else snapshot.weekly_reset_at
55
+ assert percent == 0
56
+ assert reset == reset_at
57
+ row = _quota_row(window, percent, reset, now_ts, (0.0, 0.0, 0.0), "zh-TW")
58
+ assert row.available
59
+ assert row.percent == 0
60
+ assert row.reset_done
61
+ assert row.reset_text == _t("zh-TW", "reset_done")
62
+ events = notifier.update({channel: (row.percent, row.available)})
63
+ assert [(event.kind, event.channel) for event in events] == [("restored", channel)]
64
+
65
+ monkeypatch.setenv("USAGE_LANG", "en")
66
+ monkeypatch.setattr(usage_statusline, "get_width", lambda: 116)
67
+ for name in ("PREFERENCES_FILE", "CONTEXT_BURN_FILE", "MIX_DIR"):
68
+ monkeypatch.setattr(usage_statusline, name, str(tmp_path / name))
69
+ output = re.sub(r"\x1b\[[0-9;]*m", "", usage_statusline.render(data, now))
70
+ label = "5h" if window == "five_hour" else "7d"
71
+ assert re.search(rf"{label}:[^\n]*?\b0%", output)
72
+ assert "100%" not in output
73
+
74
+ limits = rate_limits.load_rate_limits()
75
+ assert limits is not None
76
+ assert (limits.five_hour_pct if window == "five_hour" else limits.seven_day_pct) == 0
77
+
78
+
79
+ @pytest.mark.parametrize("offset, retained", [(-604799, True), (-604800, False), (-604801, False)])
80
+ def test_weekly_cache_retention_uses_a_week(offset: int, retained: bool) -> None:
81
+ now = 1_800_000_000
82
+ fresh = {"used_percentage": 46, "resets_at": now + 18000}
83
+ weekly = {"used_percentage": 100, "resets_at": now + offset}
84
+ merged = usage_statusline._merge_rate_limits({"five_hour": fresh}, {"seven_day": weekly}, now)
85
+ assert merged["five_hour"] == fresh
86
+ assert merged.get("seven_day") == (weekly if retained else None)
@@ -615,12 +615,10 @@ def test_generate_html_wires_cube_rows_to_report_filter_script() -> None:
615
615
  assert 'data-agent-id="claude-code"' in html
616
616
  assert 'data-project-index="1"' in html
617
617
  assert 'data-project-index="0"' in html
618
- assert re.search(
619
- r'<div class="rank-line"><span class="left-tick" aria-hidden="true"'
620
- r' style="background:#[0-9a-f]+"></span><span class="arrow">→</span>'
621
- r'<span class="name">unknown',
622
- html,
623
- )
618
+ assert (
619
+ '<div class="rank-line"><div class="hlabel"><span class="arrow">→</span>'
620
+ '<span class="name">unknown</span></div><div class="hlane"'
621
+ ) in html
624
622
  assert "window.usageReportFilter = {cube, aggregateRows, displayName, normalizeBounds" in html
625
623
  assert REPORT_FILTER_JS in html
626
624
 
@@ -727,8 +725,11 @@ def test_generate_html_without_cube_keeps_python_rendered_filter_sections() -> N
727
725
  def test_report_filter_script_uses_safe_dom_construction_and_separate_model_name() -> None:
728
726
  assert "innerHTML" not in REPORT_FILTER_JS
729
727
  assert "document.createElement" in REPORT_FILTER_JS
730
- assert "name.className = 'model-name'" in REPORT_FILTER_JS
731
- assert "label.className = 'child-model-name'" in REPORT_FILTER_JS
728
+ assert "nameClass: 'model-name'" in REPORT_FILTER_JS
729
+ assert (
730
+ "row.querySelector('.model-name').className = 'model-name child-model-name'"
731
+ in REPORT_FILTER_JS
732
+ )
732
733
  assert "appendTextSpan(row, 'name model-name', modelName)" not in REPORT_FILTER_JS
733
734
 
734
735
 
@@ -889,7 +890,10 @@ def test_trend_ascii_adds_week_range_tokens_and_cost_tooltip() -> None:
889
890
 
890
891
  html = html_report._trend_ascii(daily, "en", date(2026, 9, 7))
891
892
 
892
- assert 'class="trend-row" title="2026-09-01 – 2026-09-06 · 1.2M · $3.45"' in html
893
+ assert (
894
+ 'class="trend-row weekly-column partial" title="W36 (09/01–09/06) · only 6 days · '
895
+ '2026-09-01 – 2026-09-06 · 1.2M · $3.45"' in html
896
+ )
893
897
 
894
898
 
895
899
  def test_trend_ascii_tooltip_clamps_an_unfinished_final_week_to_the_report_range() -> None:
@@ -900,7 +904,10 @@ def test_trend_ascii_tooltip_clamps_an_unfinished_final_week_to_the_report_range
900
904
 
901
905
  html = html_report._trend_ascii(daily, "en", date(2026, 9, 9))
902
906
 
903
- assert 'title="2026-09-07 – 2026-09-09 · 300.0K · $3.00"' in html
907
+ assert (
908
+ 'title="W37 (09/07–09/09) · only 3 days · '
909
+ '2026-09-07 – 2026-09-09 · 300.0K · $3.00 · in progress"' in html
910
+ )
904
911
 
905
912
 
906
913
  def test_trend_ascii_shows_delta_when_final_week_ends_on_sunday() -> None:
@@ -938,7 +945,7 @@ def test_trend_summary_returns_first_week_copy_without_two_completed_weeks() ->
938
945
  )
939
946
 
940
947
 
941
- def test_project_share_bar_uses_the_neutral_project_color() -> None:
948
+ def test_project_share_bar_highlights_leader_and_uses_shared_main_color() -> None:
942
949
  data = {
943
950
  "summary": {"total_tokens": 300},
944
951
  "by_project": [
@@ -949,17 +956,14 @@ def test_project_share_bar_uses_the_neutral_project_color() -> None:
949
956
 
950
957
  html = html_report._render_project_section(data, "en")
951
958
 
952
- # 顏色留給工具;專案若沿用工具色,會被誤認成某個工具。
953
- for name, pct in (("alpha", "66.7"), ("beta", "33.3")):
954
- assert (
955
- '<span class="left-tick" aria-hidden="true" style="background:#8b8577"></span>'
956
- '<span class="arrow">→</span>'
957
- f'<span class="name">{name}</span>'
958
- ) in html
959
- assert (
960
- '<div class="gauge-rail" aria-hidden="true" '
961
- f'style="width:{pct}%;background:#8b8577"></div>'
962
- ) in html
959
+ for name, pct, color in (
960
+ ("alpha", "66.7", ";background:var(--chart-accent)"),
961
+ ("beta", "33.3", ""),
962
+ ):
963
+ assert f'<span class="name">{name}</span></div><div class="hlane"' in html
964
+ assert f'<div class="hbar" aria-hidden="true" style="width:{pct}%{color}"></div>' in html
965
+ assert 'class="rank-head"' not in html
966
+ assert html.count('class="haxis"') == 1
963
967
  assert "share-bar" not in html
964
968
  assert 'class="pct"' in html
965
969
  assert "Other projects" not in html
@@ -973,7 +977,7 @@ def test_rank_line_gauge_boundaries() -> None:
973
977
 
974
978
  assert 'style="width:0.0%;background:#5abfa0"' in zero
975
979
  assert ">—" in zero
976
- assert 'class="pct"' not in zero
980
+ assert 'class="pct">0.0%</span>' in zero
977
981
  assert "share-bar" not in zero
978
982
  assert 'style="width:100.0%;background:#e0885a"' in full
979
983
  assert ">$1.00<" in full
@@ -991,7 +995,8 @@ def test_projects_include_remainder_share_and_cost_without_mutating_input() -> N
991
995
  rendered = html_report._render_project_section(data, "en")
992
996
  assert 'data-project-other="true"' in rendered
993
997
  assert '<span class="name">Other projects</span>' in rendered
994
- assert 'data-label="Token share">25.0%</span>' in rendered
998
+ assert 'class="pct">25.0%</span>' in rendered
999
+ assert "width:25.0%;background:var(--chart-neutral)" in rendered
995
1000
  assert 'data-label="Tokens">25</span>' in rendered
996
1001
  assert 'data-label="API equivalent">$1.00</span>' in rendered
997
1002
  assert len(data["by_project"]) == 1
@@ -3530,6 +3530,43 @@ def test_menubar_poll_interval(
3530
3530
  assert menubar_title.menubar_poll_interval(app, state) == max(interval, 300)
3531
3531
 
3532
3532
 
3533
+ @pytest.mark.parametrize("age_minutes", [0, 10])
3534
+ def test_agy_projection_shows_recovery_countdown_and_short_poll(
3535
+ monkeypatch: pytest.MonkeyPatch, age_minutes: int
3536
+ ) -> None:
3537
+ from loaders.agy_quota_probe import AgyQuotaGroup, AgyQuotaResult, AgyQuotaWindow
3538
+
3539
+ now = 1_800_000_000.0
3540
+ monkeypatch.setattr(time, "time", lambda: now)
3541
+ monkeypatch.setattr(menubar, "_load_preferences", lambda: {})
3542
+ quota = AgyQuotaResult(
3543
+ groups=[
3544
+ AgyQuotaGroup(
3545
+ name="GEMINI MODELS",
3546
+ models=["gemini"],
3547
+ five_hour=AgyQuotaWindow(0, None, 90 + age_minutes),
3548
+ weekly=AgyQuotaWindow(90, None, 1440),
3549
+ )
3550
+ ],
3551
+ fetched_at=datetime.fromtimestamp(now - age_minutes * 60, UTC).isoformat(),
3552
+ )
3553
+ projection = menubar_agy.project_quota(quota, "en", now=now)
3554
+ assert projection is not None
3555
+ state = menubar._empty_state()
3556
+ for tool in ("claude", "codex", "grok", "muse"):
3557
+ setattr(state, f"hide_{tool}", True)
3558
+ state.hide_agy = False
3559
+ state.agy_session = projection.session
3560
+ state.agy_weekly = projection.weekly
3561
+ app = cast(
3562
+ Any,
3563
+ SimpleNamespace(codex_5h_pct=None, interval=60, _panel_window_is_visible=lambda: False),
3564
+ )
3565
+ assert projection.session.resets_at == now + 5400
3566
+ assert [text for _icon, text in menubar_title._menubar_items(app, state, now)] == ["1h30m"]
3567
+ assert menubar_title.menubar_poll_interval(app, state) == 60
3568
+
3569
+
3533
3570
  def test_menubar_countdown_ranks_as_full_and_preserves_codex_path(
3534
3571
  monkeypatch: pytest.MonkeyPatch,
3535
3572
  ) -> None: