usage-cli 0.34.15__tar.gz → 0.34.17__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 (298) hide show
  1. {usage_cli-0.34.15/usage_cli.egg-info → usage_cli-0.34.17}/PKG-INFO +3 -3
  2. {usage_cli-0.34.15 → usage_cli-0.34.17}/README.md +2 -2
  3. {usage_cli-0.34.15 → usage_cli-0.34.17}/analyzer/reporter.py +1 -1
  4. {usage_cli-0.34.15 → usage_cli-0.34.17}/i18n.json +15 -15
  5. {usage_cli-0.34.15 → usage_cli-0.34.17}/installer/session_hooks.py +3 -3
  6. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/agy_quota_probe.py +201 -19
  7. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/codex_disk_cache.py +10 -2
  8. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/codex_events.py +1 -0
  9. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/codex_loader.py +8 -4
  10. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/agy.py +10 -3
  11. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/refresh.py +3 -0
  12. {usage_cli-0.34.15 → usage_cli-0.34.17}/pyproject.toml +1 -1
  13. {usage_cli-0.34.15 → usage_cli-0.34.17}/quota/agy_window_keeper.py +51 -12
  14. {usage_cli-0.34.15 → usage_cli-0.34.17}/quota/status_payload.py +11 -1
  15. usage_cli-0.34.17/tests/test_agy_background_auth.py +536 -0
  16. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_agy_quota_probe.py +165 -1
  17. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_codex_loader.py +139 -0
  18. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_disk_cache_shards.py +75 -0
  19. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_pricing.py +7 -0
  20. usage_cli-0.34.17/tests/test_quota_aware.py +1772 -0
  21. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_service_status.py +52 -0
  22. usage_cli-0.34.17/tests/test_status_payload.py +64 -0
  23. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_statusline_agy.py +52 -1
  24. {usage_cli-0.34.15 → usage_cli-0.34.17}/ui/tables.py +1 -0
  25. {usage_cli-0.34.15 → usage_cli-0.34.17/usage_cli.egg-info}/PKG-INFO +3 -3
  26. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_cli.egg-info/SOURCES.txt +2 -0
  27. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_common/pricing.py +6 -0
  28. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_common/service_status.py +34 -0
  29. usage_cli-0.34.17/usage_hooks/usage_quota_aware.py +590 -0
  30. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_hooks/usage_statusline_agy.py +31 -17
  31. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_hooks/usage_terse_mode.py +50 -8
  32. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_hooks/usage_terse_reminder.py +17 -15
  33. {usage_cli-0.34.15 → usage_cli-0.34.17}/wintray/app.py +2 -0
  34. usage_cli-0.34.15/tests/test_quota_aware.py +0 -473
  35. usage_cli-0.34.15/usage_hooks/usage_quota_aware.py +0 -217
  36. {usage_cli-0.34.15 → usage_cli-0.34.17}/LICENSE +0 -0
  37. {usage_cli-0.34.15 → usage_cli-0.34.17}/THIRD_PARTY_NOTICES.md +0 -0
  38. {usage_cli-0.34.15 → usage_cli-0.34.17}/adapters/__init__.py +0 -0
  39. {usage_cli-0.34.15 → usage_cli-0.34.17}/adapters/agy.py +0 -0
  40. {usage_cli-0.34.15 → usage_cli-0.34.17}/adapters/claude.py +0 -0
  41. {usage_cli-0.34.15 → usage_cli-0.34.17}/adapters/codex.py +0 -0
  42. {usage_cli-0.34.15 → usage_cli-0.34.17}/adapters/grok.py +0 -0
  43. {usage_cli-0.34.15 → usage_cli-0.34.17}/adapters/muse.py +0 -0
  44. {usage_cli-0.34.15 → usage_cli-0.34.17}/adapters/rate_limits.py +0 -0
  45. {usage_cli-0.34.15 → usage_cli-0.34.17}/adapters/registry.py +0 -0
  46. {usage_cli-0.34.15 → usage_cli-0.34.17}/adapters/types.py +0 -0
  47. {usage_cli-0.34.15 → usage_cli-0.34.17}/analyzer/__init__.py +0 -0
  48. {usage_cli-0.34.15 → usage_cli-0.34.17}/analyzer/aggregator.py +0 -0
  49. {usage_cli-0.34.15 → usage_cli-0.34.17}/analyzer/blocks.py +0 -0
  50. {usage_cli-0.34.15 → usage_cli-0.34.17}/analyzer/diagnoser.py +0 -0
  51. {usage_cli-0.34.15 → usage_cli-0.34.17}/analyzer/diagnosis_snapshot.py +0 -0
  52. {usage_cli-0.34.15 → usage_cli-0.34.17}/analyzer/dispatch_ledger.py +0 -0
  53. {usage_cli-0.34.15 → usage_cli-0.34.17}/analyzer/history_persist.py +0 -0
  54. {usage_cli-0.34.15 → usage_cli-0.34.17}/analyzer/insights.py +0 -0
  55. {usage_cli-0.34.15 → usage_cli-0.34.17}/analyzer/ledger_backfill.py +0 -0
  56. {usage_cli-0.34.15 → usage_cli-0.34.17}/analyzer/ledger_legacy_backfill.py +0 -0
  57. {usage_cli-0.34.15 → usage_cli-0.34.17}/analyzer/persona_loader.py +0 -0
  58. {usage_cli-0.34.15 → usage_cli-0.34.17}/analyzer/subscription.py +0 -0
  59. {usage_cli-0.34.15 → usage_cli-0.34.17}/analyzer/usage_snapshot.py +0 -0
  60. {usage_cli-0.34.15 → usage_cli-0.34.17}/installer/__init__.py +0 -0
  61. {usage_cli-0.34.15 → usage_cli-0.34.17}/installer/claude_pane.py +0 -0
  62. {usage_cli-0.34.15 → usage_cli-0.34.17}/installer/codex_hook_trust.py +0 -0
  63. {usage_cli-0.34.15 → usage_cli-0.34.17}/installer/login_item.py +0 -0
  64. {usage_cli-0.34.15 → usage_cli-0.34.17}/installer/setup_hook.py +0 -0
  65. {usage_cli-0.34.15 → usage_cli-0.34.17}/installer/statusline_settings.py +0 -0
  66. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/__init__.py +0 -0
  67. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/_shared_cache_file.py +0 -0
  68. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/agy_disk_cache.py +0 -0
  69. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/agy_loader.py +0 -0
  70. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/cache_quarantine.py +0 -0
  71. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/chromium_cache.py +0 -0
  72. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/claude_desktop.py +0 -0
  73. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/claude_paths.py +0 -0
  74. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/claude_usage.py +0 -0
  75. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/codex_fork_replay.py +0 -0
  76. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/codex_paths.py +0 -0
  77. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/disk_cache_common.py +0 -0
  78. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/disk_cache_lifecycle.py +0 -0
  79. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/grok_loader.py +0 -0
  80. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/grok_quota_probe.py +0 -0
  81. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/history_disk_cache.py +0 -0
  82. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/history_loader.py +0 -0
  83. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/jsonl_limits.py +0 -0
  84. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/jsonl_utils.py +0 -0
  85. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/muse_loader.py +0 -0
  86. {usage_cli-0.34.15 → usage_cli-0.34.17}/loaders/muse_quota_probe.py +0 -0
  87. {usage_cli-0.34.15 → usage_cli-0.34.17}/main.py +0 -0
  88. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/__init__.py +0 -0
  89. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/actions.py +0 -0
  90. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/app.py +0 -0
  91. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/chrome.py +0 -0
  92. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/fsevents_watch.py +0 -0
  93. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/grok.py +0 -0
  94. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/manual_refresh.py +0 -0
  95. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/menu.py +0 -0
  96. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/muse.py +0 -0
  97. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/new_badge.py +0 -0
  98. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/notify.py +0 -0
  99. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/popover.py +0 -0
  100. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/prefs.py +0 -0
  101. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/quota_countdown.py +0 -0
  102. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/state.py +0 -0
  103. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/title.py +0 -0
  104. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/update.py +0 -0
  105. {usage_cli-0.34.15 → usage_cli-0.34.17}/menubar/whats_new.py +0 -0
  106. {usage_cli-0.34.15 → usage_cli-0.34.17}/panels/__init__.py +0 -0
  107. {usage_cli-0.34.15 → usage_cli-0.34.17}/panels/base.py +0 -0
  108. {usage_cli-0.34.15 → usage_cli-0.34.17}/panels/catalog.py +0 -0
  109. {usage_cli-0.34.15 → usage_cli-0.34.17}/panels/dynamic_height.py +0 -0
  110. {usage_cli-0.34.15 → usage_cli-0.34.17}/panels/panel_scale.py +0 -0
  111. {usage_cli-0.34.15 → usage_cli-0.34.17}/panels/panel_window.py +0 -0
  112. {usage_cli-0.34.15 → usage_cli-0.34.17}/panels/panel_window_state.py +0 -0
  113. {usage_cli-0.34.15 → usage_cli-0.34.17}/panels/payload.py +0 -0
  114. {usage_cli-0.34.15 → usage_cli-0.34.17}/panels/web_panel.py +0 -0
  115. {usage_cli-0.34.15 → usage_cli-0.34.17}/panels/window_drag.py +0 -0
  116. {usage_cli-0.34.15 → usage_cli-0.34.17}/quota/__init__.py +0 -0
  117. {usage_cli-0.34.15 → usage_cli-0.34.17}/quota/burn_rate.py +0 -0
  118. {usage_cli-0.34.15 → usage_cli-0.34.17}/quota/codex_window_keeper.py +0 -0
  119. {usage_cli-0.34.15 → usage_cli-0.34.17}/quota/keeper_outcome.py +0 -0
  120. {usage_cli-0.34.15 → usage_cli-0.34.17}/quota/notifications.py +0 -0
  121. {usage_cli-0.34.15 → usage_cli-0.34.17}/quota/quota_snapshot.py +0 -0
  122. {usage_cli-0.34.15 → usage_cli-0.34.17}/quota/usage_rate.py +0 -0
  123. {usage_cli-0.34.15 → usage_cli-0.34.17}/quota/window_keeper.py +0 -0
  124. {usage_cli-0.34.15 → usage_cli-0.34.17}/setup.cfg +0 -0
  125. {usage_cli-0.34.15 → usage_cli-0.34.17}/setup_app.py +0 -0
  126. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_adapters_claude.py +0 -0
  127. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_adapters_codex.py +0 -0
  128. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_adapters_rate_limits.py +0 -0
  129. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_aggregator.py +0 -0
  130. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_agy_adapter.py +0 -0
  131. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_agy_disk_cache.py +0 -0
  132. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_agy_loader.py +0 -0
  133. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_agy_window_keeper.py +0 -0
  134. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_analyzer_blocks.py +0 -0
  135. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_analyzer_pipeline.py +0 -0
  136. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_app_version.py +0 -0
  137. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_burn_rate.py +0 -0
  138. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_cache_quarantine.py +0 -0
  139. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_check_release_version.py +0 -0
  140. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_check_upstream.py +0 -0
  141. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_chromium_cache.py +0 -0
  142. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_claude_desktop.py +0 -0
  143. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_claude_expired_windows.py +0 -0
  144. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_claude_pane.py +0 -0
  145. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_claude_paths.py +0 -0
  146. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_cli_lookup.py +0 -0
  147. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_codex_consistency.py +0 -0
  148. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_codex_hook_trust.py +0 -0
  149. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_codex_window_keeper.py +0 -0
  150. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_compact_cards.py +0 -0
  151. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_desktop_fresh_cache.py +0 -0
  152. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_desktop_quota_display.py +0 -0
  153. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_desktop_quota_review.py +0 -0
  154. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_diagnoser.py +0 -0
  155. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_dispatch_ledger.py +0 -0
  156. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_doctor.py +0 -0
  157. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_dynamic_height.py +0 -0
  158. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_fsevents_watch.py +0 -0
  159. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_grok_loader.py +0 -0
  160. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_grok_quota_probe.py +0 -0
  161. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_history_loader.py +0 -0
  162. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_history_persist.py +0 -0
  163. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_hook_scripts_are_standalone.py +0 -0
  164. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_html_report_snapshot.py +0 -0
  165. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_i18n_format_fallback.py +0 -0
  166. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_i18n_key_parity.py +0 -0
  167. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_i18n_packaged_path.py +0 -0
  168. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_insights.py +0 -0
  169. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_jsonl_utils.py +0 -0
  170. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_keeper_menu.py +0 -0
  171. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_keeper_outcome.py +0 -0
  172. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_ledger_backfill.py +0 -0
  173. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_ledger_legacy_backfill.py +0 -0
  174. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_login_item.py +0 -0
  175. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_main.py +0 -0
  176. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_manual_refresh.py +0 -0
  177. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_menubar.py +0 -0
  178. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_menubar_agy.py +0 -0
  179. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_menubar_grok.py +0 -0
  180. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_menubar_muse.py +0 -0
  181. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_menubar_notify.py +0 -0
  182. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_menubar_permission.py +0 -0
  183. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_menubar_prefs.py +0 -0
  184. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_menubar_state.py +0 -0
  185. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_menubar_update.py +0 -0
  186. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_muse_coverage.py +0 -0
  187. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_muse_loader.py +0 -0
  188. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_muse_quota_probe.py +0 -0
  189. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_muse_review_regressions.py +0 -0
  190. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_network.py +0 -0
  191. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_new_badge.py +0 -0
  192. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_packaged_resources.py +0 -0
  193. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_packaging.py +0 -0
  194. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_panel_footer_layout.py +0 -0
  195. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_panel_scale.py +0 -0
  196. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_panel_window_state.py +0 -0
  197. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_panels.py +0 -0
  198. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_panels_lazy_import.py +0 -0
  199. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_persona_loader.py +0 -0
  200. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_project_resolver.py +0 -0
  201. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_quota_label.py +0 -0
  202. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_quota_threshold_menu.py +0 -0
  203. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_report_chart_redesign.py +0 -0
  204. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_report_cold_read.py +0 -0
  205. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_report_daily_chart.py +0 -0
  206. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_report_filter_js.py +0 -0
  207. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_report_headings.py +0 -0
  208. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_report_share_mask.py +0 -0
  209. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_reporter_build_data.py +0 -0
  210. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_session_resume.py +0 -0
  211. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_setup_app.py +0 -0
  212. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_setup_hook.py +0 -0
  213. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_setup_hook_coexistence.py +0 -0
  214. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_setup_hook_resume.py +0 -0
  215. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_setup_hook_terse.py +0 -0
  216. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_subprocess_utils.py +0 -0
  217. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_subscription.py +0 -0
  218. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_taskbar_menu.py +0 -0
  219. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_taskbar_obstacles.py +0 -0
  220. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_time_utils.py +0 -0
  221. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_tui.py +0 -0
  222. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_tui_sprite.py +0 -0
  223. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_ui_tables.py +0 -0
  224. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_update_checker.py +0 -0
  225. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_update_gate.py +0 -0
  226. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_update_release_notes.py +0 -0
  227. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_cli.py +0 -0
  228. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_client.py +0 -0
  229. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_diagnosis_snapshot.py +0 -0
  230. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_dir_sweeper.py +0 -0
  231. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_lang.py +0 -0
  232. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_logging.py +0 -0
  233. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_notifications.py +0 -0
  234. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_rate.py +0 -0
  235. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_snapshot.py +0 -0
  236. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_snapshot_replay.py +0 -0
  237. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_statusline.py +0 -0
  238. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_statusline_forwarder.py +0 -0
  239. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_statusline_grok.py +0 -0
  240. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_terse_mode.py +0 -0
  241. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_usage_terse_reminder.py +0 -0
  242. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_web_panel_payload.py +0 -0
  243. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_whats_new.py +0 -0
  244. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_wheel_packaging.py +0 -0
  245. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_win_login_item.py +0 -0
  246. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_window_keeper.py +0 -0
  247. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_windows_manifest.py +0 -0
  248. {usage_cli-0.34.15 → usage_cli-0.34.17}/tests/test_wintray.py +0 -0
  249. {usage_cli-0.34.15 → usage_cli-0.34.17}/tui/__init__.py +0 -0
  250. {usage_cli-0.34.15 → usage_cli-0.34.17}/tui/app.py +0 -0
  251. {usage_cli-0.34.15 → usage_cli-0.34.17}/tui/sprite.py +0 -0
  252. {usage_cli-0.34.15 → usage_cli-0.34.17}/ui/__init__.py +0 -0
  253. {usage_cli-0.34.15 → usage_cli-0.34.17}/ui/html_report.py +0 -0
  254. {usage_cli-0.34.15 → usage_cli-0.34.17}/ui/report_charts.py +0 -0
  255. {usage_cli-0.34.15 → usage_cli-0.34.17}/ui/report_daily_chart.py +0 -0
  256. {usage_cli-0.34.15 → usage_cli-0.34.17}/ui/report_filter.py +0 -0
  257. {usage_cli-0.34.15 → usage_cli-0.34.17}/ui/report_ledger.py +0 -0
  258. {usage_cli-0.34.15 → usage_cli-0.34.17}/ui/report_scripts.py +0 -0
  259. {usage_cli-0.34.15 → usage_cli-0.34.17}/ui/report_styles.py +0 -0
  260. {usage_cli-0.34.15 → usage_cli-0.34.17}/updates/__init__.py +0 -0
  261. {usage_cli-0.34.15 → usage_cli-0.34.17}/updates/checker.py +0 -0
  262. {usage_cli-0.34.15 → usage_cli-0.34.17}/updates/gate.py +0 -0
  263. {usage_cli-0.34.15 → usage_cli-0.34.17}/updates/release_notes.py +0 -0
  264. {usage_cli-0.34.15 → usage_cli-0.34.17}/updates/whats_new.py +0 -0
  265. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_app/__init__.py +0 -0
  266. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_app/cli.py +0 -0
  267. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_app/doctor.py +0 -0
  268. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_cli.egg-info/dependency_links.txt +0 -0
  269. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_cli.egg-info/entry_points.txt +0 -0
  270. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_cli.egg-info/requires.txt +0 -0
  271. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_cli.egg-info/top_level.txt +0 -0
  272. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_common/__init__.py +0 -0
  273. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_common/app_version.py +0 -0
  274. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_common/cli_lookup.py +0 -0
  275. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_common/i18n.py +0 -0
  276. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_common/network.py +0 -0
  277. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_common/prefs.py +0 -0
  278. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_common/project_resolver.py +0 -0
  279. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_common/subprocess_utils.py +0 -0
  280. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_common/time_utils.py +0 -0
  281. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_common/usage_dir_sweeper.py +0 -0
  282. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_common/usage_lang.py +0 -0
  283. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_common/usage_logging.py +0 -0
  284. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_hooks/__init__.py +0 -0
  285. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_hooks/usage_session_resume.py +0 -0
  286. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_hooks/usage_statusline.py +0 -0
  287. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_hooks/usage_statusline_forwarder.py +0 -0
  288. {usage_cli-0.34.15 → usage_cli-0.34.17}/usage_hooks/usage_statusline_grok.py +0 -0
  289. {usage_cli-0.34.15 → usage_cli-0.34.17}/wintray/__init__.py +0 -0
  290. {usage_cli-0.34.15 → usage_cli-0.34.17}/wintray/login_item.py +0 -0
  291. {usage_cli-0.34.15 → usage_cli-0.34.17}/wintray/menu.py +0 -0
  292. {usage_cli-0.34.15 → usage_cli-0.34.17}/wintray/prefs.py +0 -0
  293. {usage_cli-0.34.15 → usage_cli-0.34.17}/wintray/quota_label.py +0 -0
  294. {usage_cli-0.34.15 → usage_cli-0.34.17}/wintray/taskbar_menu.py +0 -0
  295. {usage_cli-0.34.15 → usage_cli-0.34.17}/wintray/taskbar_obstacles.py +0 -0
  296. {usage_cli-0.34.15 → usage_cli-0.34.17}/wintray/taskbar_overlay.py +0 -0
  297. {usage_cli-0.34.15 → usage_cli-0.34.17}/wintray/watch.py +0 -0
  298. {usage_cli-0.34.15 → usage_cli-0.34.17}/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.15
