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