usage-cli 0.33.6__tar.gz → 0.33.8__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 (272) hide show
  1. {usage_cli-0.33.6/usage_cli.egg-info → usage_cli-0.33.8}/PKG-INFO +1 -1
  2. {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/dispatch_ledger.py +4 -3
  3. {usage_cli-0.33.6 → usage_cli-0.33.8}/i18n.json +15 -0
  4. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/agy_quota_probe.py +56 -8
  5. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/agy.py +53 -22
  6. {usage_cli-0.33.6 → usage_cli-0.33.8}/pyproject.toml +2 -1
  7. {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/status_payload.py +3 -1
  8. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_agy_quota_probe.py +93 -4
  9. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_check_upstream.py +14 -1
  10. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_dispatch_ledger.py +12 -0
  11. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_menubar_agy.py +45 -0
  12. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_panel_footer_layout.py +3 -5
  13. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_statusline_agy.py +58 -0
  14. {usage_cli-0.33.6 → usage_cli-0.33.8/usage_cli.egg-info}/PKG-INFO +1 -1
  15. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_statusline_agy.py +44 -1
  16. {usage_cli-0.33.6 → usage_cli-0.33.8}/LICENSE +0 -0
  17. {usage_cli-0.33.6 → usage_cli-0.33.8}/README.md +0 -0
  18. {usage_cli-0.33.6 → usage_cli-0.33.8}/THIRD_PARTY_NOTICES.md +0 -0
  19. {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/__init__.py +0 -0
  20. {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/agy.py +0 -0
  21. {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/claude.py +0 -0
  22. {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/codex.py +0 -0
  23. {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/grok.py +0 -0
  24. {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/muse.py +0 -0
  25. {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/rate_limits.py +0 -0
  26. {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/registry.py +0 -0
  27. {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/types.py +0 -0
  28. {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/__init__.py +0 -0
  29. {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/aggregator.py +0 -0
  30. {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/blocks.py +0 -0
  31. {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/diagnoser.py +0 -0
  32. {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/diagnosis_snapshot.py +0 -0
  33. {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/history_persist.py +0 -0
  34. {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/insights.py +0 -0
  35. {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/ledger_backfill.py +0 -0
  36. {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/persona_loader.py +0 -0
  37. {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/reporter.py +0 -0
  38. {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/subscription.py +0 -0
  39. {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/usage_snapshot.py +0 -0
  40. {usage_cli-0.33.6 → usage_cli-0.33.8}/installer/__init__.py +0 -0
  41. {usage_cli-0.33.6 → usage_cli-0.33.8}/installer/claude_pane.py +0 -0
  42. {usage_cli-0.33.6 → usage_cli-0.33.8}/installer/codex_hook_trust.py +0 -0
  43. {usage_cli-0.33.6 → usage_cli-0.33.8}/installer/login_item.py +0 -0
  44. {usage_cli-0.33.6 → usage_cli-0.33.8}/installer/session_hooks.py +0 -0
  45. {usage_cli-0.33.6 → usage_cli-0.33.8}/installer/setup_hook.py +0 -0
  46. {usage_cli-0.33.6 → usage_cli-0.33.8}/installer/statusline_settings.py +0 -0
  47. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/__init__.py +0 -0
  48. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/_shared_cache_file.py +0 -0
  49. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/agy_disk_cache.py +0 -0
  50. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/agy_loader.py +0 -0
  51. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/cache_quarantine.py +0 -0
  52. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/chromium_cache.py +0 -0
  53. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/claude_desktop.py +0 -0
  54. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/claude_paths.py +0 -0
  55. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/claude_usage.py +0 -0
  56. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/codex_disk_cache.py +0 -0
  57. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/codex_events.py +0 -0
  58. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/codex_fork_replay.py +0 -0
  59. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/codex_loader.py +0 -0
  60. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/codex_paths.py +0 -0
  61. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/disk_cache_common.py +0 -0
  62. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/disk_cache_lifecycle.py +0 -0
  63. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/grok_loader.py +0 -0
  64. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/grok_quota_probe.py +0 -0
  65. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/history_disk_cache.py +0 -0
  66. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/history_loader.py +0 -0
  67. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/jsonl_limits.py +0 -0
  68. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/jsonl_utils.py +0 -0
  69. {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/muse_loader.py +0 -0
  70. {usage_cli-0.33.6 → usage_cli-0.33.8}/main.py +0 -0
  71. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/__init__.py +0 -0
  72. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/actions.py +0 -0
  73. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/app.py +0 -0
  74. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/chrome.py +0 -0
  75. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/fsevents_watch.py +0 -0
  76. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/grok.py +0 -0
  77. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/manual_refresh.py +0 -0
  78. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/menu.py +0 -0
  79. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/new_badge.py +0 -0
  80. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/notify.py +0 -0
  81. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/popover.py +0 -0
  82. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/prefs.py +0 -0
  83. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/refresh.py +0 -0
  84. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/state.py +0 -0
  85. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/title.py +0 -0
  86. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/update.py +0 -0
  87. {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/whats_new.py +0 -0
  88. {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/__init__.py +0 -0
  89. {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/base.py +0 -0
  90. {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/catalog.py +0 -0
  91. {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/dynamic_height.py +0 -0
  92. {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/panel_scale.py +0 -0
  93. {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/panel_window.py +0 -0
  94. {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/panel_window_state.py +0 -0
  95. {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/payload.py +0 -0
  96. {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/web_panel.py +0 -0
  97. {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/window_drag.py +0 -0
  98. {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/__init__.py +0 -0
  99. {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/agy_window_keeper.py +0 -0
  100. {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/burn_rate.py +0 -0
  101. {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/codex_window_keeper.py +0 -0
  102. {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/keeper_outcome.py +0 -0
  103. {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/notifications.py +0 -0
  104. {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/quota_snapshot.py +0 -0
  105. {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/usage_rate.py +0 -0
  106. {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/window_keeper.py +0 -0
  107. {usage_cli-0.33.6 → usage_cli-0.33.8}/setup.cfg +0 -0
  108. {usage_cli-0.33.6 → usage_cli-0.33.8}/setup_app.py +0 -0
  109. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_adapters_claude.py +0 -0
  110. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_adapters_codex.py +0 -0
  111. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_adapters_rate_limits.py +0 -0
  112. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_aggregator.py +0 -0
  113. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_agy_adapter.py +0 -0
  114. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_agy_disk_cache.py +0 -0
  115. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_agy_loader.py +0 -0
  116. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_agy_window_keeper.py +0 -0
  117. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_analyzer_blocks.py +0 -0
  118. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_analyzer_pipeline.py +0 -0
  119. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_app_version.py +0 -0
  120. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_burn_rate.py +0 -0
  121. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_cache_quarantine.py +0 -0
  122. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_chromium_cache.py +0 -0
  123. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_claude_desktop.py +0 -0
  124. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_claude_pane.py +0 -0
  125. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_claude_paths.py +0 -0
  126. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_codex_consistency.py +0 -0
  127. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_codex_hook_trust.py +0 -0
  128. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_codex_loader.py +0 -0
  129. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_codex_window_keeper.py +0 -0
  130. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_desktop_fresh_cache.py +0 -0
  131. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_desktop_quota_display.py +0 -0
  132. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_desktop_quota_review.py +0 -0
  133. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_diagnoser.py +0 -0
  134. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_disk_cache_shards.py +0 -0
  135. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_doctor.py +0 -0
  136. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_dynamic_height.py +0 -0
  137. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_fsevents_watch.py +0 -0
  138. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_grok_loader.py +0 -0
  139. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_grok_quota_probe.py +0 -0
  140. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_history_loader.py +0 -0
  141. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_history_persist.py +0 -0
  142. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_hook_scripts_are_standalone.py +0 -0
  143. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_html_report_snapshot.py +0 -0
  144. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_i18n_format_fallback.py +0 -0
  145. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_i18n_key_parity.py +0 -0
  146. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_i18n_packaged_path.py +0 -0
  147. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_insights.py +0 -0
  148. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_jsonl_utils.py +0 -0
  149. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_keeper_menu.py +0 -0
  150. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_keeper_outcome.py +0 -0
  151. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_ledger_backfill.py +0 -0
  152. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_login_item.py +0 -0
  153. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_main.py +0 -0
  154. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_manual_refresh.py +0 -0
  155. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_menubar.py +0 -0
  156. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_menubar_grok.py +0 -0
  157. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_menubar_notify.py +0 -0
  158. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_menubar_prefs.py +0 -0
  159. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_menubar_state.py +0 -0
  160. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_menubar_update.py +0 -0
  161. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_muse_loader.py +0 -0
  162. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_new_badge.py +0 -0
  163. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_packaged_resources.py +0 -0
  164. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_packaging.py +0 -0
  165. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_panel_scale.py +0 -0
  166. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_panel_window_state.py +0 -0
  167. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_panels.py +0 -0
  168. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_panels_lazy_import.py +0 -0
  169. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_persona_loader.py +0 -0
  170. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_pricing.py +0 -0
  171. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_project_resolver.py +0 -0
  172. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_quota_aware.py +0 -0
  173. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_quota_label.py +0 -0
  174. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_report_daily_chart.py +0 -0
  175. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_report_filter_js.py +0 -0
  176. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_report_share_mask.py +0 -0
  177. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_reporter_build_data.py +0 -0
  178. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_service_status.py +0 -0
  179. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_session_resume.py +0 -0
  180. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_setup_app.py +0 -0
  181. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_setup_hook.py +0 -0
  182. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_setup_hook_coexistence.py +0 -0
  183. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_setup_hook_resume.py +0 -0
  184. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_setup_hook_terse.py +0 -0
  185. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_subprocess_utils.py +0 -0
  186. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_subscription.py +0 -0
  187. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_taskbar_menu.py +0 -0
  188. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_time_utils.py +0 -0
  189. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_tui.py +0 -0
  190. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_tui_sprite.py +0 -0
  191. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_ui_tables.py +0 -0
  192. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_update_checker.py +0 -0
  193. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_update_gate.py +0 -0
  194. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_update_release_notes.py +0 -0
  195. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_cli.py +0 -0
  196. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_client.py +0 -0
  197. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_diagnosis_snapshot.py +0 -0
  198. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_dir_sweeper.py +0 -0
  199. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_lang.py +0 -0
  200. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_logging.py +0 -0
  201. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_notifications.py +0 -0
  202. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_rate.py +0 -0
  203. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_snapshot.py +0 -0
  204. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_snapshot_replay.py +0 -0
  205. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_statusline.py +0 -0
  206. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_statusline_forwarder.py +0 -0
  207. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_statusline_grok.py +0 -0
  208. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_terse_mode.py +0 -0
  209. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_terse_reminder.py +0 -0
  210. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_web_panel_payload.py +0 -0
  211. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_whats_new.py +0 -0
  212. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_wheel_packaging.py +0 -0
  213. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_win_login_item.py +0 -0
  214. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_window_keeper.py +0 -0
  215. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_windows_manifest.py +0 -0
  216. {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_wintray.py +0 -0
  217. {usage_cli-0.33.6 → usage_cli-0.33.8}/tui/__init__.py +0 -0
  218. {usage_cli-0.33.6 → usage_cli-0.33.8}/tui/app.py +0 -0
  219. {usage_cli-0.33.6 → usage_cli-0.33.8}/tui/sprite.py +0 -0
  220. {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/__init__.py +0 -0
  221. {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/html_report.py +0 -0
  222. {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/report_charts.py +0 -0
  223. {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/report_daily_chart.py +0 -0
  224. {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/report_filter.py +0 -0
  225. {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/report_ledger.py +0 -0
  226. {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/report_scripts.py +0 -0
  227. {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/report_styles.py +0 -0
  228. {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/tables.py +0 -0
  229. {usage_cli-0.33.6 → usage_cli-0.33.8}/updates/__init__.py +0 -0
  230. {usage_cli-0.33.6 → usage_cli-0.33.8}/updates/checker.py +0 -0
  231. {usage_cli-0.33.6 → usage_cli-0.33.8}/updates/gate.py +0 -0
  232. {usage_cli-0.33.6 → usage_cli-0.33.8}/updates/release_notes.py +0 -0
  233. {usage_cli-0.33.6 → usage_cli-0.33.8}/updates/whats_new.py +0 -0
  234. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_app/__init__.py +0 -0
  235. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_app/cli.py +0 -0
  236. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_app/doctor.py +0 -0
  237. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_cli.egg-info/SOURCES.txt +0 -0
  238. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_cli.egg-info/dependency_links.txt +0 -0
  239. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_cli.egg-info/entry_points.txt +0 -0
  240. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_cli.egg-info/requires.txt +0 -0
  241. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_cli.egg-info/top_level.txt +0 -0
  242. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/__init__.py +0 -0
  243. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/app_version.py +0 -0
  244. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/i18n.py +0 -0
  245. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/prefs.py +0 -0
  246. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/pricing.py +0 -0
  247. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/project_resolver.py +0 -0
  248. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/service_status.py +0 -0
  249. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/subprocess_utils.py +0 -0
  250. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/time_utils.py +0 -0
  251. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/usage_dir_sweeper.py +0 -0
  252. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/usage_lang.py +0 -0
  253. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/usage_logging.py +0 -0
  254. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/__init__.py +0 -0
  255. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_quota_aware.py +0 -0
  256. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_session_resume.py +0 -0
  257. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_statusline.py +0 -0
  258. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_statusline_forwarder.py +0 -0
  259. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_statusline_grok.py +0 -0
  260. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_terse_mode.py +0 -0
  261. {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_terse_reminder.py +0 -0
  262. {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/__init__.py +0 -0
  263. {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/app.py +0 -0
  264. {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/login_item.py +0 -0
  265. {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/menu.py +0 -0
  266. {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/prefs.py +0 -0
  267. {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/quota_label.py +0 -0
  268. {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/taskbar_menu.py +0 -0
  269. {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/taskbar_obstacles.py +0 -0
  270. {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/taskbar_overlay.py +0 -0
  271. {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/watch.py +0 -0
  272. {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/whats_new.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usage-cli
3
- Version: 0.33.6
3
+ Version: 0.33.8
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
@@ -290,11 +290,12 @@ def load_windows() -> dict[str, QuotaWindow]:
290
290
  fetched = _timestamp(agy.fetched_at)
291
291
  if fetched:
292
292
  for group in agy.groups:
293
- minutes = group.weekly.resets_in_minutes
294
- if minutes is None:
293
+ weekly = group.weekly
294
+ if weekly is None or weekly.resets_in_minutes is None:
295
295
  continue
296
+ minutes = weekly.resets_in_minutes
296
297
  window = QuotaWindow(
297
- 100 - group.weekly.remaining_percent,
298
+ 100 - weekly.remaining_percent,
298
299
  fetched + timedelta(minutes=minutes),
299
300
  timedelta(days=7),
300
301
  )
@@ -99,6 +99,9 @@
99
99
  "agy_group_claude_gpt": "Claude / GPT ⇄",
100
100
  "agy_group_switch_tooltip": "切換 Antigravity 額度組",
101
101
  "agy_quota_full": "額度全滿",
102
+ "agy_window_not_reported": "Google 沒有回報這項額度",
103
+ "agy_session_paused": "暫停",
104
+ "agy_session_paused_detail": "本週額度用完,等週重置",
102
105
  "agy_stale_minutes": "約 {minutes} 分鐘前",
103
106
  "agy_stale_hours": "約 {hours} 小時前",
104
107
  "agy_stale_tooltip": "顯示的是快取舊值。額度直接向官方 API 取得,通常每幾分鐘自動更新;出現舊值代表最近取用失敗(多半是 agy 尚未登入,或網路問題),成功後會自動恢復。",
@@ -643,6 +646,9 @@
643
646
  "agy_group_claude_gpt": "Claude / GPT ⇄",
644
647
  "agy_group_switch_tooltip": "Switch Antigravity quota group",
645
648
  "agy_quota_full": "Quota full",
649
+ "agy_window_not_reported": "Not reported by Google",
650
+ "agy_session_paused": "Paused",
651
+ "agy_session_paused_detail": "Weekly quota used up; waits for weekly reset",
646
652
  "agy_stale_minutes": "about {minutes} minutes ago",
647
653
  "agy_stale_hours": "about {hours} hours ago",
648
654
  "agy_stale_tooltip": "Showing a cached value. Quota is fetched straight from the official API, usually updating every few minutes; an old value means the last fetch failed (often agy isn't signed in, or there's a network issue), and it recovers automatically once a fetch succeeds.",
@@ -1187,6 +1193,9 @@
1187
1193
  "agy_group_claude_gpt": "Claude / GPT ⇄",
1188
1194
  "agy_group_switch_tooltip": "切换 Antigravity 额度组",
1189
1195
  "agy_quota_full": "额度已满",
1196
+ "agy_window_not_reported": "Google 没有回报这项额度",
1197
+ "agy_session_paused": "暂停",
1198
+ "agy_session_paused_detail": "本周额度用完,等周重置",
1190
1199
  "agy_stale_minutes": "约 {minutes} 分钟前",
1191
1200
  "agy_stale_hours": "约 {hours} 小时前",
1192
1201
  "agy_stale_tooltip": "显示的是缓存旧值。额度直接从官方 API 获取,通常每隔几分钟自动更新;出现旧值说明最近获取失败(多半是 agy 尚未登录,或网络问题),成功后会自动恢复。",
@@ -1731,6 +1740,9 @@
1731
1740
  "agy_group_claude_gpt": "Claude / GPT ⇄",
1732
1741
  "agy_group_switch_tooltip": "Antigravity の割り当てグループを切り替え",
1733
1742
  "agy_quota_full": "クォータは満タンです",
1743
+ "agy_window_not_reported": "Google から報告されていません",
1744
+ "agy_session_paused": "一時停止",
1745
+ "agy_session_paused_detail": "今週の上限に達したため、週のリセット待ち",
1734
1746
  "agy_stale_minutes": "約 {minutes} 分前",
1735
1747
  "agy_stale_hours": "約 {hours} 時間前",
1736
1748
  "agy_stale_tooltip": "キャッシュされた値を表示しています。利用枠は公式 API から直接取得し、通常数分ごとに自動更新されます。古い値が表示されるのは直近の取得に失敗したためです(多くの場合 agy が未サインイン、またはネットワークの問題)。取得に成功すると自動的に復旧します。",
@@ -2275,6 +2287,9 @@
2275
2287
  "agy_group_claude_gpt": "Claude / GPT ⇄",
2276
2288
  "agy_group_switch_tooltip": "Antigravity 할당량 그룹 전환",
2277
2289
  "agy_quota_full": "할당량이 가득 찼습니다",
2290
+ "agy_window_not_reported": "Google에서 보고하지 않음",
2291
+ "agy_session_paused": "일시 중지",
2292
+ "agy_session_paused_detail": "이번 주 할당량 소진, 주간 초기화 대기",
2278
2293
  "agy_stale_minutes": "약 {minutes}분 전",
2279
2294
  "agy_stale_hours": "약 {hours}시간 전",
2280
2295
  "agy_stale_tooltip": "캐시된 값을 표시하고 있습니다. 사용량은 공식 API에서 직접 가져오며 보통 몇 분마다 자동 업데이트됩니다. 예전 값이 보이는 건 최근 가져오기에 실패했기 때문입니다(대개 agy 미로그인 또는 네트워크 문제). 가져오기에 성공하면 자동으로 복구됩니다.",
@@ -59,7 +59,8 @@ _KEYRING_BASE64_PREFIX = "go-keyring-base64:"
59
59
 
60
60
  _TOKEN_URL = "https://oauth2.googleapis.com/token"
61
61
  _QUOTA_URL = "https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary"
62
- # Both endpoints return small JSON objects; cap the read so a broken endpoint
62
+ _MODELS_URL = "https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels"
63
+ # All endpoints return small JSON objects; cap the read so a broken endpoint
63
64
  # cannot make us buffer an unbounded response.
64
65
  _MAX_RESPONSE_BYTES = 4 * 1024 * 1024
65
66
  # Antigravity's installed-app public client constants (same values quotio ships).
@@ -108,8 +109,8 @@ class AgyQuotaGroup:
108
109
 
109
110
  name: str
110
111
  models: list[str]
111
- weekly: AgyQuotaWindow
112
- five_hour: AgyQuotaWindow
112
+ weekly: AgyQuotaWindow | None
113
+ five_hour: AgyQuotaWindow | None
113
114
 
114
115
 
115
116
  @dataclass(frozen=True, slots=True)
@@ -133,7 +134,10 @@ def probe_quota(timeout_seconds: float = 15) -> AgyQuotaResult | None:
133
134
  groups_raw = _extract_groups(raw)
134
135
  if not groups_raw:
135
136
  return None
136
- result = _build_result(groups_raw)
137
+ models_raw = None
138
+ if not any(_group_id(group) == "gemini" for group in groups_raw):
139
+ models_raw = _post_json(_MODELS_URL, access_token, {}, timeout_seconds)
140
+ result = _build_result(groups_raw, models_raw)
137
141
  if result is not None:
138
142
  with suppress(OSError):
139
143
  AUTH_EXPIRED_PATH.unlink(missing_ok=True)
@@ -505,7 +509,9 @@ def _extract_groups(raw: object) -> list[dict[str, object]] | None:
505
509
  return None
506
510
 
507
511
 
508
- def _build_result(groups_raw: list[dict[str, object]]) -> AgyQuotaResult | None:
512
+ def _build_result(
513
+ groups_raw: list[dict[str, object]], models_raw: object = None
514
+ ) -> AgyQuotaResult | None:
509
515
  """Classify buckets into the Gemini / Claude-GPT groups UI expects."""
510
516
  windows: dict[str, dict[str, AgyQuotaWindow]] = {}
511
517
  for group in groups_raw:
@@ -527,11 +533,47 @@ def _build_result(groups_raw: list[dict[str, object]]) -> AgyQuotaResult | None:
527
533
  continue
528
534
  slot[period] = window
529
535
  result_groups = _assemble_groups(windows)
536
+ if not any(_group_id(group) == "gemini" for group in groups_raw):
537
+ gemini = _gemini_group_from_models(models_raw)
538
+ if gemini is not None:
539
+ result_groups.insert(0, gemini)
530
540
  if not result_groups:
531
541
  return None
532
542
  return AgyQuotaResult(groups=result_groups, fetched_at=datetime.now(UTC).isoformat())
533
543
 
534
544
 
545
+ def _gemini_group_from_models(raw: object) -> AgyQuotaGroup | None:
546
+ """Use the lowest reported Gemini fraction and classify its reset window."""
547
+ models = raw.get("models") if isinstance(raw, dict) else None
548
+ if not isinstance(models, dict):
549
+ return None
550
+ candidates: list[tuple[float, dict[str, object]]] = []
551
+ for model_id, model in models.items():
552
+ if not isinstance(model_id, str) or not model_id.startswith("gemini-"):
553
+ continue
554
+ info = model.get("quotaInfo") if isinstance(model, dict) else None
555
+ if not isinstance(info, dict):
556
+ continue
557
+ fraction = _coerce_float(info.get("remainingFraction"))
558
+ reset = _bucket_reset_time(info)
559
+ if fraction is not None and reset is not None and _parse_timestamp(reset) is not None:
560
+ candidates.append((fraction, info))
561
+ if not candidates:
562
+ return None
563
+ _, bucket = min(candidates, key=lambda candidate: candidate[0])
564
+ window = _bucket_to_window(bucket)
565
+ reset = _bucket_reset_time(bucket)
566
+ assert reset is not None
567
+ session = _seconds_until(reset) <= 5 * 3600 + 5 * 60
568
+ template = _assemble_groups({"gemini": {}})[0]
569
+ return AgyQuotaGroup(
570
+ name=template.name,
571
+ models=template.models,
572
+ weekly=None if session else window,
573
+ five_hour=window if session else None,
574
+ )
575
+
576
+
535
577
  def _assemble_groups(
536
578
  windows: dict[str, dict[str, AgyQuotaWindow]],
537
579
  ) -> list[AgyQuotaGroup]:
@@ -744,7 +786,11 @@ def _result_to_payload(result: AgyQuotaResult) -> dict[str, object]:
744
786
  }
745
787
 
746
788
 
747
- def _window_to_payload(window: AgyQuotaWindow) -> dict[str, float | int | str | None]:
789
+ def _window_to_payload(
790
+ window: AgyQuotaWindow | None,
791
+ ) -> dict[str, float | int | str | None] | None:
792
+ if window is None:
793
+ return None
748
794
  return {
749
795
  "remaining_percent": window.remaining_percent,
750
796
  "resets_in": window.resets_in,
@@ -782,8 +828,10 @@ def _group_from_payload(payload: object) -> AgyQuotaGroup | None:
782
828
  or not name
783
829
  or not isinstance(models, list)
784
830
  or not all(isinstance(model, str) and model for model in models)
785
- or weekly is None
786
- or five_hour is None
831
+ or "weekly" not in payload
832
+ or "five_hour" not in payload
833
+ or (payload["weekly"] is not None and weekly is None)
834
+ or (payload["five_hour"] is not None and five_hour is None)
787
835
  ):
788
836
  return None
789
837
  return AgyQuotaGroup(name=name, models=models, weekly=weekly, five_hour=five_hour)
@@ -9,7 +9,7 @@
9
9
  from __future__ import annotations
10
10
 
11
11
  import time
12
- from dataclasses import dataclass
12
+ from dataclasses import dataclass, replace
13
13
  from typing import cast
14
14
 
15
15
  import quota.burn_rate as burn_rate
@@ -98,9 +98,13 @@ def project_quota(
98
98
  else:
99
99
  session_tracker = burn_rate_trackers["agy_session"]
100
100
  weekly_tracker = burn_rate_trackers["agy_weekly"]
101
- if session_tracker.last_timestamp is None or sample_ts > session_tracker.last_timestamp:
101
+ if selected.five_hour is not None and (
102
+ session_tracker.last_timestamp is None or sample_ts > session_tracker.last_timestamp
103
+ ):
102
104
  session_tracker.record(sample_ts, 100.0 - _remaining_percent(selected.five_hour))
103
- if weekly_tracker.last_timestamp is None or sample_ts > weekly_tracker.last_timestamp:
105
+ if selected.weekly is not None and (
106
+ weekly_tracker.last_timestamp is None or sample_ts > weekly_tracker.last_timestamp
107
+ ):
104
108
  weekly_tracker.record(sample_ts, 100.0 - _remaining_percent(selected.weekly))
105
109
  # agy samples arrive every 5 minutes, so the default 10-minute window
106
110
  # cannot collect MIN_FORECAST_SAMPLES; this wider window reacts more slowly.
@@ -112,25 +116,39 @@ def project_quota(
112
116
  window_seconds=burn_rate.ROLLING_WINDOW_SECONDS,
113
117
  min_span_seconds=AGY_WEEKLY_FORECAST_MIN_SPAN_SECONDS,
114
118
  )
119
+ session = _window_row(
120
+ _t(language, "session_label"),
121
+ selected.five_hour,
122
+ language,
123
+ age_minutes,
124
+ forecast_seconds=session_forecast,
125
+ )
126
+ weekly = _window_row(
127
+ _t(language, "weekly_label"),
128
+ selected.weekly,
129
+ language,
130
+ age_minutes,
131
+ forecast_seconds=weekly_forecast,
132
+ warning_max_seconds=AGY_WEEKLY_WARNING_MAX_SECONDS,
133
+ window_seconds=WEEKLY_WINDOW_SECONDS,
134
+ )
135
+ # Google pauses the 5-hour bucket once the weekly one is spent; showing it
136
+ # as "full" reads as usable, so say it is paused instead.
137
+ if (
138
+ selected.weekly is not None
139
+ and _remaining_percent(selected.weekly) == 0.0
140
+ and not weekly.reset_done
141
+ ):
142
+ session = replace(
143
+ session,
144
+ percent_text=_t(language, "agy_session_paused"),
145
+ reset_text=_t(language, "agy_session_paused_detail"),
146
+ )
115
147
  return AgyQuotaProjection(
116
148
  group_name=selected.name,
117
149
  group_switchable=len(quota.groups) >= 2,
118
- session=_window_row(
119
- _t(language, "session_label"),
120
- selected.five_hour,
121
- language,
122
- age_minutes,
123
- forecast_seconds=session_forecast,
124
- ),
125
- weekly=_window_row(
126
- _t(language, "weekly_label"),
127
- selected.weekly,
128
- language,
129
- age_minutes,
130
- forecast_seconds=weekly_forecast,
131
- warning_max_seconds=AGY_WEEKLY_WARNING_MAX_SECONDS,
132
- window_seconds=WEEKLY_WINDOW_SECONDS,
133
- ),
150
+ session=session,
151
+ weekly=weekly,
134
152
  stale=stale,
135
153
  five_hour=selected.five_hour,
136
154
  )
@@ -208,8 +226,12 @@ def fallback_projection(language: str) -> AgyQuotaProjection:
208
226
 
209
227
  def _group_remaining_percent(group: AgyQuotaGroup) -> float:
210
228
  return min(
211
- _remaining_percent(group.five_hour),
212
- _remaining_percent(group.weekly),
229
+ (
230
+ _remaining_percent(window)
231
+ for window in (group.five_hour, group.weekly)
232
+ if window is not None
233
+ ),
234
+ default=100.0,
213
235
  )
214
236
 
215
237
 
@@ -228,7 +250,7 @@ def _cache_age_minutes(fetched_at: str, now: float) -> float:
228
250
 
229
251
  def _window_row(
230
252
  title: str,
231
- window: AgyQuotaWindow,
253
+ window: AgyQuotaWindow | None,
232
254
  language: str,
233
255
  age_minutes: float = 0,
234
256
  forecast_seconds: float | None = None,
@@ -236,6 +258,15 @@ def _window_row(
236
258
  *,
237
259
  window_seconds: float | None = None,
238
260
  ) -> QuotaRowState:
261
+ if window is None:
262
+ return QuotaRowState(
263
+ title=title,
264
+ available=False,
265
+ percent=None,
266
+ percent_text="--",
267
+ reset_text=_t(language, "agy_window_not_reported"),
268
+ color=AGY_COLOR,
269
+ )
239
270
  remaining = _remaining_percent(window)
240
271
  used = 100.0 - remaining
241
272
  warning = False
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usage-cli"
3
- version = "0.33.6"
3
+ version = "0.33.8"
4
4
  description = "usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -81,6 +81,7 @@ select = ["E", "F", "I", "UP", "B", "SIM"]
81
81
  # usage_hooks/usage_session_resume.py 同樣裝進 ~/.claude/ 給系統 python3 (3.9) 跑,同一條限制。
82
82
  "usage_hooks/usage_session_resume.py" = ["UP017"]
83
83
  "usage_hooks/usage_quota_aware.py" = ["UP017", "UP045"]
84
+ "usage_hooks/usage_statusline_agy.py" = ["UP017"]
84
85
  # Imported analytics source is kept with minimal path fixes; keep formatting
85
86
  # stable for this integration commit.
86
87
  "adapters/**/*.py" = ["E501", "I001", "SIM108", "UP015", "UP017"]
@@ -97,7 +97,9 @@ def _status_antigravity(now: int) -> dict[str, Any]:
97
97
 
98
98
  fetched_timestamp = fetched_at.timestamp()
99
99
 
100
- def window_status(window: agy_quota_probe.AgyQuotaWindow) -> dict[str, Any]:
100
+ def window_status(window: agy_quota_probe.AgyQuotaWindow | None) -> dict[str, Any]:
101
+ if window is None:
102
+ return _status_window(None, None, now)
101
103
  return {
102
104
  "used_percent": round(100 - window.remaining_percent, 1),
103
105
  "resets_in_seconds": (
@@ -160,7 +160,7 @@ def test_find_agy_returns_none_when_all_paths_miss(
160
160
  # --- token freshness + refresh --------------------------------------------
161
161
 
162
162
 
163
- def test_quota_url_matches_current_cli_backend() -> None:
163
+ def test_quota_url_matches_cli_backend() -> None:
164
164
  assert _QUOTA_URL == (
165
165
  "https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary"
166
166
  )
@@ -497,9 +497,11 @@ def test_build_result_parses_group_and_windows() -> None:
497
497
  assert [group.name for group in result.groups] == ["GEMINI MODELS"]
498
498
  gemini = result.groups[0]
499
499
  assert gemini.models == ["Gemini Flash", "Gemini Pro"]
500
+ assert gemini.weekly is not None
500
501
  assert gemini.weekly.remaining_percent == pytest.approx(83.28)
501
502
  assert gemini.weekly.resets_in_minutes is not None and gemini.weekly.resets_in_minutes > 0
502
503
  assert gemini.weekly.resets_in is not None and "h" in gemini.weekly.resets_in
504
+ assert gemini.five_hour is not None
503
505
  assert gemini.five_hour.remaining_percent == pytest.approx(95.27)
504
506
  assert gemini.five_hour.resets_in is not None
505
507
 
@@ -513,6 +515,7 @@ def test_build_result_handles_remaining_fraction_three_shapes() -> None:
513
515
  groups_raw = [_gemini_group([_bucket(remaining=remaining, bucket_id="weekly")])]
514
516
  result = agy_quota_probe._build_result(groups_raw)
515
517
  assert result is not None
518
+ assert result.groups[0].weekly is not None
516
519
  assert result.groups[0].weekly.remaining_percent == pytest.approx(50.0)
517
520
 
518
521
 
@@ -532,6 +535,7 @@ def test_build_result_non_full_bucket_without_reset_time_has_no_countdown() -> N
532
535
 
533
536
  assert result is not None
534
537
  window = result.groups[0].weekly
538
+ assert window is not None
535
539
  assert window.remaining_percent == pytest.approx(40.0)
536
540
  assert window.resets_in is None
537
541
  assert window.resets_in_minutes is None
@@ -557,6 +561,7 @@ def test_build_result_skips_disabled_bucket_and_fills_missing_window_as_full() -
557
561
  gemini = result.groups[0]
558
562
  # Disabled weekly dropped; only the session bucket survived.
559
563
  assert gemini.weekly == AgyQuotaWindow(100.0, None, None)
564
+ assert gemini.five_hour is not None
560
565
  assert gemini.five_hour.remaining_percent == pytest.approx(70.0)
561
566
 
562
567
 
@@ -623,6 +628,7 @@ def test_probe_quota_refreshes_then_fetches_quota(
623
628
 
624
629
  assert result is not None
625
630
  assert result.groups[0].name == "GEMINI MODELS"
631
+ assert result.groups[0].weekly is not None
626
632
  assert result.groups[0].weekly.remaining_percent == pytest.approx(83.28)
627
633
  # Two calls: refresh then quota; the quota call carried the refreshed token.
628
634
  assert [url for url, _auth in visited] == [_TOKEN_URL, _QUOTA_URL]
@@ -745,7 +751,7 @@ def test_successful_probe_clears_expiration_marker(monkeypatch: pytest.MonkeyPat
745
751
  monkeypatch.setattr(agy_quota_probe, "_resolve_access_token", lambda timeout: "token")
746
752
  monkeypatch.setattr(agy_quota_probe, "_post_json", lambda *args: {})
747
753
  monkeypatch.setattr(agy_quota_probe, "_extract_groups", lambda raw: [{}])
748
- monkeypatch.setattr(agy_quota_probe, "_build_result", lambda raw: result)
754
+ monkeypatch.setattr(agy_quota_probe, "_build_result", lambda raw, models=None: result)
749
755
  assert agy_quota_probe.probe_quota() == result
750
756
  assert not agy_quota_probe.auth_expired()
751
757
 
@@ -755,7 +761,7 @@ def test_unusable_probe_keeps_expiration_marker(monkeypatch: pytest.MonkeyPatch)
755
761
  monkeypatch.setattr(agy_quota_probe, "_resolve_access_token", lambda timeout: "token")
756
762
  monkeypatch.setattr(agy_quota_probe, "_post_json", lambda *args: {})
757
763
  monkeypatch.setattr(agy_quota_probe, "_extract_groups", lambda raw: [{}])
758
- monkeypatch.setattr(agy_quota_probe, "_build_result", lambda raw: None)
764
+ monkeypatch.setattr(agy_quota_probe, "_build_result", lambda raw, models=None: None)
759
765
  assert agy_quota_probe.probe_quota() is None
760
766
  assert agy_quota_probe.auth_expired()
761
767
 
@@ -795,7 +801,7 @@ def test_marker_write_and_delete_errors_are_ignored(monkeypatch: pytest.MonkeyPa
795
801
  monkeypatch.setattr(agy_quota_probe, "_resolve_access_token", lambda timeout: "token")
796
802
  monkeypatch.setattr(agy_quota_probe, "_post_json", lambda *args: {})
797
803
  monkeypatch.setattr(agy_quota_probe, "_extract_groups", lambda raw: [{}])
798
- monkeypatch.setattr(agy_quota_probe, "_build_result", lambda raw: result)
804
+ monkeypatch.setattr(agy_quota_probe, "_build_result", lambda raw, models=None: result)
799
805
  assert agy_quota_probe.probe_quota() == result
800
806
  assert agy_quota_probe.auth_expired()
801
807
 
@@ -822,6 +828,7 @@ def test_carry_keeps_group_the_api_skipped_once() -> None:
822
828
 
823
829
  assert [group.name for group in merged.groups] == ["GEMINI MODELS", "CLAUDE AND GPT MODELS"]
824
830
  assert merged.groups[0] == full.groups[0]
831
+ assert merged.groups[1].weekly is not None
825
832
  assert merged.groups[1].weekly.remaining_percent == 0.0
826
833
 
827
834
 
@@ -861,3 +868,85 @@ def test_load_quota_writes_carried_group_to_cache(
861
868
  cached = agy_quota_probe._read_cache()
862
869
  assert cached is not None
863
870
  assert [group.name for group in cached.groups] == ["GEMINI MODELS", "CLAUDE AND GPT MODELS"]
871
+
872
+
873
+ @pytest.mark.parametrize("minutes, weekly", [(300, False), (305, False), (306, True)])
874
+ def test_probe_models_fallback_selects_lowest_gemini(
875
+ monkeypatch: pytest.MonkeyPatch, minutes: int, weekly: bool
876
+ ) -> None:
877
+ reset = (datetime.now(UTC) + timedelta(minutes=minutes)).isoformat()
878
+ models = {
879
+ "gemini-pro": {"quotaInfo": {"remainingFraction": 0.8, "resetTime": _FUTURE_RESET}},
880
+ "gemini-flash": {"quotaInfo": {"remainingFraction": "0.5", "resetTime": reset}},
881
+ "claude-opus": {"quotaInfo": {"resetTime": reset}},
882
+ "claude-sonnet": {"quotaInfo": {"remainingFraction": 0.01, "resetTime": reset}},
883
+ "chat_test": {"quotaInfo": {"remainingFraction": 1}},
884
+ "tab_test": {"quotaInfo": {"remainingFraction": 1}},
885
+ }
886
+ summary = _groups_payload(
887
+ [
888
+ {
889
+ "displayName": "Claude and GPT models",
890
+ "buckets": [_bucket(remaining=0.7, bucket_id="weekly")],
891
+ }
892
+ ]
893
+ )
894
+ calls = []
895
+
896
+ def post(url: str, token: str, body: dict[str, object], timeout: float) -> object:
897
+ calls.append((url, token, body, timeout))
898
+ return summary if url == _QUOTA_URL else {"models": models}
899
+
900
+ monkeypatch.setattr(agy_quota_probe, "_resolve_access_token", lambda timeout: "same-token")
901
+ monkeypatch.setattr(agy_quota_probe, "_post_json", post)
902
+ result = agy_quota_probe.probe_quota(7)
903
+ assert result is not None and len(result.groups) == 2
904
+ gemini = result.groups[0]
905
+ assert gemini.name == "GEMINI MODELS"
906
+ assert gemini.models == ["Gemini Flash", "Gemini Pro"]
907
+ window = gemini.weekly if weekly else gemini.five_hour
908
+ assert window is not None and window.remaining_percent == 50
909
+ assert (gemini.five_hour if weekly else gemini.weekly) is None
910
+ assert calls == [
911
+ (_QUOTA_URL, "same-token", {}, 7),
912
+ (agy_quota_probe._MODELS_URL, "same-token", {}, 7),
913
+ ]
914
+
915
+
916
+ @pytest.mark.parametrize(
917
+ "models",
918
+ [None, {"models": {}}, {"models": {"claude": {"quotaInfo": {"resetTime": _FUTURE_RESET}}}}],
919
+ )
920
+ def test_probe_models_unavailable_keeps_summary(
921
+ monkeypatch: pytest.MonkeyPatch, models: object
922
+ ) -> None:
923
+ summary = _groups_payload([{"displayName": "Claude and GPT models", "buckets": []}])
924
+ monkeypatch.setattr(agy_quota_probe, "_resolve_access_token", lambda timeout: "token")
925
+ monkeypatch.setattr(
926
+ agy_quota_probe, "_post_json", lambda url, *args: summary if url == _QUOTA_URL else models
927
+ )
928
+ result = agy_quota_probe.probe_quota()
929
+ assert result is not None
930
+ assert [group.name for group in result.groups] == ["CLAUDE AND GPT MODELS"]
931
+
932
+
933
+ def test_probe_summary_gemini_does_not_fetch_models(monkeypatch: pytest.MonkeyPatch) -> None:
934
+ summary = _groups_payload([_gemini_group([])])
935
+ urlopen, visited = _build_urlopen({_QUOTA_URL: summary})
936
+ monkeypatch.setattr(agy_quota_probe, "_resolve_access_token", lambda timeout: "token")
937
+ monkeypatch.setattr(agy_quota_probe, "urlopen", urlopen)
938
+ assert agy_quota_probe.probe_quota() is not None
939
+ assert visited == [(_QUOTA_URL, "Bearer token")]
940
+
941
+
942
+ @pytest.mark.parametrize("weekly", [True, False])
943
+ def test_cache_roundtrips_unreported_window(weekly: bool) -> None:
944
+ window = AgyQuotaWindow(50, "1h 0m", 60)
945
+ group = AgyQuotaGroup(
946
+ "GEMINI MODELS", ["Gemini Flash"], window if weekly else None, None if weekly else window
947
+ )
948
+ result = AgyQuotaResult([group], datetime.now(UTC).isoformat())
949
+ agy_quota_probe._write_cache(result)
950
+ payload = json.loads(agy_quota_probe.CACHE_PATH.read_text())
951
+ assert payload["groups"][0]["five_hour" if weekly else "weekly"] is None
952
+ assert agy_quota_probe._read_cache() == result
@@ -335,7 +335,14 @@ def test_sqlite_opens_readonly(monkeypatch: pytest.MonkeyPatch) -> None:
335
335
  def quota() -> dict[str, Any]:
336
336
  return {
337
337
  "fetched_at": now(),
338
- "groups": [{"weekly": {"remaining_percent": 40}, "five_hour": {"remaining_percent": 80}}],
338
+ "groups": [
339
+ {
340
+ "name": "GEMINI MODELS",
341
+ "models": ["Gemini Flash"],
342
+ "weekly": {"remaining_percent": 40},
343
+ "five_hour": {"remaining_percent": 80},
344
+ }
345
+ ],
339
346
  }
340
347
 
341
348
 
@@ -344,6 +351,12 @@ def test_agy_quota_ok(quota: dict[str, Any]) -> None:
344
351
  assert check.check_agy_quota_cache()[0] == "OK"
345
352
 
346
353
 
354
+ def test_agy_quota_accepts_unreported_window(quota: dict[str, Any]) -> None:
355
+ quota["groups"][0]["weekly"] = None
356
+ write_json(check.agy_quota_probe.CACHE_PATH, quota)
357
+ assert check.check_agy_quota_cache()[0] == "OK"
358
+
359
+
347
360
  @pytest.mark.parametrize("missing", ["groups", "weekly", "five_hour"])
348
361
  def test_agy_quota_missing(quota: dict[str, Any], missing: str) -> None:
349
362
  if missing == "groups":
@@ -222,6 +222,18 @@ def test_agy_quota_uses_cache_only(
222
222
  assert result["antigravity:GEMINI MODELS"].resets_at == NOW + timedelta(hours=2)
223
223
 
224
224
 
225
+ def test_agy_quota_skips_group_without_reported_weekly(
226
+ paths: tuple[Path, Path, Path], monkeypatch: pytest.MonkeyPatch
227
+ ) -> None:
228
+ monkeypatch.setattr(rate_limits, "load_rate_limits", lambda: None)
229
+ monkeypatch.setattr(codex_loader, "load_rate_limits", lambda: None)
230
+ five_hour = agy_quota_probe.AgyQuotaWindow(99.5, None, 100)
231
+ group = agy_quota_probe.AgyQuotaGroup("GEMINI MODELS", ["Gemini Flash"], None, five_hour)
232
+ cached = agy_quota_probe.AgyQuotaResult([group], NOW.isoformat())
233
+ monkeypatch.setattr(agy_quota_probe, "_read_cache", lambda: cached)
234
+ assert "antigravity:GEMINI MODELS" not in ledger.load_windows()
235
+
236
+
225
237
  def test_render_ledger_formats_values_and_escapes_title() -> None:
226
238
  row: ledger.LedgerRow = {
227
239
  "date": "2026-09-26",
@@ -227,6 +227,27 @@ def test_project_quota_marks_full_session_with_sliding_countdown_as_full() -> No
227
227
  assert projection.session.reset_text != "Resets in 4h 59m"
228
228
 
229
229
 
230
+ def test_project_quota_shows_session_paused_when_weekly_is_spent() -> None:
231
+ quota = AgyQuotaResult(
232
+ groups=[
233
+ AgyQuotaGroup(
234
+ name="CLAUDE AND GPT MODELS",
235
+ models=["model"],
236
+ five_hour=AgyQuotaWindow(100.0, None, None),
237
+ weekly=AgyQuotaWindow(0.0, "57h", 3420),
238
+ )
239
+ ],
240
+ fetched_at="2026-01-01T00:00:00+00:00",
241
+ )
242
+
243
+ projection = menubar_agy.project_quota(quota, "en", now=1_767_225_600.0)
244
+
245
+ assert projection is not None
246
+ assert projection.session.percent_text == "Paused"
247
+ assert projection.session.reset_text == "Weekly quota used up; waits for weekly reset"
248
+ assert projection.weekly.percent == 100.0
249
+
250
+
230
251
  def test_project_quota_marks_cached_result_stale_after_twenty_minutes() -> None:
231
252
  now = datetime(2026, 1, 1, 1, 0, tzinfo=UTC)
232
253
  quota = AgyQuotaResult(
@@ -513,3 +534,27 @@ def test_fresh_cache_with_expired_login_is_stale() -> None:
513
534
  assert state is not None
514
535
  assert state["ageText"] == "登入過期"
515
536
  assert "agy" in state["tooltip"]
537
+
538
+
539
+ @pytest.mark.parametrize("weekly", [True, False])
540
+ def test_unreported_window_is_unavailable_and_not_recorded(weekly: bool) -> None:
541
+ now = datetime.now(UTC)
542
+ window = AgyQuotaWindow(50, "1h 0m", 60)
543
+ group = AgyQuotaGroup(
544
+ "GEMINI MODELS", [], window if weekly else None, None if weekly else window
545
+ )
546
+ trackers = _burn_rate_trackers()
547
+ projection = menubar_agy.project_quota(
548
+ AgyQuotaResult([group], now.isoformat()),
549
+ "en",
550
+ now=now.timestamp(),
551
+ burn_rate_trackers=trackers,
552
+ )
553
+ assert projection is not None
554
+ row = projection.session if weekly else projection.weekly
555
+ assert row.available is False
556
+ assert row.percent is None
557
+ assert row.percent_text == "--"
558
+ assert row.reset_text == "Not reported by Google"
559
+ assert trackers["agy_session" if weekly else "agy_weekly"].last_timestamp is None
560
+ assert (projection.weekly if weekly else projection.session).percent_text == "50% used"
@@ -44,11 +44,9 @@ def test_footer_layout(tmp_path: Path, script: str) -> None:
44
44
  assert result.returncode == 0, result.stdout + result.stderr
45
45
 
46
46
 
47
- @pytest.mark.parametrize("panel", sorted((ROOT / "docs" / "panels").glob("*.html")))
48
- def test_agy_group_button_visibility(panel: Path) -> None:
49
- source = panel.read_text(encoding="utf-8")
50
- if 'data-action="set_agy_quota_group"' not in source:
51
- pytest.skip("panel has no Antigravity group button")
47
+ def test_agy_group_button_visibility() -> None:
48
+ # The app loads assets/panels/panel_core.js; docs/panels are generated demo copies.
49
+ source = (ROOT / "assets" / "panels" / "panel_core.js").read_text(encoding="utf-8")
52
50
  node = shutil.which("node")
53
51
  if node is None:
54
52
  pytest.skip("Node is required to execute panel JavaScript")