3
+ Version: 0.34.17
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
@@ -123,7 +123,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
123
123
  | **Token Saver** | Asks Claude Code and Codex for shorter, plainer replies while keeping code and error messages byte-exact. In an A/B test on real sessions, late replies stayed **~40% shorter** instead of drifting 84% longer. | Off · no extra model calls |
124
124
  | **Side pane** | Quotas, other conversations, and background jobs next to your work. [See the side pane](#claude-code-side-pane). | Off · no extra model calls |
125
125
  | **Beginner Mode** | Explains up to three technical terms from each answer in one plain line, then brings them back for review. | Off · a little Claude quota |
126
- | **Quota-Aware Mode** | When a quota runs low, Claude tells you before a big task that would use it, and lets you do a smaller part or wait for the reset. | Off · no extra model calls |
126
+ | **Quota-Aware Mode** | When a quota runs low, Claude tells you before a big task that would use it, and lets you do a smaller part or wait for the reset. Claude also gets a short quota overview at the start of each conversation. | Off · no extra model calls |
127
127
  | **Auto-start 5-hour session** | Right after a 5-hour quota resets, sends each tool one tiny message so the next window starts counting right away. | Off · negligible quota |
128
128
  | **Token-waste health check** | A daily scan of your logs for repeated file reads and noisy output. Say "show me" and the AI walks you through fixes. | Comes with Progress Concierge · no extra model calls |
129
129
 
@@ -146,7 +146,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
146
146
  - **Linux:** CI verifies `usage setup` on Ubuntu.
147
147
  - **Progress Concierge:** When you `/resume` a conversation that sat long enough for its cache to expire, it warns how many tokens the next message will re-send and suggests `/compact` first.
148
148
  - **Beginner Mode (macOS and Windows):** Terms appear above the prompt in your UI language. Press 9 to mark them understood; skipped terms come back after 1, 3, then 7 days. Understood terms return as a one-question multiple-choice quiz 7, 21, then 60 days later, at most one a day, and a wrong answer puts the term back in the hints. `/terms` opens your term history. Picking terms asks Claude Haiku through your Claude Code, and pauses while your 5-hour quota is at 90% or more.
149
- - **Quota-Aware Mode (macOS and Windows):** Claude Code gets one line with what is left and when it resets when a 5-hour quota passes 80%, 90%, or 95%, or a weekly quota passes 95%. It covers Claude Code, Codex, Antigravity, and Muse, says each level once per conversation, and treats each quota and model group separately, so a used-up one only matters for work that runs on it.
149
+ - **Quota-Aware Mode (macOS and Windows):** Claude Code gets one line with what is left and when it resets when a 5-hour quota passes 80%, 90%, or 95%, or a weekly quota passes 95%. It covers Claude Code, Codex, Antigravity, and Muse, says each level once per conversation, tells Claude once when a quota it warned about resets, and treats each quota and model group separately, so a used-up one only matters for work that runs on it. Official Claude Code / Claude API or Codex API incidents and recovery also notify Claude, once per status change in each conversation. Claude also gets a short quota overview at the start of each conversation. After `/login` changes accounts, Claude hears once that earlier Claude Code quota notices no longer apply.
150
150
  - **Auto-start 5-hour session:** Claude gets Haiku, Antigravity gets Gemini 3.8 Flash Low, and Codex gets its cheapest model. The quota used is negligible. The Claude and Codex pings skip your own hooks and stay out of `claude --resume` and `codex resume`.
151
151
  - **Panel:** It stays put when another app takes focus; a second click on the menu bar icon, or Escape, closes it. Card order is shared across every theme with quota cards (all except World Cup 2026) and survives restarts.
152
152
  - **HTML reports:** A "What you worked on" section lists the names Claude Code gave your recent conversations, and masking covers those titles too.
@@ -101,7 +101,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
101
101
  | **Token Saver** | Asks Claude Code and Codex for shorter, plainer replies while keeping code and error messages byte-exact. In an A/B test on real sessions, late replies stayed **~40% shorter** instead of drifting 84% longer. | Off · no extra model calls |
102
102
  | **Side pane** | Quotas, other conversations, and background jobs next to your work. [See the side pane](#claude-code-side-pane). | Off · no extra model calls |
103
103
  | **Beginner Mode** | Explains up to three technical terms from each answer in one plain line, then brings them back for review. | Off · a little Claude quota |
104
- | **Quota-Aware Mode** | When a quota runs low, Claude tells you before a big task that would use it, and lets you do a smaller part or wait for the reset. | Off · no extra model calls |
104
+ | **Quota-Aware Mode** | When a quota runs low, Claude tells you before a big task that would use it, and lets you do a smaller part or wait for the reset. Claude also gets a short quota overview at the start of each conversation. | Off · no extra model calls |
105
105
  | **Auto-start 5-hour session** | Right after a 5-hour quota resets, sends each tool one tiny message so the next window starts counting right away. | Off · negligible quota |
106
106
  | **Token-waste health check** | A daily scan of your logs for repeated file reads and noisy output. Say "show me" and the AI walks you through fixes. | Comes with Progress Concierge · no extra model calls |
107
107
 
@@ -124,7 +124,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
124
124
  - **Linux:** CI verifies `usage setup` on Ubuntu.
125
125
  - **Progress Concierge:** When you `/resume` a conversation that sat long enough for its cache to expire, it warns how many tokens the next message will re-send and suggests `/compact` first.
126
126
  - **Beginner Mode (macOS and Windows):** Terms appear above the prompt in your UI language. Press 9 to mark them understood; skipped terms come back after 1, 3, then 7 days. Understood terms return as a one-question multiple-choice quiz 7, 21, then 60 days later, at most one a day, and a wrong answer puts the term back in the hints. `/terms` opens your term history. Picking terms asks Claude Haiku through your Claude Code, and pauses while your 5-hour quota is at 90% or more.
127
- - **Quota-Aware Mode (macOS and Windows):** Claude Code gets one line with what is left and when it resets when a 5-hour quota passes 80%, 90%, or 95%, or a weekly quota passes 95%. It covers Claude Code, Codex, Antigravity, and Muse, says each level once per conversation, and treats each quota and model group separately, so a used-up one only matters for work that runs on it.
127
+ - **Quota-Aware Mode (macOS and Windows):** Claude Code gets one line with what is left and when it resets when a 5-hour quota passes 80%, 90%, or 95%, or a weekly quota passes 95%. It covers Claude Code, Codex, Antigravity, and Muse, says each level once per conversation, tells Claude once when a quota it warned about resets, and treats each quota and model group separately, so a used-up one only matters for work that runs on it. Official Claude Code / Claude API or Codex API incidents and recovery also notify Claude, once per status change in each conversation. Claude also gets a short quota overview at the start of each conversation. After `/login` changes accounts, Claude hears once that earlier Claude Code quota notices no longer apply.
128
128
  - **Auto-start 5-hour session:** Claude gets Haiku, Antigravity gets Gemini 3.8 Flash Low, and Codex gets its cheapest model. The quota used is negligible. The Claude and Codex pings skip your own hooks and stay out of `claude --resume` and `codex resume`.
129
129
  - **Panel:** It stays put when another app takes focus; a second click on the menu bar icon, or Escape, closes it. Card order is shared across every theme with quota cards (all except World Cup 2026) and survives restarts.
130
130
  - **HTML reports:** A "What you worked on" section lists the names Claude Code gave your recent conversations, and masking covers those titles too.
@@ -45,7 +45,7 @@ _YEAR_WEEKS = 53
45
45
  YEAR_CACHE_PATH = Path(os.path.expanduser("~/.usage/year_cache.json"))
46
46
  YEAR_LEDGER_PATH = YEAR_CACHE_PATH.with_name("year_ledger.json")
47
47
  YEAR_CACHE_TTL_SECONDS = 6 * 3600
48
- _YEAR_CACHE_SCHEMA = 7
48
+ _YEAR_CACHE_SCHEMA = 8
49
49
  _YEAR_LEDGER_SCHEMA = 1
50
50
  _year_ledger_lock = threading.RLock()
51
51
  _YEAR_LEDGER_TRIM_BUFFER_DAYS = 60
@@ -338,8 +338,8 @@
338
338
  "project_butler_tooltip": "開新對話時,自動把上次的進度交給 AI,不用再重講一次。",
339
339
  "terse_mode_menu": "省 token 模式",
340
340
  "terse_mode_tooltip": "開新對話時,自動要求 Claude 回覆更簡短、更白話,以節省輸出 token(有裝 Codex 的話也會一起套用);但程式碼、指令、路徑與錯誤訊息不會被縮減。之後你送的每則訊息也會輕聲提醒 Claude 維持精簡。",
341
- "terse_reminder_instruction": "[精簡模式仍生效:這則回覆保持精簡、用白話;程式碼、指令、路徑、錯誤訊息照舊一字不改;安全警示與不可逆操作確認仍要講完整;使用者明確要求詳細解說時,以使用者當下的要求為準。]",
342
- "terse_mode_instruction": "(這個對話已開啟「精簡模式」:請在這次對話第一則回覆的最前面提一下這件事——如果你同時收到其他要打招呼的指示(例如進度交接),就把「精簡模式已開啟」自然併入那句招呼裡就好,不要另外多開一句;如果沒有其他招呼可以搭,就自己說一行「🐾 已開啟精簡模式,回覆會盡量簡短,繼續吧!」。整個對話都適用。去掉虛詞贅字、客套語、重複鋪陳與不必要的過渡句;用詞挑簡短的(例如「修」不要「針對這個問題實作解決方案」)。精簡是預算,白話是風格:短不等於難懂。挑最口語的說法,能用日常字就不要用術語(例如「先存檔」不要「先持久化」)。非用不可的技術詞,第一次出現時在後面補十個字以內的白話解釋,之後直接用。工具或子代理的輸出不要原文轉貼:先讀懂再用白話重寫成結論,只有程式碼、指令、路徑、錯誤訊息照原文保留。不要比喻。名詞化還原成動詞:進行修改→改、做出決定→決定。刪只預告不給資訊的句子:接下來我要說明、值得注意的是、在深入之前。每句先已知後新知。一句只講一件事,一句盡量在三十字內。用主動句:寫「程式送出金鑰」,不寫「金鑰被程式送出」。同一個東西從頭到尾用同一個詞,叫過「hook」就別再換成「鉤子」。收尾就三件事:做了什麼、成功沒、下一步做什麼。不用裝飾性表格;除了開頭那句招呼,內文不放表情符號。不要複述工具名稱或呼叫過程,但開工具前用一句話說明要做什麼是可以的。不要自創縮寫(例如「設定」別縮成「設」、「函式」別縮成「函」)——這類縮寫斷詞長度跟完整詞一樣,省不到字數,反而讓讀者要多想一下,直接用完整詞更省事也更清楚。程式碼、指令、檔案路徑、錯誤訊息一個字都不能省略或改寫。遇到安全警示、不可逆操作的確認、或多步驟中省略連接詞會有誤讀風險的情況,這幾種要先恢復完整、講清楚,講完再切回精簡語氣。如果使用者明確要求詳細解說、逐步教學,或情境需要完整推理,仍以使用者當下的要求為準,不要因為這個模式而省略關鍵資訊。)",
341
+ "terse_reminder_instruction": "[精簡模式仍生效:這則回覆保持精簡、用白話、讀一遍就懂;程式碼、指令、路徑、錯誤訊息照舊一字不改;安全警示與不可逆操作確認仍要講完整;使用者明確要求詳細解說時,以使用者當下的要求為準。]",
342
+ "terse_mode_instruction": "(這個對話已開啟「精簡模式」:請在這次對話第一則回覆的最前面提一下這件事——如果你同時收到其他要打招呼的指示(例如進度交接),就把「精簡模式已開啟」自然併入那句招呼裡就好,不要另外多開一句;如果沒有其他招呼可以搭,就自己說一行「🐾 已開啟精簡模式,回覆會盡量簡短,繼續吧!」。整個對話都適用。去掉贅字(其實、基本上、真的)、客套語、重複鋪陳與不必要的過渡句;用詞挑簡短的(例如「修」不要「針對這個問題實作解決方案」)。精簡是預算,白話是風格:短不等於難懂。省字的底線是讀一遍就懂、不必回頭重讀;AI 味常常是省字省過頭造成的。讓讀者猜得到下一個詞:詞語照常見的搭配用;別讓東西做只有人會做的事,寫「快取存的是舊資料」,不寫「快取在說謊」;不用賦能、抓手、落地、閉環這類行話;不寫「這不是 X,而是 Y」的翻轉句,除非真的在糾正誤會。別讓讀者一次記太多:主詞和動詞擺近,名詞前面的長修飾語拆到下一句;名詞別疊成一串,寫「改快取過期後的處理方式」,不寫「快取失效處理機制調整」;「因為、所以、但是、如果」這類交代關係的詞,別為了省字拿掉。挑最口語的說法,能用日常字就不要用術語(例如「先存檔」不要「先持久化」)。非用不可的技術詞,第一次出現時在後面補十個字以內的白話解釋,之後直接用。工具或子代理的輸出不要原文轉貼:先讀懂再用白話重寫成結論,只有程式碼、指令、路徑、錯誤訊息照原文保留。不要比喻。名詞化還原成動詞:進行修改→改、做出決定→決定。刪只預告不給資訊的句子:接下來我要說明、值得注意的是、在深入之前。每句先已知後新知。一句只講一件事,一句盡量在三十字內。用主動句:寫「程式送出金鑰」,不寫「金鑰被程式送出」。同一個東西從頭到尾用同一個詞,叫過「hook」就別再換成「鉤子」。收尾就三件事:做了什麼、成功沒、下一步做什麼。不用裝飾性表格;除了開頭那句招呼,內文不放表情符號。不要複述工具名稱或呼叫過程,但開工具前用一句話說明要做什麼是可以的。不要自創縮寫(例如「設定」別縮成「設」、「函式」別縮成「函」)——這類縮寫斷詞長度跟完整詞一樣,省不到字數,反而讓讀者要多想一下,直接用完整詞更省事也更清楚。程式碼、指令、檔案路徑、錯誤訊息一個字都不能省略或改寫。遇到安全警示、不可逆操作的確認、或多步驟中省略連接詞會有誤讀風險的情況,這幾種要先恢復完整、講清楚,講完再切回精簡語氣。如果使用者明確要求詳細解說、逐步教學,或情境需要完整推理,仍以使用者當下的要求為準,不要因為這個模式而省略關鍵資訊。)",
343
343
  "terse_mode_enabled_msg": "已開啟。以後開新的 Claude Code 對話都會自動要求回覆更精簡、更白話(有裝 Codex 的話也會一起套用);程式碼、指令、路徑和錯誤訊息不會被縮減。下次開新對話即生效。",
344
344
  "terse_mode_disabled_msg": "已關閉。開新對話時不再自動要求精簡回覆。",
345
345
  "resume_enabled_restart": "已開啟。以後開新的 Claude Code 對話,會自動記得這個專案上次做到哪——畫面上不會有提示(這是正常的),但 Claude 已經知道。下次開新對話即生效。",
@@ -556,7 +556,7 @@
556
556
  "claude_beginner_disabled_msg": "已關閉新手模式。重開 Claude Code 後生效。",
557
557
  "claude_beginner_action_failed": "無法切換新手模式",
558
558
  "quota_aware_menu": "額度感知模式",
559
- "quota_aware_tooltip": "額度快用完時,Claude 開大任務前會先告訴你剩多少、幾點重置,讓你選先做小的還是等重置。",
559
+ "quota_aware_tooltip": "額度快用完時,Claude 開大任務前會先告訴你剩多少、幾點重置,讓你選先做小的還是等重置。Claude Code 或 Codex 官方服務出狀況時,也會告訴 Claude。每個對話開始時,也會給 Claude 一份各家額度的簡短總覽。",
560
560
  "quota_aware_enabled_msg": "已開啟額度感知模式。請重啟 Claude Code 套用。",
561
561
  "quota_aware_disabled_msg": "已關閉額度感知模式。請重啟 Claude Code 套用。",
562
562
  "quota_aware_action_failed": "無法切換額度感知模式。",
@@ -918,8 +918,8 @@
918
918
  "project_butler_tooltip": "When you open a new session, automatically hand your last progress to the AI — no need to re-explain.",
919
919
  "terse_mode_menu": "Token Saver",
920
920
  "terse_mode_tooltip": "When you open a new session, automatically ask Claude to reply more briefly and in plainer language to save output tokens (applies to Codex too, if it's installed); code, commands, paths, and error messages stay untouched. A light reminder is also injected after each message you send to keep replies terse.",
921
- "terse_reminder_instruction": "[Terse mode is still on: keep this reply brief and plain-spoken; code, commands, paths, and error messages stay byte-exact; security warnings and irreversible-action confirmations must still be written out in full; if the user explicitly asks for a detailed walkthrough, follow that instead.]",
922
- "terse_mode_instruction": "(Terse mode is on for this entire conversation. Mention this at the very start of your first reply — if you're already leading with another greeting (e.g. a resume handoff), fold \"terse mode is on\" into that same line instead of adding a separate one; if there's no other greeting to fold into, say your own line: \"🐾 Terse mode is on — keeping replies short, let's go!\" Drop filler (just/really/basically/actually) and pleasantries (sure/certainly/happy to). Prefer short synonyms (big, not extensive; fix, not \"implement a solution for\"). Terse is the budget; plain is the style — short must never mean cryptic. Pick the everyday word over the jargon one (\"save it first\", not \"persist it first\"). When a technical term is unavoidable, gloss it once on first use in eight words or fewer, then just use it. Never paste a tool's or subagent's output verbatim: read it, then rewrite it as a plain-language conclusion — only code, commands, paths, and error messages stay verbatim. No analogies. Zombie nouns to verbs: \"make a decision\" to \"decide\". Cut metadiscourse: \"Let me explain\", \"It's worth noting\", \"Before diving in\". Given info first in a sentence, new info last. One idea per sentence; keep sentences under about 20 words. Use active voice: \"the program sends the key\", not \"the key is sent\". One word for one thing: once you call it \"hook\", don't switch to \"callback\". Close with what you did, whether it worked, and what to do next. No decorative tables, and no emoji in the body beyond the opening greeting. Don't recite tool names or narrate calls — but one line of intent before running a tool is fine. Never invent abbreviations (cfg/impl/req/res) — the tokenizer splits them the same as the full word, so nothing is saved and the reader still has to decode it; use the full word instead. Code, commands, file paths, and error messages must stay byte-exact, never trimmed or rewritten. Drop terseness for security warnings, irreversible-action confirmations, and multi-step instructions where a fragment or dropped conjunction risks being misread — write those out in full, then resume terse mode after. If the user explicitly asks for a detailed walkthrough, step-by-step teaching, or the situation needs full reasoning, follow that instead — don't drop essential information just to stay terse.)",
921
+ "terse_reminder_instruction": "[Terse mode is still on: keep this reply brief, plain-spoken, and clear on a first read; code, commands, paths, and error messages stay byte-exact; security warnings and irreversible-action confirmations must still be written out in full; if the user explicitly asks for a detailed walkthrough, follow that instead.]",
922
+ "terse_mode_instruction": "(Terse mode is on for this entire conversation. Mention this at the very start of your first reply — if you're already leading with another greeting (e.g. a resume handoff), fold \"terse mode is on\" into that same line instead of adding a separate one; if there's no other greeting to fold into, say your own line: \"🐾 Terse mode is on — keeping replies short, let's go!\" Drop filler (just/really/basically/actually) and pleasantries (sure/certainly/happy to). Prefer short synonyms (big, not extensive; fix, not \"implement a solution for\"). Terse is the budget; plain is the style — short must never mean cryptic. The floor for cutting: the reader gets it in one pass, without rereading. Much AI-sounding prose comes from cutting below that floor. Keep the next word predictable: use common word pairings; don't give things human actions (\"the cache holds stale data\", not \"the cache is lying\"); don't turn nouns into verbs (\"show the error\", not \"surface the error\"); no buzzwords like leverage or empower; skip the \"it's not X, it's Y\" reversal unless you're correcting a real misunderstanding. Don't make the reader hold too much in mind: keep the verb close to its subject and move long modifiers to the next sentence; unstack noun piles (\"fix how the cache handles expiry\", not \"fix the cache expiry handling logic\"); keep small words that mark structure, like \"that\" (\"she knew that the answer was wrong\") and because/so/but/if. Pick the everyday word over the jargon one (\"save it first\", not \"persist it first\"). When a technical term is unavoidable, gloss it once on first use in eight words or fewer, then just use it. Never paste a tool's or subagent's output verbatim: read it, then rewrite it as a plain-language conclusion — only code, commands, paths, and error messages stay verbatim. No analogies. Zombie nouns to verbs: \"make a decision\" to \"decide\". Cut metadiscourse: \"Let me explain\", \"It's worth noting\", \"Before diving in\". Given info first in a sentence, new info last. One idea per sentence; keep sentences under about 20 words. Use active voice: \"the program sends the key\", not \"the key is sent\". One word for one thing: once you call it \"hook\", don't switch to \"callback\". Close with what you did, whether it worked, and what to do next. No decorative tables, and no emoji in the body beyond the opening greeting. Don't recite tool names or narrate calls — but one line of intent before running a tool is fine. Never invent abbreviations (cfg/impl/req/res) — the tokenizer splits them the same as the full word, so nothing is saved and the reader still has to decode it; use the full word instead. Code, commands, file paths, and error messages must stay byte-exact, never trimmed or rewritten. Drop terseness for security warnings, irreversible-action confirmations, and multi-step instructions where a fragment or dropped conjunction risks being misread — write those out in full, then resume terse mode after. If the user explicitly asks for a detailed walkthrough, step-by-step teaching, or the situation needs full reasoning, follow that instead — don't drop essential information just to stay terse.)",
923
923
  "terse_mode_enabled_msg": "Enabled. New Claude Code sessions will automatically ask for terser, plainer replies (Codex too, if it's installed), while code, commands, paths, and error messages stay untouched. Takes effect the next time you start a session.",
924
924
  "terse_mode_disabled_msg": "Disabled. New sessions will no longer auto-request terse replies.",
925
925
  "resume_enabled_restart": "Enabled. New Claude Code sessions will automatically know where you left off in this project — you won't see a prompt (that's expected), but Claude will. Takes effect the next time you start a session.",
@@ -1136,7 +1136,7 @@
1136
1136
  "claude_beginner_disabled_msg": "Beginner Mode disabled. Restart Claude Code to apply.",
1137
1137
  "claude_beginner_action_failed": "Could not change Beginner Mode",
1138
1138
  "quota_aware_menu": "Quota-Aware Mode",
1139
- "quota_aware_tooltip": "When quota is running low, Claude tells you how much remains and when it resets before starting a large task, so you can choose a smaller part now or wait for the reset.",
1139
+ "quota_aware_tooltip": "When quota is running low, Claude tells you how much remains and when it resets before starting a large task, so you can choose a smaller part now or wait for the reset. Claude is also notified of official Claude Code or Codex service incidents. Claude also gets a short quota overview at the start of each conversation.",
1140
1140
  "quota_aware_enabled_msg": "Quota-Aware Mode enabled. Restart Claude Code to apply.",
1141
1141
  "quota_aware_disabled_msg": "Quota-Aware Mode disabled. Restart Claude Code to apply.",
1142
1142
  "quota_aware_action_failed": "Could not change Quota-Aware Mode.",
@@ -1498,8 +1498,8 @@
1498
1498
  "project_butler_tooltip": "开新对话时,自动把上次的进度交给 AI,不用再重讲一次。",
1499
1499
  "terse_mode_menu": "省 token 模式",
1500
1500
  "terse_mode_tooltip": "开新对话时,自动要求 Claude 回复更简短、更白话,以节省输出 token(装了 Codex 的话也会一起套用);但代码、指令、路径和错误信息不会被缩减。之后你发的每则消息也会轻声提醒 Claude 保持精简。",
1501
- "terse_reminder_instruction": "[精简模式仍生效:这则回复保持精简、用白话;代码、指令、路径、错误信息照旧一字不改;安全警示与不可逆操作确认仍要讲完整;用户明确要求详细讲解时,以用户当下的要求为准。]",
1502
- "terse_mode_instruction": "(这个对话已开启「精简模式」:请在这次对话第一则回复的最前面提一下这件事——如果你同时收到其他要打招呼的指示(例如进度交接),就把「精简模式已开启」自然并入那句招呼里就好,不要另外多开一句;如果没有其他招呼可以搭,就自己说一行「🐾 已开启精简模式,回复会尽量简短,继续吧!」。整个对话都适用。去掉虚词赘字、客套语、重复铺陈和不必要的过渡句;用词挑简短的(例如「修」不要「针对这个问题实现解决方案」)。精简是预算,白话是风格:短不等于难懂。挑最口语的说法,能用日常字就不要用术语(例如「先保存」不要「先持久化」)。非用不可的技术词,第一次出现时在后面补十个字以内的白话解释,之后直接用。工具或子代理的输出不要原文转贴:先读懂再用白话重写成结论,只有代码、指令、路径、错误信息照原文保留。不要比喻。名词化还原成动词:进行修改→改、做出决定→决定。删只预告不给信息的句子:接下来我要说明、值得注意的是、在深入之前。每句先已知后新知。一句只讲一件事,一句尽量在三十字内。用主动句:写「程序发送密钥」,不写「密钥被程序发送」。同一个东西从头到尾用同一个词,叫过「hook」就别再换成「钩子」。收尾就三件事:做了什么、成功没、下一步做什么。不用装饰性表格;除了开头那句招呼,正文不放表情符号。不要复述工具名称或调用过程,但开工具前用一句话说明要做什么是可以的。不要自创缩写(例如「配置」别缩成「配」、「函数」别缩成「函」)——这类缩写分词长度跟完整词一样,省不到字数,反而让读者要多想一下,直接用完整词更省事也更清楚。代码、指令、文件路径、错误信息一个字都不能省略或改写。遇到安全警示、不可逆操作的确认、或多步骤中省略连接词会有误读风险的情况,这几种要先恢复完整、讲清楚,讲完再切回精简语气。如果用户明确要求详细讲解、逐步教学,或情境需要完整推理,仍以用户当下的要求为准,不要因为这个模式而省略关键信息。)",
1501
+ "terse_reminder_instruction": "[精简模式仍生效:这则回复保持精简、用白话、读一遍就懂;代码、指令、路径、错误信息照旧一字不改;安全警示与不可逆操作确认仍要讲完整;用户明确要求详细讲解时,以用户当下的要求为准。]",
1502
+ "terse_mode_instruction": "(这个对话已开启「精简模式」:请在这次对话第一则回复的最前面提一下这件事——如果你同时收到其他要打招呼的指示(例如进度交接),就把「精简模式已开启」自然并入那句招呼里就好,不要另外多开一句;如果没有其他招呼可以搭,就自己说一行「🐾 已开启精简模式,回复会尽量简短,继续吧!」。整个对话都适用。去掉赘字(其实、基本上、真的)、客套语、重复铺陈和不必要的过渡句;用词挑简短的(例如「修」不要「针对这个问题实现解决方案」)。精简是预算,白话是风格:短不等于难懂。省字的底线是读一遍就懂、不用回头重读;AI 味常常是省字省过头造成的。让读者猜得到下一个词:词语按常见的搭配用;别让东西做只有人会做的事,写「缓存存的是旧数据」,不写「缓存在撒谎」;不用赋能、抓手、落地、闭环这类黑话;不写「这不是 X,而是 Y」的反转句,除非真的在纠正误会。别让读者一次记太多:主语和动词放近,名词前面的长修饰语拆到下一句;名词别叠成一串,写「改缓存过期后的处理方式」,不写「缓存失效处理机制调整」;「因为、所以、但是、如果」这类交代关系的词,别为了省字删掉。挑最口语的说法,能用日常字就不要用术语(例如「先保存」不要「先持久化」)。非用不可的技术词,第一次出现时在后面补十个字以内的白话解释,之后直接用。工具或子代理的输出不要原文转贴:先读懂再用白话重写成结论,只有代码、指令、路径、错误信息照原文保留。不要比喻。名词化还原成动词:进行修改→改、做出决定→决定。删只预告不给信息的句子:接下来我要说明、值得注意的是、在深入之前。每句先已知后新知。一句只讲一件事,一句尽量在三十字内。用主动句:写「程序发送密钥」,不写「密钥被程序发送」。同一个东西从头到尾用同一个词,叫过「hook」就别再换成「钩子」。收尾就三件事:做了什么、成功没、下一步做什么。不用装饰性表格;除了开头那句招呼,正文不放表情符号。不要复述工具名称或调用过程,但开工具前用一句话说明要做什么是可以的。不要自创缩写(例如「配置」别缩成「配」、「函数」别缩成「函」)——这类缩写分词长度跟完整词一样,省不到字数,反而让读者要多想一下,直接用完整词更省事也更清楚。代码、指令、文件路径、错误信息一个字都不能省略或改写。遇到安全警示、不可逆操作的确认、或多步骤中省略连接词会有误读风险的情况,这几种要先恢复完整、讲清楚,讲完再切回精简语气。如果用户明确要求详细讲解、逐步教学,或情境需要完整推理,仍以用户当下的要求为准,不要因为这个模式而省略关键信息。)",
1503
1503
  "terse_mode_enabled_msg": "已开启。以后开新的 Claude Code 对话都会自动要求回复更精简、更白话(装了 Codex 的话也会一起套用);代码、指令、路径和错误信息不会被缩减。下次开新对话即生效。",
1504
1504
  "terse_mode_disabled_msg": "已关闭。开新对话时不再自动要求精简回复。",
1505
1505
  "resume_enabled_restart": "已开启。以后开新的 Claude Code 对话,会自动记得这个项目上次做到哪——画面上不会有提示(这是正常的),但 Claude 已经知道。下次开新对话即生效。",
@@ -1716,7 +1716,7 @@
1716
1716
  "claude_beginner_disabled_msg": "已关闭新手模式。重启 Claude Code 后生效。",
1717
1717
  "claude_beginner_action_failed": "无法切换新手模式",
1718
1718
  "quota_aware_menu": "额度感知模式",
1719
- "quota_aware_tooltip": "额度快用完时,Claude 开大任务前会先告诉你还剩多少、几点重置,让你选先做小的还是等重置。",
1719
+ "quota_aware_tooltip": "额度快用完时,Claude 开大任务前会先告诉你还剩多少、几点重置,让你选先做小的还是等重置。Claude Code 或 Codex 官方服务出状况时,也会告诉 Claude。每个对话开始时,也会给 Claude 一份各家额度的简短总览。",
1720
1720
  "quota_aware_enabled_msg": "已开启额度感知模式。请重启 Claude Code 应用。",
1721
1721
  "quota_aware_disabled_msg": "已关闭额度感知模式。请重启 Claude Code 应用。",
1722
1722
  "quota_aware_action_failed": "无法切换额度感知模式。",
@@ -2078,8 +2078,8 @@
2078
2078
  "project_butler_tooltip": "新規セッションを開くと、前回の進捗を自動で AI に引き継ぎます。説明し直す必要はありません。",
2079
2079
  "terse_mode_menu": "トークン節約モード",
2080
2080
  "terse_mode_tooltip": "新規セッションを開くと、Claude に短めで平易な言葉での返答を自動で求めて出力 token を節約します(Codex を導入済みならそちらにも適用されます)。コード、コマンド、パス、エラーメッセージは省略しません。その後もあなたが送る各メッセージで、簡潔さを保つよう軽く念押しされます。",
2081
- "terse_reminder_instruction": "[簡潔モードは引き続き有効:この返信は簡潔に、平易な言葉で;コード、コマンド、パス、エラーメッセージは一字そのまま;セキュリティ警告と不可逆操作の確認は引き続き完全に記述すること;ユーザーが明確に詳しい解説を求めた場合は、その要求を優先してください。]",
2082
- "terse_mode_instruction": "(この会話では「簡潔モード」が有効です。最初の返信の冒頭でこのことに触れてください——すでに他の挨拶(進捗の引き継ぎなど)を述べる予定がある場合は、「簡潔モードが有効」であることをその挨拶に自然に組み込み、別の行を追加しないでください。組み込める挨拶がない場合は、自分で一行「🐾 簡潔モードが有効になりました。返答は短くしていきます!」と述べてください。会話全体に適用されます。前置き、丁寧すぎる言い回し、重複した表現、不必要なつなぎ文を省き、短文や箇条書きで済むなら長い段落に広げないでください。言葉は短い方を選んでください(例:「修正」であって「この問題に対する解決策を実装する」ではない)。簡潔さは予算、平易な言葉はスタイルです。短いからといって難解であってはなりません。専門用語よりも日常的な言葉を選んでください(例:「先に永続化する」ではなく「先に保存する」)。どうしても専門用語が必要な場合は、最初の使用時に10文字以内で平易な説明を補足し、以後はそのまま使用してください。ツールやサブエージェントの出力をそのまま貼り付けないこと。まず理解し、平易な言葉で結論として書き直す。原文のまま残すのはコード、コマンド、パス、エラーメッセージだけ。比喩は不要です。名詞化は動詞に戻す(修正を行う→修正する)。予告だけの文は削る(「これから説明します」「注目すべきは」)。各文は既知が先、新情報が後。一文に一つの内容だけ、一文はおおむね40字以内。能動態で書く(「キーはプログラムによって送られる」ではなく「プログラムがキーを送る」)。同じものには最後まで同じ語を使う(一度「hook」と呼んだら「フック関数」に言い換えない)。最後は、何をしたか、成功したか、次に何をするかの3点だけで締めくくってください。装飾的な表は不要です。冒頭の挨拶を除き、本文に絵文字は入れないでください。ツール名や呼び出し過程の実況は不要ですが、ツールを使う前に何をするかを一言添えるのは構いません。独自の省略語は作らないでください(例:「設定」を「設」に略すなど)——トークナイザー上は完全な語と同じ長さになり、何も節約にならず読み手が余計に解読する手間が増えるだけなので、完全な語のままの方が得です。コード、コマンド、ファイルパス、エラーメッセージは一文字たりとも省略・書き換えしないこと。セキュリティ警告、不可逆操作の確認、接続詞を省くと誤読の恐れがある複数手順の説明では、簡潔さより先に完全な文で明確に伝え、伝え終えたら簡潔モードに戻ってください。ユーザーが明確に詳しい解説や段階的な説明を求めた場合、または状況的に十分な推論が必要な場合は、その要求を優先し、このモードのために重要な情報を落とさないでください。)",
2081
+ "terse_reminder_instruction": "[簡潔モードは引き続き有効:この返信は簡潔に、平易な言葉で、一度読めば分かるように;コード、コマンド、パス、エラーメッセージは一字そのまま;セキュリティ警告と不可逆操作の確認は引き続き完全に記述すること;ユーザーが明確に詳しい解説を求めた場合は、その要求を優先してください。]",
2082
+ "terse_mode_instruction": "(この会話では「簡潔モード」が有効です。最初の返信の冒頭でこのことに触れてください——すでに他の挨拶(進捗の引き継ぎなど)を述べる予定がある場合は、「簡潔モードが有効」であることをその挨拶に自然に組み込み、別の行を追加しないでください。組み込める挨拶がない場合は、自分で一行「🐾 簡潔モードが有効になりました。返答は短くしていきます!」と述べてください。会話全体に適用されます。前置き、丁寧すぎる言い回し、重複した表現、不必要なつなぎ文を省き、短文や箇条書きで済むなら長い段落に広げないでください。言葉は短い方を選んでください(例:「修正」であって「この問題に対する解決策を実装する」ではない)。簡潔さは予算、平易な言葉はスタイルです。短いからといって難解であってはなりません。削ってよいのは、読み返さずに一度で分かるところまでです。AI っぽい文章の多くは、そこを越えて削ることから生まれます。次の語を予想しやすくする:語の組み合わせはよくあるものを使う。物に人間しかしない動作をさせない(「キャッシュが嘘をついている」ではなく「キャッシュに古いデータが残っている」)。エンパワー、ソリューション、シームレスのようなバズワードは使わない。誤解を本当に正すとき以外は、「これは X ではなく Y です」という反転の言い回しを使わない。一度に多くを覚えさせない:主語と述語を近づけ、名詞の前の長い修飾語は次の文に分ける。名詞を積み重ねない(「キャッシュ失効処理機構調整」ではなく「キャッシュが切れた後の処理を直す」)。「なので、だから、でも、もし」のような関係を示す語は、字数を減らすために削らない。専門用語よりも日常的な言葉を選んでください(例:「先に永続化する」ではなく「先に保存する」)。どうしても専門用語が必要な場合は、最初の使用時に10文字以内で平易な説明を補足し、以後はそのまま使用してください。ツールやサブエージェントの出力をそのまま貼り付けないこと。まず理解し、平易な言葉で結論として書き直す。原文のまま残すのはコード、コマンド、パス、エラーメッセージだけ。比喩は不要です。名詞化は動詞に戻す(修正を行う→修正する)。予告だけの文は削る(「これから説明します」「注目すべきは」)。各文は既知が先、新情報が後。一文に一つの内容だけ、一文はおおむね40字以内。能動態で書く(「キーはプログラムによって送られる」ではなく「プログラムがキーを送る」)。同じものには最後まで同じ語を使う(一度「hook」と呼んだら「フック関数」に言い換えない)。最後は、何をしたか、成功したか、次に何をするかの3点だけで締めくくってください。装飾的な表は不要です。冒頭の挨拶を除き、本文に絵文字は入れないでください。ツール名や呼び出し過程の実況は不要ですが、ツールを使う前に何をするかを一言添えるのは構いません。独自の省略語は作らないでください(例:「設定」を「設」に略すなど)——トークナイザー上は完全な語と同じ長さになり、何も節約にならず読み手が余計に解読する手間が増えるだけなので、完全な語のままの方が得です。コード、コマンド、ファイルパス、エラーメッセージは一文字たりとも省略・書き換えしないこと。セキュリティ警告、不可逆操作の確認、接続詞を省くと誤読の恐れがある複数手順の説明では、簡潔さより先に完全な文で明確に伝え、伝え終えたら簡潔モードに戻ってください。ユーザーが明確に詳しい解説や段階的な説明を求めた場合、または状況的に十分な推論が必要な場合は、その要求を優先し、このモードのために重要な情報を落とさないでください。)",
2083
2083
  "terse_mode_enabled_msg": "有効化しました。今後、新しい Claude Code セッションでは返答をより簡潔で平易な言葉にするよう自動で求めます(Codex を導入済みならそちらにも適用されます)。コード、コマンド、パス、エラーメッセージは省略しません。次回セッション開始時に反映されます。",
2084
2084
  "terse_mode_disabled_msg": "無効化しました。新規セッションで簡潔な返答を自動要求しなくなります。",
2085
2085
  "resume_enabled_restart": "有効化しました。今後、新しい Claude Code セッションを開くと、このプロジェクトで前回どこまで進めたかを自動的に把握します。画面に通知は出ません(正常です)が、Claude は把握しています。次回セッション開始時に反映されます。",
@@ -2296,7 +2296,7 @@
2296
2296
  "claude_beginner_disabled_msg": "初心者モードを無効にしました。Claude Code を再起動してください。",
2297
2297
  "claude_beginner_action_failed": "初心者モードを切り替えられません",
2298
2298
  "quota_aware_menu": "クォータ認識モード",
2299
- "quota_aware_tooltip": "クォータが少なくなると、大きなタスクを始める前に Claude が残量とリセット時刻を伝えます。小さな部分を先に進めるか、リセットを待つか選べます。",
2299
+ "quota_aware_tooltip": "クォータが少なくなると、大きなタスクを始める前に Claude が残量とリセット時刻を伝えます。小さな部分を先に進めるか、リセットを待つか選べます。Claude Code または Codex の公式サービスに障害が起きたときも Claude に伝えます。会話の開始時にも、各ツールのクォータの短い概要を Claude に伝えます。",
2300
2300
  "quota_aware_enabled_msg": "クォータ認識モードを有効にしました。Claude Code を再起動してください。",
2301
2301
  "quota_aware_disabled_msg": "クォータ認識モードを無効にしました。Claude Code を再起動してください。",
2302
2302
  "quota_aware_action_failed": "クォータ認識モードを切り替えられませんでした。",
@@ -2658,8 +2658,8 @@
2658
2658
  "project_butler_tooltip": "새 세션을 열 때 지난 진행 상황을 AI에게 자동으로 전달합니다. 처음부터 다시 설명할 필요 없어요.",
2659
2659
  "terse_mode_menu": "토큰 절약 모드",
2660
2660
  "terse_mode_tooltip": "새 세션을 열 때 Claude에게 더 짧고 쉬운 말로 답하도록 자동으로 요청해 출력 token을 아낍니다 (Codex가 설치되어 있으면 그쪽에도 적용됩니다). 코드, 명령어, 경로, 오류 메시지는 줄이지 않습니다. 이후 보내는 각 메시지에서도 간결함을 유지하도록 가볍게 다시 일러줍니다.",
2661
- "terse_reminder_instruction": "[간결 모드 계속 적용 중: 이 답변은 짧게, 쉬운 말로; 코드, 명령어, 경로, 오류 메시지는 한 글자 그대로; 보안 경고와 되돌릴 수 없는 작업 확인은 여전히 완전히 적을 것; 사용자가 자세한 설명이나 단계별 안내를 명확히 요청했다면 그 요구를 우선하세요.]",
2662
- "terse_mode_instruction": "(이 대화에는 '간결 모드'가 켜져 있습니다. 첫 답변 맨 앞에서 이 사실을 언급하세요 — 이미 다른 인사(예: 진행 상황 인수인계)를 할 예정이라면, '간결 모드가 켜졌다'는 내용을 그 인사에 자연스럽게 합쳐서 한 줄로 말하고 따로 추가하지 마세요. 합칠 인사가 없다면 직접 한 줄로 \"🐾 간결 모드를 켰어요. 답변을 짧게 이어갈게요!\"라고 말하세요. 대화 전체에 적용됩니다. 빈말, 지나친 격식, 반복되는 서두, 불필요한 연결 문장은 덜어내고, 짧은 문장이나 목록으로 충분하면 긴 단락으로 늘이지 마세요. 단어는 짧은 쪽을 고르세요 (예: \"이 문제에 대한 해결책을 구현하다\" 대신 \"고치다\"). 간결함은 예산이고, 쉬운 말은 스타일입니다. 짧다고 해서 이해하기 어려워서는 안 됩니다. 전문 용어보다 일상적인 단어를 고르세요(예: \"먼저 영속화하다\" 대신 \"먼저 저장하다\"). 전문 용어를 꼭 써야 한다면, 처음 사용할 때 10글자 이내로 쉬운 설명을 덧붙이고 그 뒤로는 그냥 쓰세요. 도구나 서브에이전트의 출력을 그대로 붙여넣지 말 것: 먼저 이해한 뒤 쉬운 말로 결론으로 다시 쓰고, 코드·명령어·경로·오류 메시지만 원문 그대로 둔다. 비유는 빼주세요. 명사화는 동사로 (수정을 진행한다 → 고친다). 예고만 하는 문장은 삭제 (\"이제 설명하겠습니다\", \"주목할 점은\"). 각 문장은 아는 것 먼저, 새 정보 나중. 한 문장에는 한 가지 내용만, 문장은 대략 40자 이내로. 능동문으로 쓰세요 (\"키가 프로그램에 의해 전송된다\" 대신 \"프로그램이 키를 보낸다\"). 같은 대상은 끝까지 같은 단어로 부르세요 (한 번 \"hook\"이라고 했으면 \"훅 함수\"로 바꾸지 않기). 마무리는 무엇을 했는지, 성공했는지, 다음에 무엇을 할 것인지 이 3가지만 적으세요. 장식용 표는 넣지 마세요. 첫 인사말을 제외하면 본문에 이모지는 넣지 마세요. 도구 이름이나 호출 과정을 중계할 필요는 없지만, 도구를 쓰기 전에 무엇을 할지 한 줄로 말하는 것은 괜찮습니다. 임의로 줄임말을 만들지 마세요 (예: \"설정\"을 \"설\"로 줄이는 식) — 토크나이저 상으로는 완전한 단어와 길이가 같아서 절약되는 게 없고 읽는 사람만 더 해석해야 하니, 완전한 단어를 쓰는 편이 더 낫습니다. 코드, 명령어, 파일 경로, 오류 메시지는 한 글자도 생략하거나 바꾸면 안 됩니다. 보안 경고, 되돌릴 수 없는 작업의 확인, 접속사를 생략하면 오독 위험이 있는 다단계 설명에서는 간결함보다 먼저 완전하고 명확하게 전달한 뒤, 전달이 끝나면 다시 간결 모드로 돌아가세요. 사용자가 자세한 설명이나 단계별 안내를 명확히 요청했거나, 상황상 충분한 추론이 꼭 필요하다면 그 요구를 우선하고, 이 모드 때문에 핵심 정보를 빼먹지 마세요.)",
2661
+ "terse_reminder_instruction": "[간결 모드 계속 적용 중: 이 답변은 짧게, 쉬운 말로, 한 번 읽으면 이해되게; 코드, 명령어, 경로, 오류 메시지는 한 글자 그대로; 보안 경고와 되돌릴 수 없는 작업 확인은 여전히 완전히 적을 것; 사용자가 자세한 설명이나 단계별 안내를 명확히 요청했다면 그 요구를 우선하세요.]",
2662
+ "terse_mode_instruction": "(이 대화에는 '간결 모드'가 켜져 있습니다. 첫 답변 맨 앞에서 이 사실을 언급하세요 — 이미 다른 인사(예: 진행 상황 인수인계)를 할 예정이라면, '간결 모드가 켜졌다'는 내용을 그 인사에 자연스럽게 합쳐서 한 줄로 말하고 따로 추가하지 마세요. 합칠 인사가 없다면 직접 한 줄로 \"🐾 간결 모드를 켰어요. 답변을 짧게 이어갈게요!\"라고 말하세요. 대화 전체에 적용됩니다. 빈말, 지나친 격식, 반복되는 서두, 불필요한 연결 문장은 덜어내고, 짧은 문장이나 목록으로 충분하면 긴 단락으로 늘이지 마세요. 단어는 짧은 쪽을 고르세요 (예: \"이 문제에 대한 해결책을 구현하다\" 대신 \"고치다\"). 간결함은 예산이고, 쉬운 말은 스타일입니다. 짧다고 해서 이해하기 어려워서는 안 됩니다. 줄여도 되는 한계는 다시 읽지 않고 한 번에 이해되는 데까지입니다. AI 같은 글은 흔히 그 한계를 넘어 줄이는 데서 생깁니다. 다음 단어를 예측할 수 있게 쓰세요: 단어는 흔히 쓰는 조합으로 쓰고, 사물에게 사람만 하는 행동을 시키지 마세요(\"캐시가 거짓말을 한다\" 대신 \"캐시에 오래된 데이터가 남아 있다\"). 시너지, 솔루션, 레버리지 같은 유행어는 쓰지 말고, 정말 오해를 바로잡을 때가 아니면 \"이것은 X가 아니라 Y입니다\" 식의 뒤집는 문장도 쓰지 마세요. 한 번에 너무 많이 기억하게 하지 마세요: 주어와 서술어를 가깝게 두고, 명사 앞의 긴 수식어는 다음 문장으로 나누세요. 명사를 줄줄이 쌓지 마세요(\"캐시 만료 처리 메커니즘 조정\" 대신 \"캐시가 만료된 뒤 처리 방식을 고치다\"). \"왜냐하면, 그래서, 하지만, 만약\" 같은 관계를 알려 주는 말은 글자 수를 줄이려고 빼지 마세요. 전문 용어보다 일상적인 단어를 고르세요(예: \"먼저 영속화하다\" 대신 \"먼저 저장하다\"). 전문 용어를 꼭 써야 한다면, 처음 사용할 때 10글자 이내로 쉬운 설명을 덧붙이고 그 뒤로는 그냥 쓰세요. 도구나 서브에이전트의 출력을 그대로 붙여넣지 말 것: 먼저 이해한 뒤 쉬운 말로 결론으로 다시 쓰고, 코드·명령어·경로·오류 메시지만 원문 그대로 둔다. 비유는 빼주세요. 명사화는 동사로 (수정을 진행한다 → 고친다). 예고만 하는 문장은 삭제 (\"이제 설명하겠습니다\", \"주목할 점은\"). 각 문장은 아는 것 먼저, 새 정보 나중. 한 문장에는 한 가지 내용만, 문장은 대략 40자 이내로. 능동문으로 쓰세요 (\"키가 프로그램에 의해 전송된다\" 대신 \"프로그램이 키를 보낸다\"). 같은 대상은 끝까지 같은 단어로 부르세요 (한 번 \"hook\"이라고 했으면 \"훅 함수\"로 바꾸지 않기). 마무리는 무엇을 했는지, 성공했는지, 다음에 무엇을 할 것인지 이 3가지만 적으세요. 장식용 표는 넣지 마세요. 첫 인사말을 제외하면 본문에 이모지는 넣지 마세요. 도구 이름이나 호출 과정을 중계할 필요는 없지만, 도구를 쓰기 전에 무엇을 할지 한 줄로 말하는 것은 괜찮습니다. 임의로 줄임말을 만들지 마세요 (예: \"설정\"을 \"설\"로 줄이는 식) — 토크나이저 상으로는 완전한 단어와 길이가 같아서 절약되는 게 없고 읽는 사람만 더 해석해야 하니, 완전한 단어를 쓰는 편이 더 낫습니다. 코드, 명령어, 파일 경로, 오류 메시지는 한 글자도 생략하거나 바꾸면 안 됩니다. 보안 경고, 되돌릴 수 없는 작업의 확인, 접속사를 생략하면 오독 위험이 있는 다단계 설명에서는 간결함보다 먼저 완전하고 명확하게 전달한 뒤, 전달이 끝나면 다시 간결 모드로 돌아가세요. 사용자가 자세한 설명이나 단계별 안내를 명확히 요청했거나, 상황상 충분한 추론이 꼭 필요하다면 그 요구를 우선하고, 이 모드 때문에 핵심 정보를 빼먹지 마세요.)",
2663
2663
  "terse_mode_enabled_msg": "활성화했습니다. 이제 새 Claude Code 세션에서는 더 간결하고 쉬운 말로 답하도록 자동으로 요청합니다 (Codex가 설치되어 있으면 그쪽에도 적용됩니다). 코드, 명령어, 경로, 오류 메시지는 줄이지 않습니다. 다음 세션 시작 시 적용됩니다.",
2664
2664
  "terse_mode_disabled_msg": "비활성화했습니다. 새 세션에서 더 이상 간결한 답변을 자동 요청하지 않습니다.",
2665
2665
  "resume_enabled_restart": "활성화했습니다. 이제 새 Claude Code 세션을 시작하면 이 프로젝트에서 지난번에 어디까지 했는지 자동으로 파악합니다. 화면에는 안내가 표시되지 않지만(정상입니다) Claude는 알고 있습니다. 다음 세션 시작 시 적용됩니다.",
@@ -2876,7 +2876,7 @@
2876
2876
  "claude_beginner_disabled_msg": "초보자 모드를 껐습니다. Claude Code를 다시 시작하세요.",
2877
2877
  "claude_beginner_action_failed": "초보자 모드를 전환할 수 없습니다",
2878
2878
  "quota_aware_menu": "할당량 인식 모드",
2879
- "quota_aware_tooltip": "할당량이 부족해지면 Claude가 큰 작업을 시작하기 전에 남은 양과 초기화 시간을 알려 줍니다. 작은 부분을 먼저 진행하거나 초기화를 기다릴 수 있습니다.",
2879
+ "quota_aware_tooltip": "할당량이 부족해지면 Claude가 큰 작업을 시작하기 전에 남은 양과 초기화 시간을 알려 줍니다. 작은 부분을 먼저 진행하거나 초기화를 기다릴 수 있습니다. Claude Code 또는 Codex 공식 서비스에 장애가 생겨도 Claude에게 알려 줍니다. 대화가 시작될 때도 Claude에게 각 도구의 할당량을 간단히 요약해 줍니다.",
2880
2880
  "quota_aware_enabled_msg": "할당량 인식 모드를 켰습니다. Claude Code를 다시 시작해 주세요.",
2881
2881
  "quota_aware_disabled_msg": "할당량 인식 모드를 껐습니다. Claude Code를 다시 시작해 주세요.",
2882
2882
  "quota_aware_action_failed": "할당량 인식 모드를 변경할 수 없습니다.",
@@ -74,7 +74,7 @@ _RESUME_DIAGNOSIS_CAUSE_KEYS = (
74
74
  )
75
75
  TERSE_HOOK_TARGET = Path(os.path.expanduser("~/.claude/usage-terse-mode.py"))
76
76
  TERSE_PROMPT_SIDECAR = Path(os.path.expanduser("~/.claude/usage-terse-prompt.json"))
77
- TERSE_HOOK_VERSION = "1.7"
77
+ TERSE_HOOK_VERSION = "1.8"
78
78
  TERSE_MATCHER = "startup|clear"
79
79
  TERSE_LANGS = ("zh-TW", "zh-CN", "en", "ja", "ko")
80
80
  _TERSE_MARKER = "usage-terse-mode"
@@ -89,13 +89,13 @@ _FEATURES_HOOKS_REGEX = re.compile(r"(?m)^[ \t]*hooks\s*=\s*[A-Za-z0-9_]+")
89
89
  # terse hook. Re-injects a one-line nudge on every prompt so the terse style holds across a
90
90
  # long conversation. Claude Code only — Codex CLI has no UserPromptSubmit equivalent.
91
91
  TERSE_REMINDER_HOOK_TARGET = Path(os.path.expanduser("~/.claude/usage-terse-reminder.py"))
92
- TERSE_REMINDER_HOOK_VERSION = "1.2"
92
+ TERSE_REMINDER_HOOK_VERSION = "1.3"
93
93
  TERSE_REMINDER_MATCHER = ""
94
94
  _TERSE_REMINDER_MARKER = "usage-terse-reminder"
95
95
  _TERSE_REMINDER_MARKERS = (_TERSE_REMINDER_MARKER, "usage_terse_reminder")
96
96
  # Independent, opt-in Claude Code UserPromptSubmit hook.
97
97
  QUOTA_AWARE_HOOK_TARGET = Path(os.path.expanduser("~/.claude/usage-quota-aware.py"))
98
- QUOTA_AWARE_HOOK_VERSION = "1.5"
98
+ QUOTA_AWARE_HOOK_VERSION = "1.12"
99
99
  QUOTA_AWARE_MATCHER = ""
100
100
  _QUOTA_AWARE_MARKER = "usage-quota-aware"
101
101
  _QUOTA_AWARE_MARKERS = (_QUOTA_AWARE_MARKER, "usage_quota_aware")
@@ -15,12 +15,15 @@ the internal ``retrieveUserQuotaSummary`` endpoint for the quota summary.
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ import hashlib
18
19
  import json
20
+ import math
19
21
  import os
20
22
  import platform
21
23
  import subprocess
22
24
  import sys
23
25
  import tempfile
26
+ import threading
24
27
  import time
25
28
  import urllib.parse
26
29
  from base64 import b64decode
@@ -41,7 +44,9 @@ from usage_common.subprocess_utils import hidden_console_kwargs
41
44
 
42
45
  CACHE_PATH = Path(os.path.expanduser("~/.usage/agy_quota_cache.json"))
43
46
  AUTH_EXPIRED_PATH = CACHE_PATH.parent / "agy_auth_expired"
47
+ CLI_SIGNED_OUT_PATH = CACHE_PATH.parent / "agy_cli_signed_out"
44
48
  _CLI_LOG_PATH = CACHE_PATH.parent / "agy_usage_cli.log"
49
+ BACKGROUND_AGY_LOCK = threading.Lock()
45
50
 
46
51
 
47
52
  def auth_expired() -> bool:
@@ -54,6 +59,38 @@ def _mark_auth_expired() -> None:
54
59
  AUTH_EXPIRED_PATH.touch()
55
60
 
56
61
 
62
+ def mark_cli_signed_out(before: str | None) -> None:
63
+ after = _credential_fingerprint()
64
+ if after != before:
65
+ return
66
+ with suppress(OSError):
67
+ CLI_SIGNED_OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
68
+ CLI_SIGNED_OUT_PATH.write_text(
69
+ json.dumps({"fingerprint": after, "marked_at": time.time()}),
70
+ encoding="utf-8",
71
+ )
72
+
73
+
74
+ def agy_cli_signed_in(timeout: float) -> bool:
75
+ """Open the gate for a readable credential; bypass cooldown only for a known change."""
76
+ global _cli_retry_after_monotonic
77
+ if not CLI_SIGNED_OUT_PATH.exists():
78
+ return True
79
+ try:
80
+ marker = json.loads(CLI_SIGNED_OUT_PATH.read_text(encoding="utf-8"))
81
+ except (OSError, ValueError, UnicodeError):
82
+ marker = {}
83
+ stored = marker.get("fingerprint") if isinstance(marker, dict) else None
84
+ current = _credential_fingerprint()
85
+ if current is None or current == stored:
86
+ return False
87
+ with suppress(OSError):
88
+ CLI_SIGNED_OUT_PATH.unlink(missing_ok=True)
89
+ if isinstance(stored, str):
90
+ _cli_retry_after_monotonic = 0.0
91
+ return True
92
+
93
+
57
94
  # Legacy OAuth token written by older Antigravity CLI versions. Read-only: we
58
95
  # never write back here (that is the CLI's home and could corrupt its login).
59
96
  _TOKEN_PATH = Path(os.path.expanduser("~/.gemini/antigravity-cli/antigravity-oauth-token"))
@@ -121,6 +158,8 @@ class AgyQuotaGroup:
121
158
  models: list[str]
122
159
  weekly: AgyQuotaWindow | None
123
160
  five_hour: AgyQuotaWindow | None
161
+ weekly_fetched_at: str | None = None
162
+ weekly_resets_at: str | None = None
124
163
 
125
164
 
126
165
  @dataclass(frozen=True, slots=True)
@@ -169,19 +208,40 @@ def _probe_via_cli(timeout: float) -> AgyQuotaResult | None:
169
208
  agy = find_agy()
170
209
  if agy is None:
171
210
  return None
172
- if time.monotonic() < _cli_retry_after_monotonic:
173
- return None
174
- # Offline (e.g. just after a sleep wake) the CLI would open a browser sign-in page.
175
- if not google_auth_reachable():
211
+ if not BACKGROUND_AGY_LOCK.acquire(blocking=False):
176
212
  return None
177
- result = _run_usage_command(agy, timeout)
178
- _cli_retry_after_monotonic = (
179
- 0.0 if result is not None else time.monotonic() + _CLI_FAILURE_COOLDOWN_SECONDS
180
- )
181
- return result
213
+ try:
214
+ if not agy_cli_signed_in(timeout) or time.monotonic() < _cli_retry_after_monotonic:
215
+ return None
216
+ # Offline, the CLI would open a browser sign-in page.
217
+ if not google_auth_reachable():
218
+ return None
219
+ before = _credential_fingerprint()
220
+ log_offset = 0
221
+ with suppress(OSError):
222
+ log_offset = _CLI_LOG_PATH.stat().st_size
223
+ result = _run_usage_command(agy, timeout, before)
224
+ if result is None:
225
+ with suppress(OSError):
226
+ with _CLI_LOG_PATH.open("rb") as log:
227
+ size = log.seek(0, os.SEEK_END)
228
+ start = log_offset if size >= log_offset else 0
229
+ log.seek(max(start, size - 65536))
230
+ tail = log.read()
231
+ if (
232
+ b"Print mode: not authenticated" in tail
233
+ or b"triggering interactive OAuth" in tail
234
+ ):
235
+ mark_cli_signed_out(before)
236
+ _cli_retry_after_monotonic = (
237
+ 0.0 if result is not None else time.monotonic() + _CLI_FAILURE_COOLDOWN_SECONDS
238
+ )
239
+ return result
240
+ finally:
241
+ BACKGROUND_AGY_LOCK.release()
182
242
 
183
243
 
184
- def _run_usage_command(agy: str, timeout: float) -> AgyQuotaResult | None:
244
+ def _run_usage_command(agy: str, timeout: float, before: str | None) -> AgyQuotaResult | None:
185
245
  command = [agy, "-p", "/usage", "--output-format", "json", "--log-file", str(_CLI_LOG_PATH)]
186
246
  try:
187
247
  completed = subprocess.run(
@@ -193,8 +253,19 @@ def _run_usage_command(agy: str, timeout: float) -> AgyQuotaResult | None:
193
253
  env=cli_env(agy),
194
254
  **hidden_console_kwargs(),
195
255
  )
256
+ except subprocess.TimeoutExpired as exc:
257
+ if "Authentication required" in str(exc.stdout) or "Authentication required" in str(
258
+ exc.stderr
259
+ ):
260
+ mark_cli_signed_out(before)
261
+ return None
196
262
  except (OSError, subprocess.SubprocessError):
197
263
  return None
264
+ if "Authentication required" in str(completed.stdout) or "Authentication required" in str(
265
+ getattr(completed, "stderr", "")
266
+ ):
267
+ mark_cli_signed_out(before)
268
+ return None
198
269
  if completed.returncode != 0:
199
270
  return None
200
271
  try:
@@ -226,11 +297,57 @@ def load_quota(max_age_minutes: float = 5) -> AgyQuotaResult | None:
226
297
  probed = probe_quota()
227
298
  if probed is None:
228
299
  return cached
300
+ probed = _carry_missing_weekly(probed, cached)
229
301
  probed = _carry_missing_groups(probed, cached)
230
302
  _write_cache(probed)
231
303
  return probed
232
304
 
233
305
 
306
+ def _carry_missing_weekly(probed: AgyQuotaResult, cached: AgyQuotaResult | None) -> AgyQuotaResult:
307
+ """Retain a missing weekly window until its cached reset, without refreshing its age."""
308
+ if cached is None:
309
+ return probed
310
+ cached_at = _parse_timestamp(cached.fetched_at)
311
+ probed_at = _parse_timestamp(probed.fetched_at)
312
+ if cached_at is None or probed_at is None:
313
+ return probed
314
+ previous = {group.name: group for group in cached.groups}
315
+ groups = []
316
+ for group in probed.groups:
317
+ old = previous.get(group.name)
318
+ window = old.weekly if old is not None else None
319
+ if (
320
+ group.weekly is None
321
+ and old is not None
322
+ and window is not None
323
+ and window.resets_in_minutes is not None
324
+ ):
325
+ resets_at = (
326
+ _parse_timestamp(old.weekly_resets_at)
327
+ if old.weekly_resets_at is not None
328
+ else cached_at + timedelta(minutes=window.resets_in_minutes)
329
+ )
330
+ if resets_at is not None and resets_at > probed_at:
331
+ minutes = max(0, math.ceil((resets_at - probed_at).total_seconds() / 60))
332
+ group = replace(
333
+ group,
334
+ weekly=replace(
335
+ window, resets_in_minutes=minutes, resets_in=_format_resets_in(minutes)
336
+ ),
337
+ weekly_fetched_at=old.weekly_fetched_at or cached.fetched_at,
338
+ weekly_resets_at=resets_at.isoformat(),
339
+ )
340
+ groups.append(group)
341
+ return replace(probed, groups=groups)
342
+
343
+
344
+ def _age_window(window: AgyQuotaWindow | None, elapsed: int) -> AgyQuotaWindow | None:
345
+ if window is None or window.resets_in_minutes is None or elapsed == 0:
346
+ return window
347
+ minutes = max(0, window.resets_in_minutes - elapsed)
348
+ return replace(window, resets_in_minutes=minutes, resets_in=_format_resets_in(minutes))
349
+
350
+
234
351
  def _carry_missing_groups(probed: AgyQuotaResult, cached: AgyQuotaResult | None) -> AgyQuotaResult:
235
352
  """Keep a group the API skipped this once, so the group toggle has something to show.
236
353
 
@@ -261,14 +378,12 @@ def _carry_missing_groups(probed: AgyQuotaResult, cached: AgyQuotaResult | None)
261
378
  else 0
262
379
  )
263
380
 
264
- def age_window(window: AgyQuotaWindow | None) -> AgyQuotaWindow | None:
265
- if window is None or window.resets_in_minutes is None or elapsed == 0:
266
- return window
267
- minutes = max(0, window.resets_in_minutes - elapsed)
268
- return replace(window, resets_in_minutes=minutes, resets_in=_format_resets_in(minutes))
269
-
270
381
  missing = [
271
- replace(group, weekly=age_window(group.weekly), five_hour=age_window(group.five_hour))
382
+ replace(
383
+ group,
384
+ weekly=_age_window(group.weekly, elapsed),
385
+ five_hour=_age_window(group.five_hour, elapsed),
386
+ )
272
387
  for group in missing
273
388
  ]
274
389
  order = {group.name: index for index, group in enumerate(cached.groups)}
@@ -276,6 +391,21 @@ def _carry_missing_groups(probed: AgyQuotaResult, cached: AgyQuotaResult | None)
276
391
  return AgyQuotaResult(groups=groups, fetched_at=probed.fetched_at)
277
392
 
278
393
 
394
+ def _credential_fingerprint() -> str | None:
395
+ """Hash only the stored credential, without resolving or refreshing a token."""
396
+ token_data: object = None
397
+ if sys.platform == "darwin":
398
+ token_data = _read_macos_credential()
399
+ if not _has_usable_token(token_data):
400
+ token_data = _read_token_file()
401
+ if not _has_usable_token(token_data) and sys.platform == "win32":
402
+ token_data = _read_windows_credential()
403
+ token = token_data.get("token") if isinstance(token_data, dict) else None
404
+ if not isinstance(token, dict):
405
+ return None
406
+ return hashlib.sha256(json.dumps(token, sort_keys=True).encode()).hexdigest()
407
+
408
+
279
409
  def _resolve_access_token(timeout: float) -> str | None:
280
410
  """Return a usable access token, refreshing when stale.
281
411
 
@@ -845,7 +975,31 @@ def _read_cache() -> AgyQuotaResult | None:
845
975
  return None
846
976
  except OSError:
847
977
  return None
848
- return _result_from_payload(payload)
978
+ result = _result_from_payload(payload)
979
+ if result is None:
980
+ return None
981
+ fetched_at = _parse_timestamp(result.fetched_at)
982
+ assert fetched_at is not None
983
+ now = datetime.now(UTC)
984
+
985
+ def weekly_expired(group: AgyQuotaGroup) -> bool:
986
+ if group.weekly_resets_at is not None:
987
+ resets_at = _parse_timestamp(group.weekly_resets_at)
988
+ if resets_at is not None:
989
+ return resets_at <= now
990
+ return (
991
+ group.weekly is not None
992
+ and group.weekly.resets_in_minutes is not None
993
+ and fetched_at + timedelta(minutes=group.weekly.resets_in_minutes) <= now
994
+ )
995
+
996
+ groups = [
997
+ replace(group, weekly=None, weekly_fetched_at=None, weekly_resets_at=None)
998
+ if group.weekly_fetched_at is not None and weekly_expired(group)
999
+ else group
1000
+ for group in result.groups
1001
+ ]
1002
+ return replace(result, groups=groups)
849
1003
 
850
1004
 
851
1005
  def _write_cache(result: AgyQuotaResult) -> None:
@@ -874,6 +1028,12 @@ def _result_to_payload(result: AgyQuotaResult) -> dict[str, object]:
874
1028
  "models": group.models,
875
1029
  "weekly": _window_to_payload(group.weekly),
876
1030
  "five_hour": _window_to_payload(group.five_hour),
1031
+ **(
1032
+ {"weekly_fetched_at": group.weekly_fetched_at}
1033
+ if group.weekly_fetched_at
1034
+ else {}
1035
+ ),
1036
+ **({"weekly_resets_at": group.weekly_resets_at} if group.weekly_resets_at else {}),
877
1037
  }
878
1038
  for group in result.groups
879
1039
  ],
