usage-cli 0.34.6__tar.gz → 0.34.7__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.6/usage_cli.egg-info → usage_cli-0.34.7}/PKG-INFO +20 -21
- {usage_cli-0.34.6 → usage_cli-0.34.7}/README.md +19 -20
- {usage_cli-0.34.6 → usage_cli-0.34.7}/analyzer/reporter.py +17 -12
- {usage_cli-0.34.6 → usage_cli-0.34.7}/analyzer/usage_snapshot.py +6 -3
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/agy_loader.py +0 -5
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/agy_quota_probe.py +19 -1
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/agy.py +13 -2
- {usage_cli-0.34.6 → usage_cli-0.34.7}/panels/payload.py +3 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/panels/web_panel.py +1 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/pyproject.toml +1 -1
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_agy_loader.py +0 -21
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_agy_quota_probe.py +33 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_history_persist.py +73 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_menubar_agy.py +13 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_panel_footer_layout.py +11 -2
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_web_panel_payload.py +52 -1
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_wintray.py +30 -1
- {usage_cli-0.34.6 → usage_cli-0.34.7/usage_cli.egg-info}/PKG-INFO +20 -21
- {usage_cli-0.34.6 → usage_cli-0.34.7}/wintray/app.py +2 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/LICENSE +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/THIRD_PARTY_NOTICES.md +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/adapters/__init__.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/adapters/agy.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/adapters/claude.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/adapters/codex.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/adapters/grok.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/adapters/muse.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/adapters/rate_limits.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/adapters/registry.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/adapters/types.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/analyzer/__init__.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/analyzer/aggregator.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/analyzer/blocks.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/analyzer/diagnoser.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/analyzer/diagnosis_snapshot.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/analyzer/dispatch_ledger.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/analyzer/history_persist.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/analyzer/insights.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/analyzer/ledger_backfill.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/analyzer/persona_loader.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/analyzer/subscription.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/i18n.json +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/installer/__init__.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/installer/claude_pane.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/installer/codex_hook_trust.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/installer/login_item.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/installer/session_hooks.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/installer/setup_hook.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/installer/statusline_settings.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/__init__.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/_shared_cache_file.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/agy_disk_cache.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/cache_quarantine.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/chromium_cache.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/claude_desktop.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/claude_paths.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/claude_usage.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/codex_disk_cache.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/codex_events.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/codex_fork_replay.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/codex_loader.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/codex_paths.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/disk_cache_common.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/disk_cache_lifecycle.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/grok_loader.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/grok_quota_probe.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/history_disk_cache.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/history_loader.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/jsonl_limits.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/jsonl_utils.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/muse_loader.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/loaders/muse_quota_probe.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/main.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/__init__.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/actions.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/app.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/chrome.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/fsevents_watch.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/grok.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/manual_refresh.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/menu.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/muse.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/new_badge.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/notify.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/popover.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/prefs.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/refresh.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/state.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/title.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/update.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/menubar/whats_new.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/panels/__init__.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/panels/base.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/panels/catalog.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/panels/dynamic_height.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/panels/panel_scale.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/panels/panel_window.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/panels/panel_window_state.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/panels/window_drag.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/quota/__init__.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/quota/agy_window_keeper.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/quota/burn_rate.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/quota/codex_window_keeper.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/quota/keeper_outcome.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/quota/notifications.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/quota/quota_snapshot.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/quota/status_payload.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/quota/usage_rate.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/quota/window_keeper.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/setup.cfg +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/setup_app.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_adapters_claude.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_adapters_codex.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_adapters_rate_limits.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_aggregator.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_agy_adapter.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_agy_disk_cache.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_agy_window_keeper.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_analyzer_blocks.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_analyzer_pipeline.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_app_version.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_burn_rate.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_cache_quarantine.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_check_upstream.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_chromium_cache.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_claude_desktop.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_claude_pane.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_claude_paths.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_codex_consistency.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_codex_hook_trust.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_codex_loader.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_codex_window_keeper.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_compact_cards.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_desktop_fresh_cache.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_desktop_quota_display.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_desktop_quota_review.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_diagnoser.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_disk_cache_shards.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_dispatch_ledger.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_doctor.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_dynamic_height.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_fsevents_watch.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_grok_loader.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_grok_quota_probe.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_history_loader.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_hook_scripts_are_standalone.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_html_report_snapshot.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_i18n_format_fallback.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_i18n_key_parity.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_i18n_packaged_path.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_insights.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_jsonl_utils.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_keeper_menu.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_keeper_outcome.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_ledger_backfill.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_login_item.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_main.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_manual_refresh.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_menubar.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_menubar_grok.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_menubar_muse.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_menubar_notify.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_menubar_prefs.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_menubar_state.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_menubar_update.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_muse_coverage.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_muse_loader.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_muse_quota_probe.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_muse_review_regressions.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_new_badge.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_packaged_resources.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_packaging.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_panel_scale.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_panel_window_state.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_panels.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_panels_lazy_import.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_persona_loader.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_pricing.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_project_resolver.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_quota_aware.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_quota_label.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_report_cold_read.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_report_daily_chart.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_report_filter_js.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_report_share_mask.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_reporter_build_data.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_service_status.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_session_resume.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_setup_app.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_setup_hook.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_setup_hook_coexistence.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_setup_hook_resume.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_setup_hook_terse.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_subprocess_utils.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_subscription.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_taskbar_menu.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_taskbar_obstacles.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_time_utils.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_tui.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_tui_sprite.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_ui_tables.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_update_checker.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_update_gate.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_update_release_notes.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_cli.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_client.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_dir_sweeper.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_lang.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_logging.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_notifications.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_rate.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_snapshot.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_snapshot_replay.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_statusline.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_statusline_agy.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_statusline_forwarder.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_statusline_grok.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_terse_mode.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_usage_terse_reminder.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_whats_new.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_wheel_packaging.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_win_login_item.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_window_keeper.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tests/test_windows_manifest.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tui/__init__.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tui/app.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/tui/sprite.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/ui/__init__.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/ui/html_report.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/ui/report_charts.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/ui/report_daily_chart.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/ui/report_filter.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/ui/report_ledger.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/ui/report_scripts.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/ui/report_styles.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/ui/tables.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/updates/__init__.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/updates/checker.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/updates/gate.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/updates/release_notes.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/updates/whats_new.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_app/__init__.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_app/cli.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_app/doctor.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_cli.egg-info/SOURCES.txt +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_cli.egg-info/dependency_links.txt +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_cli.egg-info/entry_points.txt +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_cli.egg-info/requires.txt +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_cli.egg-info/top_level.txt +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_common/__init__.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_common/app_version.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_common/i18n.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_common/prefs.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_common/pricing.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_common/project_resolver.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_common/service_status.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_common/subprocess_utils.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_common/time_utils.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_common/usage_dir_sweeper.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_common/usage_lang.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_common/usage_logging.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_hooks/__init__.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_hooks/usage_quota_aware.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_hooks/usage_session_resume.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_hooks/usage_statusline.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_hooks/usage_statusline_agy.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_hooks/usage_statusline_forwarder.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_hooks/usage_statusline_grok.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_hooks/usage_terse_mode.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/usage_hooks/usage_terse_reminder.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/wintray/__init__.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/wintray/login_item.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/wintray/menu.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/wintray/prefs.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/wintray/quota_label.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/wintray/taskbar_menu.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/wintray/taskbar_obstacles.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/wintray/taskbar_overlay.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/wintray/watch.py +0 -0
- {usage_cli-0.34.6 → usage_cli-0.34.7}/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.
|
|
3
|
+
Version: 0.34.7
|
|
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
|
|
@@ -28,7 +28,7 @@ Dynamic: license-file
|
|
|
28
28
|
|
|
29
29
|
### Your Claude Code, Codex, Antigravity, Grok CLI, and Muse Code quota, always on screen.
|
|
30
30
|
|
|
31
|
-
`usage` puts your 5-hour and weekly limits in the macOS menu bar or Windows system tray,
|
|
31
|
+
`usage` puts your 5-hour and weekly limits in the macOS menu bar or Windows system tray, and turns a quota red as it nears its limit. Hitting the limit halfway through a long refactor is a bad way to find out you were running low. Now you see it coming. There's nothing to run and no page to open.
|
|
32
32
|
|
|
33
33
|
[繁體中文](docs/README.zh-TW.md) · [简体中文](docs/README.zh-CN.md) · English · [日本語](docs/README.ja.md) · [한국어](docs/README.ko.md) | [Discussions](https://github.com/aqua5230/usage/discussions) | [Landing page](https://aqua5230.github.io/usage/)
|
|
34
34
|
|
|
@@ -48,7 +48,7 @@ Dynamic: license-file
|
|
|
48
48
|
</p>
|
|
49
49
|
|
|
50
50
|
- **Every quota at a glance:** Claude Code, Codex, Antigravity, and Muse Code session and weekly limits with reset countdowns, plus Grok CLI's weekly credit.
|
|
51
|
-
- **Reads what's already on your machine:** Claude Code, Codex, Grok CLI, and Muse Code tokens and cost come from local logs. Antigravity quota comes from Google's official endpoint
|
|
51
|
+
- **Reads what's already on your machine:** Claude Code, Codex, Grok CLI, and Muse Code tokens and cost come from local logs. Antigravity quota comes from its own CLI (no model call), or from Google's official endpoint using the sign-in that CLI already stores. Muse quota comes from a small background model call through its own signed-in CLI.
|
|
52
52
|
- **Warns before tokens go to waste:** The Claude Code status line flags a bloating context window and a cold prompt cache. A banner shows Claude or Codex outages.
|
|
53
53
|
- **Helpers inside Claude Code:** A side pane, shorter replies, a progress hand-off for new sessions, and quota-aware planning. All optional.
|
|
54
54
|
- **Reports and 16 themes:** HTML reports of token trends and cost, and 16 panel themes to pick from.
|
|
@@ -95,7 +95,7 @@ Then click the menu bar or tray icon.
|
|
|
95
95
|
If you've used Codex, `usage` picks up its history automatically. For Claude Code, click the **"Set Up Status Line"** button in the app popover to install the sync hook.
|
|
96
96
|
Restart the relevant tool afterward (on macOS, fully Cmd+Q Claude Code and re-open it; on Windows, restart your terminal or start a new session).
|
|
97
97
|
|
|
98
|
-
The same button also sets up a status line for the Antigravity CLI and for Grok CLI when they are installed on your machine, and does nothing at all when they aren't. Any status line you configured there yourself is backed up first and restored when you turn the switch off.
|
|
98
|
+
The same button also sets up a status line for the Antigravity CLI and for Grok CLI when they are installed on your machine, and does nothing at all when they aren't. Any status line you configured there yourself is backed up first and restored when you turn the switch off. The switch is **⚙ Settings → Claude Code → Sync quota via status line**; with it off, the Claude quota stops updating.
|
|
99
99
|
|
|
100
100
|
Once set up, the bottom of the Claude Code window will show a status line like this:
|
|
101
101
|
|
|
@@ -107,8 +107,8 @@ Once set up, the bottom of the Claude Code window will show a status line like t
|
|
|
107
107
|
|
|
108
108
|
### On Screen
|
|
109
109
|
|
|
110
|
-
- **Menu bar monitor:**
|
|
111
|
-
- **Antigravity card:** Shows the Gemini pool by default. Tap the `Gemini ⇄` tag to
|
|
110
|
+
- **Menu bar monitor:** A quota turns red as it nears its limit. Click for the full session, weekly, and per-project breakdown. On macOS, the menu bar keeps at most three tools with the highest Session usage, in Claude → Codex → Antigravity → Grok → Muse order; the panel still shows all tools.
|
|
111
|
+
- **Antigravity card:** Shows the Gemini pool by default. Tap the `Gemini ⇄` tag to cycle through the separate Claude / GPT and Gemini 4 Argon pools; the choice is remembered. Argon has a weekly limit only.
|
|
112
112
|
- **Grok CLI card:** The weekly credit percentage from Grok CLI's local debug log. Its tokens also count toward cost and project totals.
|
|
113
113
|
- **Muse Code card:** Shows 5-hour (Session) and weekly quota with reset countdowns. Available in all 15 quota themes, with a menu bar item and optional low, exhausted, and recovered quota notifications. Hidden automatically when Muse is not installed. Tokens and cost still count toward today's cost, project totals, reports, and the CLI.
|
|
114
114
|
- **Outage alerts:** An orange-red banner when Claude Code, Claude API, or Codex API is down or degraded, read from their public status pages.
|
|
@@ -129,10 +129,10 @@ Once set up, the bottom of the Claude Code window will show a status line like t
|
|
|
129
129
|
|
|
130
130
|
### Reports and More
|
|
131
131
|
|
|
132
|
-
- **HTML reports:** Daily and weekly token trends, project rankings, cost, and a Year in Review with a contribution heatmap. Export .html, .csv, or .png fully offline, with optional project-name masking.
|
|
132
|
+
- **HTML reports:** Daily and weekly token trends, project rankings, cost, and a Year in Review with a contribution heatmap. Export .html, .csv, or .png fully offline, with optional project-name masking. If you hand work from Claude Code to Codex or Antigravity, the report shows which conversations used how much of each weekly quota.
|
|
133
133
|
- **Terminal integration:** `usage status --json` hands your Claude Code, Codex, Antigravity, Grok, and Muse quota to Starship, tmux, or your own scripts. [Ready-made snippets](docs/DEVELOPMENT.md#quota-status-for-other-tools-usage-status).
|
|
134
|
-
- **AI Update Daily:** A daily public [page](https://aqua5230.github.io/ai-updates/) of Claude Code, Codex, and
|
|
135
|
-
- **Your layout:** Drag the panel anywhere, drag quota cards to reorder them, and switch among 16 themes
|
|
134
|
+
- **AI Update Daily:** A daily public [page](https://aqua5230.github.io/ai-updates/) of changes to Claude Code, Codex, Antigravity, Grok Build, Muse Code, GitHub CLI, and usage itself, with plain-language summaries in five languages.
|
|
135
|
+
- **Your layout:** Drag the panel anywhere, drag quota cards to reorder them, and switch among 16 themes from **⚙ Settings → Switch Panel**. The UI follows your system language: Traditional Chinese, Simplified Chinese, English, Japanese, or Korean. With four or more quota cards visible, all cards switch to a compact layout; three or fewer keep the usual layout.
|
|
136
136
|
|
|
137
137
|
<details>
|
|
138
138
|
<summary>Thresholds, versions, and fine print</summary>
|
|
@@ -147,7 +147,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
|
|
|
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
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.
|
|
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.
|
|
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.
|
|
153
153
|
- **AI Update Daily:** Unreviewed items show the original source text. The full history is kept.
|
|
@@ -218,19 +218,18 @@ Switch between **16 visual themes** directly from the UI:
|
|
|
218
218
|
|
|
219
219
|
## Comparison
|
|
220
220
|
|
|
221
|
+
As of October 2026. ccusage and TokenTracker read many more AI tools; `usage` focuses on five and their quotas.
|
|
222
|
+
|
|
221
223
|
| Feature | usage | ccusage | TokenTracker |
|
|
222
224
|
|---------|:-----:|:-------:|:------------:|
|
|
223
|
-
|
|
|
224
|
-
|
|
|
225
|
-
|
|
|
226
|
-
|
|
|
227
|
-
|
|
|
228
|
-
|
|
|
229
|
-
| Claude Code & Codex service-status alerts | ✅ | — | — |
|
|
230
|
-
| HTML deep reports & UI | ✅ | ✅ | — |
|
|
231
|
-
| Claude Code helpers (Token Saver, Progress Concierge, Beginner and Quota-Aware modes, health check) | ✅ | — | — |
|
|
225
|
+
| Menu bar or system tray app | macOS, Windows | — | macOS, Windows, Linux |
|
|
226
|
+
| Token and cost reports | ✅ | ✅ | ✅ |
|
|
227
|
+
| Live 5-hour and weekly quota | ✅ | Estimated Claude 5-hour blocks only | ✅ |
|
|
228
|
+
| Muse Code quota | ✅ | — | — |
|
|
229
|
+
| Service status from official status pages | Claude, Codex | — | 8 providers |
|
|
230
|
+
| Claude Code helpers (Token Saver, Progress Concierge, Beginner and Quota-Aware modes, side pane, health check) | ✅ | — | — |
|
|
232
231
|
| AI Update Daily | ✅ | — | — |
|
|
233
|
-
| Open-source license | AGPL-3.0 | MIT |
|
|
232
|
+
| Open-source license | AGPL-3.0 | MIT | MIT |
|
|
234
233
|
|
|
235
234
|
## When usage Isn't the Right Fit
|
|
236
235
|
|
|
@@ -271,7 +270,7 @@ If the menu bar shows `--`, it's usually not broken — there's just no local da
|
|
|
271
270
|
|
|
272
271
|
- **Local logs:** Claude Code, Codex, Grok CLI, and Muse Code token and cost numbers are read from log files on your machine. Their contents are never uploaded.
|
|
273
272
|
- **Claude Desktop:** Without the Claude Code CLI or a status line, keep Claude Desktop open and `usage` reads its local plan-usage history. No cookies, login tokens, or API calls are needed. Reset times appear only when its local cache confirms them; they are never estimated.
|
|
274
|
-
- **Antigravity**, only if you use it, needs network access: quota is fetched from Google's official quota endpoint with the OAuth credential the Antigravity CLI already stored after sign-in — read from macOS Keychain, Windows Credential Manager, or a local token file depending on CLI version. `usage` never writes that credential back and keeps any refreshed access token in memory only; the call itself reads quota metadata.
|
|
273
|
+
- **Antigravity**, only if you use it, needs network access: quota comes first from `agy -p /usage`, which makes no model call; if that fails, it is fetched from Google's official quota endpoint with the OAuth credential the Antigravity CLI already stored after sign-in — read from macOS Keychain, Windows Credential Manager, or a local token file depending on CLI version. `usage` never writes that credential back and keeps any refreshed access token in memory only; the call itself reads quota metadata.
|
|
275
274
|
- **Muse quota:** Muse stores no quota file locally. `usage` runs `muse exec --json` through Muse's own sign-in and saves the returned quota to `~/.usage/muse_quota.json`. This connects to Muse and makes a real model call, using a little Muse quota. It runs in an empty folder with `--no-session-log`. With Muse installed and a conversation available, it fetches the first snapshot; later probes require a snapshot over 30 minutes old and a newer Muse conversation. After failure, it waits at least 30 minutes and requires new Muse usage before retrying. Hide its card under **Hide sections**.
|
|
276
275
|
- **Other background network activity:** Public Claude and Codex status pages to flag outages, a public model-pricing table to estimate cost (built-in prices are used offline), and an occasional GitHub check for a new version.
|
|
277
276
|
- **Beginner Mode**, only if you turn it on, sends Claude Code's latest answer to Claude Haiku through your own Claude Code sign-in to pick terms. Your term list stays in `~/.usage/glossary.json`.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
### Your Claude Code, Codex, Antigravity, Grok CLI, and Muse Code quota, always on screen.
|
|
8
8
|
|
|
9
|
-
`usage` puts your 5-hour and weekly limits in the macOS menu bar or Windows system tray,
|
|
9
|
+
`usage` puts your 5-hour and weekly limits in the macOS menu bar or Windows system tray, and turns a quota red as it nears its limit. Hitting the limit halfway through a long refactor is a bad way to find out you were running low. Now you see it coming. There's nothing to run and no page to open.
|
|
10
10
|
|
|
11
11
|
[繁體中文](docs/README.zh-TW.md) · [简体中文](docs/README.zh-CN.md) · English · [日本語](docs/README.ja.md) · [한국어](docs/README.ko.md) | [Discussions](https://github.com/aqua5230/usage/discussions) | [Landing page](https://aqua5230.github.io/usage/)
|
|
12
12
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
</p>
|
|
27
27
|
|
|
28
28
|
- **Every quota at a glance:** Claude Code, Codex, Antigravity, and Muse Code session and weekly limits with reset countdowns, plus Grok CLI's weekly credit.
|
|
29
|
-
- **Reads what's already on your machine:** Claude Code, Codex, Grok CLI, and Muse Code tokens and cost come from local logs. Antigravity quota comes from Google's official endpoint
|
|
29
|
+
- **Reads what's already on your machine:** Claude Code, Codex, Grok CLI, and Muse Code tokens and cost come from local logs. Antigravity quota comes from its own CLI (no model call), or from Google's official endpoint using the sign-in that CLI already stores. Muse quota comes from a small background model call through its own signed-in CLI.
|
|
30
30
|
- **Warns before tokens go to waste:** The Claude Code status line flags a bloating context window and a cold prompt cache. A banner shows Claude or Codex outages.
|
|
31
31
|
- **Helpers inside Claude Code:** A side pane, shorter replies, a progress hand-off for new sessions, and quota-aware planning. All optional.
|
|
32
32
|
- **Reports and 16 themes:** HTML reports of token trends and cost, and 16 panel themes to pick from.
|
|
@@ -73,7 +73,7 @@ Then click the menu bar or tray icon.
|
|
|
73
73
|
If you've used Codex, `usage` picks up its history automatically. For Claude Code, click the **"Set Up Status Line"** button in the app popover to install the sync hook.
|
|
74
74
|
Restart the relevant tool afterward (on macOS, fully Cmd+Q Claude Code and re-open it; on Windows, restart your terminal or start a new session).
|
|
75
75
|
|
|
76
|
-
The same button also sets up a status line for the Antigravity CLI and for Grok CLI when they are installed on your machine, and does nothing at all when they aren't. Any status line you configured there yourself is backed up first and restored when you turn the switch off.
|
|
76
|
+
The same button also sets up a status line for the Antigravity CLI and for Grok CLI when they are installed on your machine, and does nothing at all when they aren't. Any status line you configured there yourself is backed up first and restored when you turn the switch off. The switch is **⚙ Settings → Claude Code → Sync quota via status line**; with it off, the Claude quota stops updating.
|
|
77
77
|
|
|
78
78
|
Once set up, the bottom of the Claude Code window will show a status line like this:
|
|
79
79
|
|
|
@@ -85,8 +85,8 @@ Once set up, the bottom of the Claude Code window will show a status line like t
|
|
|
85
85
|
|
|
86
86
|
### On Screen
|
|
87
87
|
|
|
88
|
-
- **Menu bar monitor:**
|
|
89
|
-
- **Antigravity card:** Shows the Gemini pool by default. Tap the `Gemini ⇄` tag to
|
|
88
|
+
- **Menu bar monitor:** A quota turns red as it nears its limit. Click for the full session, weekly, and per-project breakdown. On macOS, the menu bar keeps at most three tools with the highest Session usage, in Claude → Codex → Antigravity → Grok → Muse order; the panel still shows all tools.
|
|
89
|
+
- **Antigravity card:** Shows the Gemini pool by default. Tap the `Gemini ⇄` tag to cycle through the separate Claude / GPT and Gemini 4 Argon pools; the choice is remembered. Argon has a weekly limit only.
|
|
90
90
|
- **Grok CLI card:** The weekly credit percentage from Grok CLI's local debug log. Its tokens also count toward cost and project totals.
|
|
91
91
|
- **Muse Code card:** Shows 5-hour (Session) and weekly quota with reset countdowns. Available in all 15 quota themes, with a menu bar item and optional low, exhausted, and recovered quota notifications. Hidden automatically when Muse is not installed. Tokens and cost still count toward today's cost, project totals, reports, and the CLI.
|
|
92
92
|
- **Outage alerts:** An orange-red banner when Claude Code, Claude API, or Codex API is down or degraded, read from their public status pages.
|
|
@@ -107,10 +107,10 @@ Once set up, the bottom of the Claude Code window will show a status line like t
|
|
|
107
107
|
|
|
108
108
|
### Reports and More
|
|
109
109
|
|
|
110
|
-
- **HTML reports:** Daily and weekly token trends, project rankings, cost, and a Year in Review with a contribution heatmap. Export .html, .csv, or .png fully offline, with optional project-name masking.
|
|
110
|
+
- **HTML reports:** Daily and weekly token trends, project rankings, cost, and a Year in Review with a contribution heatmap. Export .html, .csv, or .png fully offline, with optional project-name masking. If you hand work from Claude Code to Codex or Antigravity, the report shows which conversations used how much of each weekly quota.
|
|
111
111
|
- **Terminal integration:** `usage status --json` hands your Claude Code, Codex, Antigravity, Grok, and Muse quota to Starship, tmux, or your own scripts. [Ready-made snippets](docs/DEVELOPMENT.md#quota-status-for-other-tools-usage-status).
|
|
112
|
-
- **AI Update Daily:** A daily public [page](https://aqua5230.github.io/ai-updates/) of Claude Code, Codex, and
|
|
113
|
-
- **Your layout:** Drag the panel anywhere, drag quota cards to reorder them, and switch among 16 themes
|
|
112
|
+
- **AI Update Daily:** A daily public [page](https://aqua5230.github.io/ai-updates/) of changes to Claude Code, Codex, Antigravity, Grok Build, Muse Code, GitHub CLI, and usage itself, with plain-language summaries in five languages.
|
|
113
|
+
- **Your layout:** Drag the panel anywhere, drag quota cards to reorder them, and switch among 16 themes from **⚙ Settings → Switch Panel**. The UI follows your system language: Traditional Chinese, Simplified Chinese, English, Japanese, or Korean. With four or more quota cards visible, all cards switch to a compact layout; three or fewer keep the usual layout.
|
|
114
114
|
|
|
115
115
|
<details>
|
|
116
116
|
<summary>Thresholds, versions, and fine print</summary>
|
|
@@ -125,7 +125,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
|
|
|
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
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.
|
|
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.
|
|
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.
|
|
131
131
|
- **AI Update Daily:** Unreviewed items show the original source text. The full history is kept.
|
|
@@ -196,19 +196,18 @@ Switch between **16 visual themes** directly from the UI:
|
|
|
196
196
|
|
|
197
197
|
## Comparison
|
|
198
198
|
|
|
199
|
+
As of October 2026. ccusage and TokenTracker read many more AI tools; `usage` focuses on five and their quotas.
|
|
200
|
+
|
|
199
201
|
| Feature | usage | ccusage | TokenTracker |
|
|
200
202
|
|---------|:-----:|:-------:|:------------:|
|
|
201
|
-
|
|
|
202
|
-
|
|
|
203
|
-
|
|
|
204
|
-
|
|
|
205
|
-
|
|
|
206
|
-
|
|
|
207
|
-
| Claude Code & Codex service-status alerts | ✅ | — | — |
|
|
208
|
-
| HTML deep reports & UI | ✅ | ✅ | — |
|
|
209
|
-
| Claude Code helpers (Token Saver, Progress Concierge, Beginner and Quota-Aware modes, health check) | ✅ | — | — |
|
|
203
|
+
| Menu bar or system tray app | macOS, Windows | — | macOS, Windows, Linux |
|
|
204
|
+
| Token and cost reports | ✅ | ✅ | ✅ |
|
|
205
|
+
| Live 5-hour and weekly quota | ✅ | Estimated Claude 5-hour blocks only | ✅ |
|
|
206
|
+
| Muse Code quota | ✅ | — | — |
|
|
207
|
+
| Service status from official status pages | Claude, Codex | — | 8 providers |
|
|
208
|
+
| Claude Code helpers (Token Saver, Progress Concierge, Beginner and Quota-Aware modes, side pane, health check) | ✅ | — | — |
|
|
210
209
|
| AI Update Daily | ✅ | — | — |
|
|
211
|
-
| Open-source license | AGPL-3.0 | MIT |
|
|
210
|
+
| Open-source license | AGPL-3.0 | MIT | MIT |
|
|
212
211
|
|
|
213
212
|
## When usage Isn't the Right Fit
|
|
214
213
|
|
|
@@ -249,7 +248,7 @@ If the menu bar shows `--`, it's usually not broken — there's just no local da
|
|
|
249
248
|
|
|
250
249
|
- **Local logs:** Claude Code, Codex, Grok CLI, and Muse Code token and cost numbers are read from log files on your machine. Their contents are never uploaded.
|
|
251
250
|
- **Claude Desktop:** Without the Claude Code CLI or a status line, keep Claude Desktop open and `usage` reads its local plan-usage history. No cookies, login tokens, or API calls are needed. Reset times appear only when its local cache confirms them; they are never estimated.
|
|
252
|
-
- **Antigravity**, only if you use it, needs network access: quota is fetched from Google's official quota endpoint with the OAuth credential the Antigravity CLI already stored after sign-in — read from macOS Keychain, Windows Credential Manager, or a local token file depending on CLI version. `usage` never writes that credential back and keeps any refreshed access token in memory only; the call itself reads quota metadata.
|
|
251
|
+
- **Antigravity**, only if you use it, needs network access: quota comes first from `agy -p /usage`, which makes no model call; if that fails, it is fetched from Google's official quota endpoint with the OAuth credential the Antigravity CLI already stored after sign-in — read from macOS Keychain, Windows Credential Manager, or a local token file depending on CLI version. `usage` never writes that credential back and keeps any refreshed access token in memory only; the call itself reads quota metadata.
|
|
253
252
|
- **Muse quota:** Muse stores no quota file locally. `usage` runs `muse exec --json` through Muse's own sign-in and saves the returned quota to `~/.usage/muse_quota.json`. This connects to Muse and makes a real model call, using a little Muse quota. It runs in an empty folder with `--no-session-log`. With Muse installed and a conversation available, it fetches the first snapshot; later probes require a snapshot over 30 minutes old and a newer Muse conversation. After failure, it waits at least 30 minutes and requires new Muse usage before retrying. Hide its card under **Hide sections**.
|
|
254
253
|
- **Other background network activity:** Public Claude and Codex status pages to flag outages, a public model-pricing table to estimate cost (built-in prices are used offline), and an occasional GitHub check for a new version.
|
|
255
254
|
- **Beginner Mode**, only if you turn it on, sends Claude Code's latest answer to Claude Haiku through your own Claude Code sign-in to pick terms. Your term list stays in `~/.usage/glossary.json`.
|
|
@@ -16,6 +16,7 @@ import logging
|
|
|
16
16
|
import os
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
import tempfile
|
|
19
|
+
import threading
|
|
19
20
|
import time
|
|
20
21
|
from typing import Any, NotRequired, TypedDict, cast
|
|
21
22
|
|
|
@@ -45,6 +46,7 @@ YEAR_LEDGER_PATH = YEAR_CACHE_PATH.with_name("year_ledger.json")
|
|
|
45
46
|
YEAR_CACHE_TTL_SECONDS = 6 * 3600
|
|
46
47
|
_YEAR_CACHE_SCHEMA = 6
|
|
47
48
|
_YEAR_LEDGER_SCHEMA = 1
|
|
49
|
+
_year_ledger_lock = threading.Lock()
|
|
48
50
|
_YEAR_LEDGER_TRIM_BUFFER_DAYS = 60
|
|
49
51
|
|
|
50
52
|
|
|
@@ -675,18 +677,21 @@ def _merge_year_ledger(
|
|
|
675
677
|
*,
|
|
676
678
|
trim_before: date,
|
|
677
679
|
) -> _YearLedger:
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
680
|
+
with _year_ledger_lock:
|
|
681
|
+
ledger = _read_year_ledger()
|
|
682
|
+
days = ledger["days"]
|
|
683
|
+
for day_key, current_day in current_days.items():
|
|
684
|
+
ledger_day = days.get(day_key)
|
|
685
|
+
if ledger_day is None or current_day["total_tokens"] >= ledger_day["total_tokens"]:
|
|
686
|
+
days[day_key] = current_day
|
|
687
|
+
|
|
688
|
+
ledger["days"] = {
|
|
689
|
+
day_key: day
|
|
690
|
+
for day_key, day in days.items()
|
|
691
|
+
if date.fromisoformat(day_key) >= trim_before
|
|
692
|
+
}
|
|
693
|
+
_write_year_ledger(ledger)
|
|
694
|
+
return ledger
|
|
690
695
|
|
|
691
696
|
|
|
692
697
|
def _build_year_output_from_ledger(
|
|
@@ -23,6 +23,7 @@ import logging
|
|
|
23
23
|
import os
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
import tempfile
|
|
26
|
+
import threading
|
|
26
27
|
from typing import NotRequired, TypeGuard, TypedDict
|
|
27
28
|
|
|
28
29
|
from adapters.types import UsageEntry
|
|
@@ -33,6 +34,7 @@ logger = logging.getLogger(__name__)
|
|
|
33
34
|
|
|
34
35
|
SNAPSHOT_PATH = Path(os.path.expanduser("~/.usage/usage_snapshot.json"))
|
|
35
36
|
SNAPSHOT_SCHEMA = 1
|
|
37
|
+
_snapshot_lock = threading.Lock()
|
|
36
38
|
|
|
37
39
|
_RowKey = tuple[str, str, str, str, str]
|
|
38
40
|
|
|
@@ -79,9 +81,10 @@ def record_entries(
|
|
|
79
81
|
) -> UsageSnapshot:
|
|
80
82
|
"""Merge this load into the on-disk snapshot and return the result."""
|
|
81
83
|
new_rows, new_headers = _aggregate_entries(entries, entry_dates)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
with _snapshot_lock:
|
|
85
|
+
merged = _merge_snapshot(_read_snapshot(), new_rows, new_headers)
|
|
86
|
+
_write_snapshot(merged)
|
|
87
|
+
return merged
|
|
85
88
|
|
|
86
89
|
|
|
87
90
|
def read_snapshot() -> UsageSnapshot:
|
|
@@ -176,11 +176,6 @@ def flush_caches_on_terminate() -> None:
|
|
|
176
176
|
_flush_caches_on_terminate(lambda: _flush_caches_to_disk(force=True))
|
|
177
177
|
|
|
178
178
|
|
|
179
|
-
def recent_input_output_tokens(hours_back: int) -> int:
|
|
180
|
-
"""Return recent input plus output tokens, excluding cache-read and thinking."""
|
|
181
|
-
return sum(entry.input_tokens + entry.output_tokens for entry in load_entries(hours_back))
|
|
182
|
-
|
|
183
|
-
|
|
184
179
|
def _load_database(
|
|
185
180
|
path: Path,
|
|
186
181
|
cutoff: datetime | None,
|
|
@@ -27,7 +27,7 @@ import urllib.parse
|
|
|
27
27
|
from base64 import b64decode
|
|
28
28
|
from binascii import Error as Base64Error
|
|
29
29
|
from contextlib import suppress
|
|
30
|
-
from dataclasses import dataclass
|
|
30
|
+
from dataclasses import dataclass, replace
|
|
31
31
|
from datetime import UTC, datetime, timedelta
|
|
32
32
|
from email.utils import parsedate_to_datetime
|
|
33
33
|
from pathlib import Path
|
|
@@ -237,6 +237,24 @@ def _carry_missing_groups(probed: AgyQuotaResult, cached: AgyQuotaResult | None)
|
|
|
237
237
|
]
|
|
238
238
|
if not missing:
|
|
239
239
|
return probed
|
|
240
|
+
cached_at = _parse_timestamp(cached.fetched_at)
|
|
241
|
+
probed_at = _parse_timestamp(probed.fetched_at)
|
|
242
|
+
elapsed = (
|
|
243
|
+
max(0, int((probed_at - cached_at).total_seconds() // 60))
|
|
244
|
+
if cached_at is not None and probed_at is not None
|
|
245
|
+
else 0
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def age_window(window: AgyQuotaWindow | None) -> AgyQuotaWindow | None:
|
|
249
|
+
if window is None or window.resets_in_minutes is None or elapsed == 0:
|
|
250
|
+
return window
|
|
251
|
+
minutes = max(0, window.resets_in_minutes - elapsed)
|
|
252
|
+
return replace(window, resets_in_minutes=minutes, resets_in=_format_resets_in(minutes))
|
|
253
|
+
|
|
254
|
+
missing = [
|
|
255
|
+
replace(group, weekly=age_window(group.weekly), five_hour=age_window(group.five_hour))
|
|
256
|
+
for group in missing
|
|
257
|
+
]
|
|
240
258
|
order = {group.name: index for index, group in enumerate(cached.groups)}
|
|
241
259
|
groups = sorted([*probed.groups, *missing], key=lambda group: order.get(group.name, 0))
|
|
242
260
|
return AgyQuotaResult(groups=groups, fetched_at=probed.fetched_at)
|
|
@@ -82,9 +82,10 @@ def project_quota(
|
|
|
82
82
|
preferred = next(
|
|
83
83
|
(group for group in quota.groups if _matches_group(group.name, group_preference)), None
|
|
84
84
|
)
|
|
85
|
+
fallback_groups = [group for group in quota.groups if not _matches_group(group.name, "argon")]
|
|
85
86
|
selected = preferred or next(
|
|
86
|
-
(group for group in quota.groups if "gemini"
|
|
87
|
-
min(quota.groups, key=_group_remaining_percent),
|
|
87
|
+
(group for group in quota.groups if _matches_group(group.name, "gemini")),
|
|
88
|
+
min(fallback_groups or quota.groups, key=_group_remaining_percent),
|
|
88
89
|
)
|
|
89
90
|
current_time = time.time() if now is None else now
|
|
90
91
|
age_minutes = _cache_age_minutes(quota.fetched_at, current_time)
|
|
@@ -199,6 +200,16 @@ def reproject_cached_quota(
|
|
|
199
200
|
)
|
|
200
201
|
|
|
201
202
|
|
|
203
|
+
def quota_group_keys() -> list[str]:
|
|
204
|
+
"""Available toggle targets from the last quota, shared by both panel hosts."""
|
|
205
|
+
quota = _last_quota
|
|
206
|
+
return [
|
|
207
|
+
key
|
|
208
|
+
for key in ("gemini", "claude_gpt", "argon")
|
|
209
|
+
if quota is not None and any(_matches_group(group.name, key) for group in quota.groups)
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
|
|
202
213
|
def _matches_group(name: str, preference: str) -> bool:
|
|
203
214
|
normalized = name.lower()
|
|
204
215
|
if preference == "claude_gpt":
|
|
@@ -91,6 +91,8 @@ def _row_payload(row: QuotaRowState) -> dict[str, object]:
|
|
|
91
91
|
def _state_payload(
|
|
92
92
|
state: PopoverState, *, system_accent_color: str | None = None
|
|
93
93
|
) -> dict[str, object]:
|
|
94
|
+
from menubar.agy import quota_group_keys
|
|
95
|
+
|
|
94
96
|
codex_rows = {
|
|
95
97
|
key: _row_payload(row)
|
|
96
98
|
for key, row in (("session", state.codex_session), ("weekly", state.codex_weekly))
|
|
@@ -131,6 +133,7 @@ def _state_payload(
|
|
|
131
133
|
"weekly": _row_payload(state.agy_weekly),
|
|
132
134
|
"groupName": state.agy_group_name,
|
|
133
135
|
"groupSwitchable": state.agy_group_switchable,
|
|
136
|
+
"availableGroups": quota_group_keys(),
|
|
134
137
|
"weeklyOnly": state.agy_weekly_only,
|
|
135
138
|
"stale": state.agy_stale,
|
|
136
139
|
},
|
|
@@ -205,6 +205,7 @@ class UsageScriptBridge(NSObject):
|
|
|
205
205
|
state.agy_weekly = projection.weekly
|
|
206
206
|
state.agy_group_name = projection.group_name
|
|
207
207
|
state.agy_group_switchable = projection.group_switchable
|
|
208
|
+
state.agy_weekly_only = projection.weekly_only
|
|
208
209
|
state.agy_stale = projection.stale
|
|
209
210
|
self.delegate.popover_controller.setState_(state)
|
|
210
211
|
return
|
|
@@ -274,27 +274,6 @@ def test_load_entries_prefers_legacy_generation_timestamp(sessions_dir: Path) ->
|
|
|
274
274
|
assert entries[0].timestamp == generation_timestamp
|
|
275
275
|
|
|
276
276
|
|
|
277
|
-
def test_recent_input_output_tokens_excludes_cache_and_thinking(sessions_dir: Path) -> None:
|
|
278
|
-
now = datetime.now(UTC)
|
|
279
|
-
_write_database(
|
|
280
|
-
sessions_dir / "totals.db",
|
|
281
|
-
[
|
|
282
|
-
_generation_blob(
|
|
283
|
-
timestamp=now,
|
|
284
|
-
dedup_key="total",
|
|
285
|
-
system_tokens=1_000,
|
|
286
|
-
input_tokens=20,
|
|
287
|
-
output_tokens=30,
|
|
288
|
-
cache_read_tokens=99_999,
|
|
289
|
-
thinking_tokens=500,
|
|
290
|
-
)
|
|
291
|
-
],
|
|
292
|
-
now,
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
assert agy_loader.recent_input_output_tokens(1) == 1_050
|
|
296
|
-
|
|
297
|
-
|
|
298
277
|
def test_load_entries_skips_database_when_sqlite_is_locked(
|
|
299
278
|
monkeypatch: pytest.MonkeyPatch, sessions_dir: Path
|
|
300
279
|
) -> None:
|
|
@@ -1074,3 +1074,36 @@ def test_assemble_groups_argon_missing_windows() -> None:
|
|
|
1074
1074
|
assert groups[0].name == "GEMINI 4 ARGON"
|
|
1075
1075
|
assert groups[0].weekly == AgyQuotaWindow(100.0, None, None)
|
|
1076
1076
|
assert groups[0].five_hour is None
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
@pytest.mark.parametrize("minutes, expected", [(5, 0), (25, 15)])
|
|
1080
|
+
def test_carry_ages_both_reset_countdowns(minutes: int, expected: int) -> None:
|
|
1081
|
+
fetched = datetime(2026, 1, 1, 11, 50, tzinfo=UTC)
|
|
1082
|
+
window = AgyQuotaWindow(20, f"{minutes}m", minutes)
|
|
1083
|
+
cached = AgyQuotaResult(
|
|
1084
|
+
[AgyQuotaGroup("GEMINI MODELS", [], window, window)], fetched.isoformat()
|
|
1085
|
+
)
|
|
1086
|
+
agy_quota_probe._carry_missing_groups(cached, None)
|
|
1087
|
+
merged = agy_quota_probe._carry_missing_groups(
|
|
1088
|
+
_only_claude(fetched + timedelta(minutes=10)), cached
|
|
1089
|
+
)
|
|
1090
|
+
group = next(group for group in merged.groups if group.name == "GEMINI MODELS")
|
|
1091
|
+
for carried in (group.weekly, group.five_hour):
|
|
1092
|
+
assert carried is not None
|
|
1093
|
+
assert carried.resets_in_minutes == expected
|
|
1094
|
+
assert carried.resets_in == f"{expected}m"
|
|
1095
|
+
assert cached.groups[0].weekly == window
|
|
1096
|
+
if expected:
|
|
1097
|
+
# A future reset keeps the same absolute time after rebasing.
|
|
1098
|
+
assert datetime.fromisoformat(merged.fetched_at) + timedelta(minutes=expected) == (
|
|
1099
|
+
fetched + timedelta(minutes=minutes)
|
|
1100
|
+
)
|
|
1101
|
+
else:
|
|
1102
|
+
from menubar.agy import project_quota
|
|
1103
|
+
|
|
1104
|
+
projection = project_quota(
|
|
1105
|
+
merged, "en", now=datetime.fromisoformat(merged.fetched_at).timestamp()
|
|
1106
|
+
)
|
|
1107
|
+
assert projection is not None
|
|
1108
|
+
assert projection.weekly.reset_done
|
|
1109
|
+
assert projection.session.reset_done
|
|
@@ -170,3 +170,76 @@ def test_timer_is_daemon_and_fires_scheduler(monkeypatch: pytest.MonkeyPatch) ->
|
|
|
170
170
|
timers[0].callback()
|
|
171
171
|
assert calls == ["started", "refresh"]
|
|
172
172
|
assert history_persist._timer is None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.mark.parametrize("writer", ["snapshot", "ledger"])
|
|
176
|
+
def test_concurrent_history_writes_preserve_both_entries(
|
|
177
|
+
monkeypatch: pytest.MonkeyPatch, writer: str
|
|
178
|
+
) -> None:
|
|
179
|
+
read_entered = threading.Event()
|
|
180
|
+
release_first = threading.Event()
|
|
181
|
+
second_started = threading.Event()
|
|
182
|
+
second_done = threading.Event()
|
|
183
|
+
errors: list[BaseException] = []
|
|
184
|
+
if writer == "snapshot":
|
|
185
|
+
module: Any = usage_snapshot
|
|
186
|
+
reader_name = "_read_snapshot"
|
|
187
|
+
|
|
188
|
+
def write(entry: UsageEntry) -> None:
|
|
189
|
+
usage_snapshot.record_entries([entry])
|
|
190
|
+
|
|
191
|
+
path = usage_snapshot.SNAPSHOT_PATH
|
|
192
|
+
else:
|
|
193
|
+
module = reporter
|
|
194
|
+
reader_name = "_read_year_ledger"
|
|
195
|
+
|
|
196
|
+
def write(entry: UsageEntry) -> None:
|
|
197
|
+
reporter.persist_year_entries([entry], NOW.date())
|
|
198
|
+
|
|
199
|
+
path = reporter.YEAR_LEDGER_PATH
|
|
200
|
+
original_read = getattr(module, reader_name)
|
|
201
|
+
|
|
202
|
+
def paused_read() -> Any:
|
|
203
|
+
result = original_read()
|
|
204
|
+
if threading.current_thread().name == "first-writer":
|
|
205
|
+
read_entered.set()
|
|
206
|
+
assert release_first.wait(5)
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
def run(entry: UsageEntry, second: bool = False) -> None:
|
|
210
|
+
try:
|
|
211
|
+
if second:
|
|
212
|
+
second_started.set()
|
|
213
|
+
write(entry)
|
|
214
|
+
except BaseException as exc:
|
|
215
|
+
errors.append(exc)
|
|
216
|
+
finally:
|
|
217
|
+
if second:
|
|
218
|
+
second_done.set()
|
|
219
|
+
|
|
220
|
+
monkeypatch.setattr(module, reader_name, paused_read)
|
|
221
|
+
first = threading.Thread(target=run, args=(_entry(session="first"),), name="first-writer")
|
|
222
|
+
second = threading.Thread(target=run, args=(_entry(NOW - timedelta(days=1), "second"), True))
|
|
223
|
+
first.start()
|
|
224
|
+
try:
|
|
225
|
+
assert read_entered.wait(5)
|
|
226
|
+
second.start()
|
|
227
|
+
assert second_started.wait(5)
|
|
228
|
+
# Unlocked code lets the second writer finish before the first replaces its file.
|
|
229
|
+
# Locked code holds the second writer until the first transaction completes.
|
|
230
|
+
second_done.wait(0.2)
|
|
231
|
+
finally:
|
|
232
|
+
release_first.set()
|
|
233
|
+
first.join(5)
|
|
234
|
+
if second.ident is not None:
|
|
235
|
+
second.join(5)
|
|
236
|
+
assert not first.is_alive() and not second.is_alive()
|
|
237
|
+
assert not errors
|
|
238
|
+
saved = json.loads(path.read_text())
|
|
239
|
+
if writer == "snapshot":
|
|
240
|
+
assert {row["session_id"] for row in saved["rows"]} == {"first", "second"}
|
|
241
|
+
else:
|
|
242
|
+
assert set(saved["days"]) == {
|
|
243
|
+
NOW.date().isoformat(),
|
|
244
|
+
(NOW - timedelta(days=1)).date().isoformat(),
|
|
245
|
+
}
|
|
@@ -626,3 +626,16 @@ def test_project_quota_missing_argon_falls_back_to_gemini() -> None:
|
|
|
626
626
|
assert projection.group_name == "GEMINI MODELS"
|
|
627
627
|
assert projection.weekly_only is False
|
|
628
628
|
assert projection.session.percent_text == "60% used"
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def test_missing_gemini_fallback_prefers_claude_over_argon() -> None:
|
|
632
|
+
quota = AgyQuotaResult(
|
|
633
|
+
groups=[
|
|
634
|
+
_group("GEMINI 4 ARGON", session_remaining=0, weekly_remaining=0),
|
|
635
|
+
_group("CLAUDE AND GPT MODELS", session_remaining=70, weekly_remaining=70),
|
|
636
|
+
],
|
|
637
|
+
fetched_at="2026-01-01T00:00:00+00:00",
|
|
638
|
+
)
|
|
639
|
+
projection = menubar_agy.project_quota(quota, "en", now=1_767_225_600.0)
|
|
640
|
+
assert projection is not None
|
|
641
|
+
assert projection.group_name == "CLAUDE AND GPT MODELS"
|
|
@@ -72,7 +72,7 @@ for (const agy of [null, {}, { groupSwitchable: false },
|
|
|
72
72
|
{ groupSwitchable: true }, { groupSwitchable: false },
|
|
73
73
|
{ groupSwitchable: true, groupName: 'CLAUDE AND GPT MODELS' },
|
|
74
74
|
{ groupSwitchable: true, groupName: 'Gemini 4 Argon' }]) {
|
|
75
|
-
context.agy = agy;
|
|
75
|
+
context.agy = agy && { ...agy, availableGroups: ['gemini', 'claude_gpt', 'argon'] };
|
|
76
76
|
vm.runInContext('renderAgy(agy)', context);
|
|
77
77
|
assert.equal(button.hidden, !(agy && agy.groupSwitchable));
|
|
78
78
|
if (agy && agy.groupSwitchable) {
|
|
@@ -87,12 +87,21 @@ const names = { gemini: 'GEMINI MODELS', claude_gpt: 'CLAUDE AND GPT MODELS',
|
|
|
87
87
|
argon: 'GEMINI 4 ARGON' };
|
|
88
88
|
let current = 'gemini';
|
|
89
89
|
for (const next of ['claude_gpt', 'argon', 'gemini']) {
|
|
90
|
-
context.agy = { groupSwitchable: true, groupName: names[current]
|
|
90
|
+
context.agy = { groupSwitchable: true, groupName: names[current],
|
|
91
|
+
availableGroups: ['gemini', 'claude_gpt', 'argon'] };
|
|
91
92
|
vm.runInContext('renderAgy(agy)', context);
|
|
92
93
|
assert.equal(button.textContent, 'agy_group_' + current);
|
|
93
94
|
assert.equal(button.dataset.group, next);
|
|
94
95
|
current = button.dataset.group;
|
|
95
96
|
}
|
|
97
|
+
current = 'claude_gpt';
|
|
98
|
+
for (const next of ['argon', 'claude_gpt', 'argon', 'claude_gpt']) {
|
|
99
|
+
context.agy = { groupSwitchable: true, groupName: names[current],
|
|
100
|
+
availableGroups: ['claude_gpt', 'argon'] };
|
|
101
|
+
vm.runInContext('renderAgy(agy)', context);
|
|
102
|
+
assert.equal(button.dataset.group, next);
|
|
103
|
+
current = button.dataset.group;
|
|
104
|
+
}
|
|
96
105
|
""",
|
|
97
106
|
script,
|
|
98
107
|
],
|