@@ -917,6 +1077,8 @@ def _group_from_payload(payload: object) -> AgyQuotaGroup | None:
917
1077
  models = payload.get("models")
918
1078
  weekly = _window_from_payload(payload.get("weekly"))
919
1079
  five_hour = _window_from_payload(payload.get("five_hour"))
1080
+ weekly_fetched_at = payload.get("weekly_fetched_at")
1081
+ weekly_resets_at = payload.get("weekly_resets_at")
920
1082
  if (
921
1083
  not isinstance(name, str)
922
1084
  or not name
@@ -926,9 +1088,29 @@ def _group_from_payload(payload: object) -> AgyQuotaGroup | None:
926
1088
  or "five_hour" not in payload
927
1089
  or (payload["weekly"] is not None and weekly is None)
928
1090
  or (payload["five_hour"] is not None and five_hour is None)
1091
+ or (
1092
+ weekly_fetched_at is not None
1093
+ and (
1094
+ not isinstance(weekly_fetched_at, str)
1095
+ or _parse_timestamp(weekly_fetched_at) is None
1096
+ )
1097
+ )
1098
+ or (
1099
+ weekly_resets_at is not None
1100
+ and (
1101
+ not isinstance(weekly_resets_at, str) or _parse_timestamp(weekly_resets_at) is None
1102
+ )
1103
+ )
929
1104
  ):
930
1105
  return None
931
- return AgyQuotaGroup(name=name, models=models, weekly=weekly, five_hour=five_hour)
1106
+ return AgyQuotaGroup(
1107
+ name=name,
1108
+ models=models,
1109
+ weekly=weekly,
1110
+ five_hour=five_hour,
1111
+ weekly_fetched_at=weekly_fetched_at,
1112
+ weekly_resets_at=weekly_resets_at,
1113
+ )
932
1114
 
933
1115
 
934
1116
  def _window_from_payload(payload: object) -> AgyQuotaWindow | None:
@@ -116,6 +116,9 @@ def seed_caches(
116
116
  size = file_data["size"]
117
117
  session_id = file_data["session_id"]
118
118
  forked_from_id = file_data["forked_from_id"]
119
+ is_thread_spawn = file_data["is_thread_spawn"]
120
+ if not isinstance(is_thread_spawn, bool):
121
+ continue
119
122
  entries_data = file_data["entries"]
120
123
  confirmed_offset = int(file_data.get("confirmed_offset", 0))
121
124
  digest_hex = file_data.get("confirmed_prefix_digest", "")
@@ -128,7 +131,11 @@ def seed_caches(
128
131
  file_info_cache[path] = (
129
132
  mtime,
130
133
  size,
131
- _SessionFileInfo(session_id=session_id, forked_from_id=forked_from_id),
134
+ _SessionFileInfo(
135
+ session_id=session_id,
136
+ forked_from_id=forked_from_id,
137
+ is_thread_spawn=is_thread_spawn,
138
+ ),
132
139
  )
133
140
  if entries_data is None:
134
141
  continue
@@ -184,7 +191,7 @@ def flush_caches(
184
191
  shards: list[dict[str, Any]] = [{} for _ in range(_SHARD_COUNT)]
185
192
  for path, entry in jsonl_cache.items():
186
193
  info = file_info_cache.get(path)
187
- if entry.replay_cache_key is not None:
194
+ if entry.replay_cache_key is not None or (info is not None and info[2].forked_from_id):
188
195
  if info is None:
189
196
  continue
190
197
  entries_data = None
@@ -205,6 +212,7 @@ def flush_caches(
205
212
  "size": entry.size,
206
213
  "session_id": session_id,
207
214
  "forked_from_id": forked_from_id,
215
+ "is_thread_spawn": info[2].is_thread_spawn if info is not None else False,
208
216
  "entries": entries_data,
209
217
  "confirmed_offset": entry.confirmed_offset,
210
218
  "confirmed_prefix_digest": entry.confirmed_prefix_digest.hex(),