usage-cli 0.32.2__tar.gz → 0.32.3__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.32.2 → usage_cli-0.32.3}/PKG-INFO +1 -1
- {usage_cli-0.32.2 → usage_cli-0.32.3}/i18n.json +15 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/agy.py +16 -7
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/app.py +5 -5
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/grok.py +8 -17
- usage_cli-0.32.3/menubar/manual_refresh.py +51 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/state.py +18 -3
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/title.py +19 -13
- {usage_cli-0.32.2 → usage_cli-0.32.3}/panels/payload.py +1 -1
- {usage_cli-0.32.2 → usage_cli-0.32.3}/pyproject.toml +1 -1
- usage_cli-0.32.3/tests/test_manual_refresh.py +62 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_menubar.py +38 -4
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_menubar_agy.py +19 -2
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_menubar_grok.py +14 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_menubar_state.py +50 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_web_panel_payload.py +10 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_wintray.py +67 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_cli.egg-info/PKG-INFO +1 -1
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_cli.egg-info/SOURCES.txt +2 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/wintray/app.py +36 -7
- {usage_cli-0.32.2 → usage_cli-0.32.3}/LICENSE +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/README.md +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/adapters/__init__.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/adapters/agy.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/adapters/claude.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/adapters/codex.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/adapters/grok.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/adapters/muse.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/adapters/rate_limits.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/adapters/registry.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/adapters/types.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/analyzer/__init__.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/analyzer/aggregator.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/analyzer/blocks.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/analyzer/diagnoser.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/analyzer/dispatch_ledger.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/analyzer/insights.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/analyzer/persona_loader.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/analyzer/reporter.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/analyzer/subscription.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/analyzer/usage_snapshot.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/doctor.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/fsevents_watch.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/i18n.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/installer/__init__.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/installer/claude_pane.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/installer/login_item.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/installer/session_hooks.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/installer/setup_hook.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/installer/statusline_settings.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/__init__.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/agy_disk_cache.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/agy_loader.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/agy_quota_probe.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/cache_quarantine.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/claude_paths.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/codex_disk_cache.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/codex_events.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/codex_fork_replay.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/codex_loader.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/codex_paths.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/disk_cache_common.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/disk_cache_lifecycle.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/grok_loader.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/grok_quota_probe.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/history_disk_cache.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/history_loader.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/jsonl_limits.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/jsonl_utils.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/loaders/muse_loader.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/main.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/__init__.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/actions.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/chrome.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/menu.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/new_badge.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/notify.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/popover.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/prefs.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/refresh.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/menubar/update.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/panels/__init__.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/panels/base.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/panels/dynamic_height.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/panels/panel_scale.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/panels/panel_window.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/panels/panel_window_state.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/panels/web_panel.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/panels/window_drag.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/prefs.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/pricing.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/project_resolver.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/quota/__init__.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/quota/agy_window_keeper.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/quota/burn_rate.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/quota/codex_window_keeper.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/quota/usage_rate.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/quota/window_keeper.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/service_status.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/setup.cfg +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/setup_app.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_adapters_claude.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_adapters_codex.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_adapters_rate_limits.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_aggregator.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_agy_adapter.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_agy_disk_cache.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_agy_loader.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_agy_quota_probe.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_agy_window_keeper.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_analyzer_blocks.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_analyzer_pipeline.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_burn_rate.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_cache_quarantine.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_claude_pane.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_claude_paths.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_codex_consistency.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_codex_loader.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_codex_window_keeper.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_diagnoser.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_disk_cache_shards.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_dispatch_ledger.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_doctor.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_dynamic_height.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_fsevents_watch.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_grok_loader.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_grok_quota_probe.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_history_loader.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_hook_scripts_are_standalone.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_html_report_snapshot.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_i18n_format_fallback.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_i18n_key_parity.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_i18n_packaged_path.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_insights.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_jsonl_utils.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_login_item.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_main.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_menubar_notify.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_menubar_prefs.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_muse_loader.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_new_badge.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_packaged_resources.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_packaging.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_panel_footer_layout.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_panel_scale.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_panel_window_state.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_panels.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_panels_lazy_import.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_persona_loader.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_pricing.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_project_resolver.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_report_daily_chart.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_report_filter_js.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_reporter_build_data.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_service_status.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_session_resume.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_setup_app.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_setup_hook.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_setup_hook_coexistence.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_setup_hook_resume.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_setup_hook_terse.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_subprocess_utils.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_subscription.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_time_utils.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_tui.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_tui_sprite.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_ui_tables.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_update_checker.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_update_gate.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_update_release_notes.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_cli.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_client.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_dir_sweeper.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_lang.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_logging.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_notifications.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_rate.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_snapshot.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_snapshot_replay.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_statusline.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_statusline_agy.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_statusline_forwarder.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_statusline_grok.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_terse_mode.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_usage_terse_reminder.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_wheel_packaging.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_win_login_item.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_window_keeper.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tests/test_windows_manifest.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tui/__init__.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tui/app.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/tui/sprite.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/ui/__init__.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/ui/html_report.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/ui/report_charts.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/ui/report_daily_chart.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/ui/report_filter.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/ui/report_ledger.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/ui/report_scripts.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/ui/report_styles.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/ui/tables.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/updates/__init__.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/updates/checker.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/updates/gate.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/updates/release_notes.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_cli.egg-info/dependency_links.txt +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_cli.egg-info/entry_points.txt +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_cli.egg-info/requires.txt +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_cli.egg-info/top_level.txt +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_cli.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_client.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_common/__init__.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_common/subprocess_utils.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_common/time_utils.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_common/usage_dir_sweeper.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_common/usage_lang.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_common/usage_logging.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_notifications.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_session_resume.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_statusline.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_statusline_agy.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_statusline_forwarder.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_statusline_grok.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_terse_mode.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/usage_terse_reminder.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/wintray/__init__.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/wintray/login_item.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/wintray/menu.py +0 -0
- {usage_cli-0.32.2 → usage_cli-0.32.3}/wintray/watch.py +0 -0
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"data_stale_hint": "資料來自備援來源,建議檢查設定",
|
|
57
57
|
"status_loading": "載入中",
|
|
58
58
|
"status_synced": "✓ 已同步",
|
|
59
|
+
"status_refreshing": "↻ 更新中…",
|
|
59
60
|
"status_no_data": "無資料",
|
|
60
61
|
"awaiting_rate_limits": "請對 Claude Code 發送一句訊息以同步配額",
|
|
61
62
|
"hook_broken_not_installed": "⚠ 狀態列沒在更新,請按「設定狀態列」重新安裝",
|
|
@@ -75,6 +76,7 @@
|
|
|
75
76
|
"percent_used": "{value}% 已用",
|
|
76
77
|
"reset_in": "重置 {time}",
|
|
77
78
|
"reset_imminent": "即將重置",
|
|
79
|
+
"reset_done": "已重置",
|
|
78
80
|
"burn_warning": "⚠ 照目前速度 {empty}後用完 · 重置 {reset}",
|
|
79
81
|
"burn_warning_compact": "⚠ {empty}後用完",
|
|
80
82
|
"reset_placeholder": "重置 --",
|
|
@@ -490,6 +492,7 @@
|
|
|
490
492
|
"claude_pane_invalid_sidecar": "usage-pane.json 格式錯誤",
|
|
491
493
|
"claude_pane_elapsed": "{minutes}分{seconds}秒",
|
|
492
494
|
"claude_pane_remaining": "剩",
|
|
495
|
+
"claude_pane_reset_done": "已重置",
|
|
493
496
|
"claude_pane_hours_ago": "{count}時前"
|
|
494
497
|
},
|
|
495
498
|
"en": {
|
|
@@ -549,6 +552,7 @@
|
|
|
549
552
|
"data_stale_hint": "Data is from a fallback source. Check settings.",
|
|
550
553
|
"status_loading": "Loading",
|
|
551
554
|
"status_synced": "✓ Synced",
|
|
555
|
+
"status_refreshing": "↻ Refreshing…",
|
|
552
556
|
"status_no_data": "No data",
|
|
553
557
|
"awaiting_rate_limits": "Send a message in Claude Code to sync your quota",
|
|
554
558
|
"hook_broken_not_installed": "⚠ Status line is not updating. Click \"Set Up Status Line\" to reinstall it.",
|
|
@@ -568,6 +572,7 @@
|
|
|
568
572
|
"percent_used": "{value}% used",
|
|
569
573
|
"reset_in": "Resets in {time}",
|
|
570
574
|
"reset_imminent": "Reset imminent",
|
|
575
|
+
"reset_done": "Reset",
|
|
571
576
|
"burn_warning": "⚠ At this pace, empty in {empty} · resets in {reset}",
|
|
572
577
|
"burn_warning_compact": "⚠ Empty in {empty}",
|
|
573
578
|
"reset_placeholder": "Resets in --",
|
|
@@ -983,6 +988,7 @@
|
|
|
983
988
|
"claude_pane_invalid_sidecar": "Invalid usage-pane.json",
|
|
984
989
|
"claude_pane_elapsed": "{minutes}m{seconds}s",
|
|
985
990
|
"claude_pane_remaining": "left",
|
|
991
|
+
"claude_pane_reset_done": "Reset",
|
|
986
992
|
"claude_pane_hours_ago": "{count}h ago"
|
|
987
993
|
},
|
|
988
994
|
"zh-CN": {
|
|
@@ -1042,6 +1048,7 @@
|
|
|
1042
1048
|
"data_stale_hint": "数据来自备用来源,建议检查设置",
|
|
1043
1049
|
"status_loading": "加载中",
|
|
1044
1050
|
"status_synced": "✓ 已同步",
|
|
1051
|
+
"status_refreshing": "↻ 更新中…",
|
|
1045
1052
|
"status_no_data": "无数据",
|
|
1046
1053
|
"awaiting_rate_limits": "请对 Claude Code 发送一句消息以同步配额",
|
|
1047
1054
|
"hook_broken_not_installed": "⚠ 状态栏没有更新,请点「设置状态栏」重新安装",
|
|
@@ -1061,6 +1068,7 @@
|
|
|
1061
1068
|
"percent_used": "{value}% 已用",
|
|
1062
1069
|
"reset_in": "重置 {time}",
|
|
1063
1070
|
"reset_imminent": "即将重置",
|
|
1071
|
+
"reset_done": "已重置",
|
|
1064
1072
|
"burn_warning": "⚠ 按当前速度 {empty}后用完 · 重置 {reset}",
|
|
1065
1073
|
"burn_warning_compact": "⚠ {empty}后用完",
|
|
1066
1074
|
"reset_placeholder": "重置 --",
|
|
@@ -1476,6 +1484,7 @@
|
|
|
1476
1484
|
"claude_pane_invalid_sidecar": "usage-pane.json 格式错误",
|
|
1477
1485
|
"claude_pane_elapsed": "{minutes}分{seconds}秒",
|
|
1478
1486
|
"claude_pane_remaining": "剩",
|
|
1487
|
+
"claude_pane_reset_done": "已重置",
|
|
1479
1488
|
"claude_pane_hours_ago": "{count}时前"
|
|
1480
1489
|
},
|
|
1481
1490
|
"ja": {
|
|
@@ -1535,6 +1544,7 @@
|
|
|
1535
1544
|
"data_stale_hint": "データはフォールバック元から取得しています。設定を確認してください。",
|
|
1536
1545
|
"status_loading": "読み込み中",
|
|
1537
1546
|
"status_synced": "✓ 同期済み",
|
|
1547
|
+
"status_refreshing": "↻ 更新中…",
|
|
1538
1548
|
"status_no_data": "データなし",
|
|
1539
1549
|
"awaiting_rate_limits": "Claude Code にメッセージを送信してクォータを同期してください",
|
|
1540
1550
|
"hook_broken_not_installed": "⚠ ステータスラインが更新されていません。「ステータスラインを設定」を押して再インストールしてください。",
|
|
@@ -1554,6 +1564,7 @@
|
|
|
1554
1564
|
"percent_used": "{value}% 使用済み",
|
|
1555
1565
|
"reset_in": "{time}後にリセット",
|
|
1556
1566
|
"reset_imminent": "まもなくリセット",
|
|
1567
|
+
"reset_done": "リセット済み",
|
|
1557
1568
|
"burn_warning": "⚠ このペースで{empty}で使い切り·リセット{reset}",
|
|
1558
1569
|
"burn_warning_compact": "⚠ {empty}で使い切り",
|
|
1559
1570
|
"reset_placeholder": "リセット --",
|
|
@@ -1969,6 +1980,7 @@
|
|
|
1969
1980
|
"claude_pane_invalid_sidecar": "usage-pane.json の形式が不正です",
|
|
1970
1981
|
"claude_pane_elapsed": "{minutes}分{seconds}秒",
|
|
1971
1982
|
"claude_pane_remaining": "残り",
|
|
1983
|
+
"claude_pane_reset_done": "リセット済み",
|
|
1972
1984
|
"claude_pane_hours_ago": "{count}時間前"
|
|
1973
1985
|
},
|
|
1974
1986
|
"ko": {
|
|
@@ -2028,6 +2040,7 @@
|
|
|
2028
2040
|
"data_stale_hint": "데이터가 대체 소스에서 왔습니다. 설정을 확인하세요.",
|
|
2029
2041
|
"status_loading": "불러오는 중",
|
|
2030
2042
|
"status_synced": "✓ 동기화됨",
|
|
2043
|
+
"status_refreshing": "↻ 새로 고치는 중…",
|
|
2031
2044
|
"status_no_data": "데이터 없음",
|
|
2032
2045
|
"awaiting_rate_limits": "Claude Code에 메시지를 보내 할당량을 동기화하세요",
|
|
2033
2046
|
"hook_broken_not_installed": "⚠ 상태 표시줄이 업데이트되지 않습니다. \"상태 표시줄 설정\"을 눌러 다시 설치하세요.",
|
|
@@ -2047,6 +2060,7 @@
|
|
|
2047
2060
|
"percent_used": "{value}% 사용됨",
|
|
2048
2061
|
"reset_in": "{time} 후 초기화",
|
|
2049
2062
|
"reset_imminent": "곧 초기화",
|
|
2063
|
+
"reset_done": "리셋됨",
|
|
2050
2064
|
"burn_warning": "⚠ 현재 속도면 {empty} 후 소진 · 초기화까지 {reset}",
|
|
2051
2065
|
"burn_warning_compact": "⚠ {empty} 후 소진",
|
|
2052
2066
|
"reset_placeholder": "초기화까지 --",
|
|
@@ -2462,6 +2476,7 @@
|
|
|
2462
2476
|
"claude_pane_invalid_sidecar": "usage-pane.json 형식이 잘못되었습니다",
|
|
2463
2477
|
"claude_pane_elapsed": "{minutes}분{seconds}초",
|
|
2464
2478
|
"claude_pane_remaining": "남음",
|
|
2479
|
+
"claude_pane_reset_done": "리셋됨",
|
|
2465
2480
|
"claude_pane_hours_ago": "{count}시간 전"
|
|
2466
2481
|
}
|
|
2467
2482
|
}
|
|
@@ -213,20 +213,20 @@ def _remaining_percent(window: AgyQuotaWindow) -> float:
|
|
|
213
213
|
return max(0.0, min(100.0, float(window.remaining_percent)))
|
|
214
214
|
|
|
215
215
|
|
|
216
|
-
def _cache_age_minutes(fetched_at: str, now: float) ->
|
|
217
|
-
"""
|
|
216
|
+
def _cache_age_minutes(fetched_at: str, now: float) -> float:
|
|
217
|
+
"""Minutes since the cached snapshot was taken (never negative)."""
|
|
218
218
|
try:
|
|
219
219
|
age_seconds = now - parse_iso8601_utc_or_raise(fetched_at).timestamp()
|
|
220
220
|
except (TypeError, ValueError):
|
|
221
221
|
return 0
|
|
222
|
-
return max(0,
|
|
222
|
+
return max(0.0, age_seconds / 60)
|
|
223
223
|
|
|
224
224
|
|
|
225
225
|
def _window_row(
|
|
226
226
|
title: str,
|
|
227
227
|
window: AgyQuotaWindow,
|
|
228
228
|
language: str,
|
|
229
|
-
age_minutes:
|
|
229
|
+
age_minutes: float = 0,
|
|
230
230
|
forecast_seconds: float | None = None,
|
|
231
231
|
warning_max_seconds: float | None = None,
|
|
232
232
|
*,
|
|
@@ -236,12 +236,20 @@ def _window_row(
|
|
|
236
236
|
used = 100.0 - remaining
|
|
237
237
|
warning = False
|
|
238
238
|
warning_seconds: float | None = None
|
|
239
|
-
|
|
239
|
+
reset_done = window.resets_in_minutes is not None and window.resets_in_minutes <= max(
|
|
240
|
+
0, age_minutes
|
|
241
|
+
)
|
|
242
|
+
if reset_done:
|
|
243
|
+
used = 0.0
|
|
244
|
+
reset_text = _t(language, "reset_done")
|
|
245
|
+
elif remaining == 100.0:
|
|
240
246
|
reset_text = _t(language, "agy_quota_full")
|
|
241
247
|
elif window.resets_in_minutes is None:
|
|
242
248
|
reset_text = _t(language, "reset_placeholder")
|
|
249
|
+
elif window.resets_in_minutes - max(0, age_minutes) < 1:
|
|
250
|
+
reset_text = _t(language, "reset_imminent")
|
|
243
251
|
else:
|
|
244
|
-
minutes_left =
|
|
252
|
+
minutes_left = window.resets_in_minutes - int(max(0, age_minutes))
|
|
245
253
|
time_to_reset = minutes_left * 60
|
|
246
254
|
if window_seconds is None:
|
|
247
255
|
if (
|
|
@@ -275,12 +283,13 @@ def _window_row(
|
|
|
275
283
|
)
|
|
276
284
|
return QuotaRowState(
|
|
277
285
|
title=title,
|
|
278
|
-
percent=
|
|
286
|
+
percent=100.0 - remaining,
|
|
279
287
|
percent_text=_t(language, "percent_used", value=_format_percent(used)),
|
|
280
288
|
reset_text=reset_text,
|
|
281
289
|
color=_bar_color(used, AGY_COLOR),
|
|
282
290
|
warning=warning,
|
|
283
291
|
available=True,
|
|
292
|
+
reset_done=reset_done,
|
|
284
293
|
)
|
|
285
294
|
|
|
286
295
|
|
|
@@ -70,6 +70,7 @@ from loaders.history_loader import (
|
|
|
70
70
|
flush_caches_on_terminate as flush_history_cache,
|
|
71
71
|
)
|
|
72
72
|
from menubar import actions as menubar_actions
|
|
73
|
+
from menubar import manual_refresh
|
|
73
74
|
from menubar import menu as menubar_menu
|
|
74
75
|
from menubar import notify as menubar_notify
|
|
75
76
|
from menubar import refresh as menubar_refresh
|
|
@@ -419,7 +420,7 @@ class AppDelegate(NSObject):
|
|
|
419
420
|
self._panel_window_did_hide()
|
|
420
421
|
|
|
421
422
|
def refreshNow_(self, sender: Any) -> None:
|
|
422
|
-
|
|
423
|
+
manual_refresh.start_mac(self)
|
|
423
424
|
|
|
424
425
|
def installHook_(self, sender: Any) -> None:
|
|
425
426
|
thread = threading.Thread(target=self._install_hook_in_background, daemon=True)
|
|
@@ -822,7 +823,7 @@ class AppDelegate(NSObject):
|
|
|
822
823
|
codex_model = result.get("codex_model", "unknown")
|
|
823
824
|
self.codex_5h_pct = codex_5h_pct
|
|
824
825
|
self.codex_model = codex_model
|
|
825
|
-
self.latest_state = state
|
|
826
|
+
self.latest_state = manual_refresh.finish_mac(self, state)
|
|
826
827
|
self._process_quota_notifications(state)
|
|
827
828
|
if self._panel_window_is_visible():
|
|
828
829
|
self.popover_controller.setState_(self.latest_state)
|
|
@@ -831,8 +832,7 @@ class AppDelegate(NSObject):
|
|
|
831
832
|
menubar_title._set_button_title(self, state)
|
|
832
833
|
finally:
|
|
833
834
|
should_refresh_again = bool(self._refresh_queued)
|
|
834
|
-
self._refresh_queued = False
|
|
835
|
-
self._refresh_in_flight = False
|
|
835
|
+
self._refresh_queued = self._refresh_in_flight = False
|
|
836
836
|
if should_refresh_again:
|
|
837
837
|
self._refresh()
|
|
838
838
|
if started_at:
|
|
@@ -842,7 +842,7 @@ class AppDelegate(NSObject):
|
|
|
842
842
|
)
|
|
843
843
|
|
|
844
844
|
def _clearRefreshInFlight_(self, _sender: Any) -> None:
|
|
845
|
-
self
|
|
845
|
+
manual_refresh.fail_mac(self)
|
|
846
846
|
|
|
847
847
|
def _load_codex_refresh_result(self) -> dict[str, Any]:
|
|
848
848
|
history_scan = None if self.mock else self._history_source_scan()
|
|
@@ -18,9 +18,7 @@ from menubar.state import (
|
|
|
18
18
|
GROK_COLOR,
|
|
19
19
|
GrokStaleState,
|
|
20
20
|
QuotaRowState,
|
|
21
|
-
|
|
22
|
-
_format_percent,
|
|
23
|
-
format_human_time,
|
|
21
|
+
_quota_row,
|
|
24
22
|
)
|
|
25
23
|
from usage_common.time_utils import parse_iso8601_utc_or_raise
|
|
26
24
|
|
|
@@ -56,22 +54,15 @@ def project_quota(
|
|
|
56
54
|
period_end = parse_iso8601_utc_or_raise(quota.period_end).timestamp()
|
|
57
55
|
except (TypeError, ValueError):
|
|
58
56
|
return None
|
|
59
|
-
if period_end < current_time:
|
|
60
|
-
return None
|
|
61
57
|
stale = _stale_state(quota.fetched_at, current_time, language)
|
|
62
|
-
used = max(0.0, min(100.0, quota.used_percent))
|
|
63
58
|
return GrokQuotaProjection(
|
|
64
|
-
weekly=
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
time=format_human_time(max(0.0, period_end - current_time), language),
|
|
72
|
-
),
|
|
73
|
-
color=_bar_color(used, GROK_COLOR),
|
|
74
|
-
available=True,
|
|
59
|
+
weekly=_quota_row(
|
|
60
|
+
_t(language, "weekly_label"),
|
|
61
|
+
quota.used_percent,
|
|
62
|
+
period_end,
|
|
63
|
+
current_time,
|
|
64
|
+
GROK_COLOR,
|
|
65
|
+
language,
|
|
75
66
|
),
|
|
76
67
|
stale=stale,
|
|
77
68
|
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Status-chip feedback for user-requested refreshes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from i18n import _t
|
|
8
|
+
from menubar.state import PopoverState
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def begin(state: PopoverState, *, queued: bool = False) -> None:
|
|
12
|
+
if state.refresh_status is None:
|
|
13
|
+
state.refresh_status = (state.status_text, state.status_long)
|
|
14
|
+
state.refresh_queued = state.refresh_queued or queued
|
|
15
|
+
state.status_text = _t(
|
|
16
|
+
state.language, "status_text", value=_t(state.language, "status_refreshing")
|
|
17
|
+
)
|
|
18
|
+
state.status_long = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def finish(
|
|
22
|
+
previous: PopoverState | None, state: PopoverState, *, queued: bool = False
|
|
23
|
+
) -> PopoverState:
|
|
24
|
+
if previous is not None and previous.refresh_status is not None:
|
|
25
|
+
keep = previous.refresh_queued and queued
|
|
26
|
+
previous.status_text, previous.status_long = previous.refresh_status
|
|
27
|
+
previous.refresh_status = None
|
|
28
|
+
previous.refresh_queued = False
|
|
29
|
+
if keep:
|
|
30
|
+
begin(state)
|
|
31
|
+
return state
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def finish_mac(app: Any, state: PopoverState) -> PopoverState:
|
|
35
|
+
return finish(app.latest_state, state, queued=bool(app._refresh_queued))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def start_mac(app: Any) -> None:
|
|
39
|
+
begin(app.latest_state, queued=bool(app._refresh_in_flight))
|
|
40
|
+
app._refresh(queue_if_busy=True)
|
|
41
|
+
if app._panel_window_is_visible():
|
|
42
|
+
app.popover_controller.setState_(app.latest_state)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def fail_mac(app: Any) -> None:
|
|
46
|
+
state = getattr(app, "latest_state", None)
|
|
47
|
+
if state is not None:
|
|
48
|
+
finish(state, state)
|
|
49
|
+
app._refresh_in_flight = False
|
|
50
|
+
if state is not None and app._panel_window_is_visible():
|
|
51
|
+
app.popover_controller.setState_(state)
|
|
@@ -99,6 +99,13 @@ class QuotaRowState:
|
|
|
99
99
|
warning: bool = False
|
|
100
100
|
available: bool = True
|
|
101
101
|
reset_text_compact: str = ""
|
|
102
|
+
reset_done: bool = False
|
|
103
|
+
|
|
104
|
+
# Keep percent unchanged for quota notifications; renderers use display_percent.
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def display_percent(self) -> float | None:
|
|
108
|
+
return 0.0 if self.reset_done else self.percent
|
|
102
109
|
|
|
103
110
|
|
|
104
111
|
class CodexStaleState(TypedDict):
|
|
@@ -156,6 +163,8 @@ class PopoverState:
|
|
|
156
163
|
grok_stale: GrokStaleState | None = None
|
|
157
164
|
card_order: tuple[str, ...] = ("claude", "codex", "agy", "grok")
|
|
158
165
|
history_error: HistoryLoadErrorState | None = None
|
|
166
|
+
refresh_status: tuple[str, bool] | None = None
|
|
167
|
+
refresh_queued: bool = False
|
|
159
168
|
|
|
160
169
|
|
|
161
170
|
@dataclass(frozen=True, slots=True)
|
|
@@ -970,7 +979,10 @@ def _quota_row(
|
|
|
970
979
|
pct = max(0.0, min(100.0, float(pct)))
|
|
971
980
|
time_to_reset = resets_at - now
|
|
972
981
|
warning_seconds: float | None = None
|
|
973
|
-
if time_to_reset
|
|
982
|
+
if time_to_reset <= 0:
|
|
983
|
+
reset_text = _t(language, "reset_done")
|
|
984
|
+
warning = False
|
|
985
|
+
elif time_to_reset < 60:
|
|
974
986
|
reset_text = _t(language, "reset_imminent")
|
|
975
987
|
warning = False
|
|
976
988
|
else:
|
|
@@ -1007,11 +1019,14 @@ def _quota_row(
|
|
|
1007
1019
|
row = QuotaRowState(
|
|
1008
1020
|
title=title,
|
|
1009
1021
|
percent=pct,
|
|
1010
|
-
percent_text=_t(
|
|
1022
|
+
percent_text=_t(
|
|
1023
|
+
language, "percent_used", value=_format_percent(0.0 if time_to_reset <= 0 else pct)
|
|
1024
|
+
),
|
|
1011
1025
|
reset_text=reset_text,
|
|
1012
|
-
color=_bar_color(pct, color),
|
|
1026
|
+
color=_bar_color(0.0 if time_to_reset <= 0 else pct, color),
|
|
1013
1027
|
warning=warning,
|
|
1014
1028
|
available=True,
|
|
1029
|
+
reset_done=time_to_reset <= 0,
|
|
1015
1030
|
)
|
|
1016
1031
|
if warning_seconds is not None:
|
|
1017
1032
|
row.reset_text_compact = _t(
|
|
@@ -66,20 +66,22 @@ def _menubar_attributed_title(app: _TitleApp, state: PopoverState) -> Any:
|
|
|
66
66
|
if not state.hide_claude:
|
|
67
67
|
claude_percent = (
|
|
68
68
|
"--"
|
|
69
|
-
if state.claude_session.
|
|
70
|
-
else f"{_format_percent(state.claude_session.
|
|
69
|
+
if state.claude_session.display_percent is None
|
|
70
|
+
else f"{_format_percent(state.claude_session.display_percent)}%"
|
|
71
71
|
)
|
|
72
72
|
title.appendAttributedString_(_menubar_icon_attachment_string(_claude_menubar_icon()))
|
|
73
73
|
title.appendAttributedString_(_menubar_text_string(app, f" {claude_percent}"))
|
|
74
74
|
if not state.hide_codex and (app.codex_5h_pct is not None or state.hide_claude):
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
codex_row = (
|
|
76
|
+
state.codex_session if state.codex_session.percent is not None else state.codex_weekly
|
|
77
77
|
)
|
|
78
|
+
codex_used = 0.0 if codex_row.reset_done else app.codex_5h_pct
|
|
79
|
+
codex_percent = "--" if codex_used is None else f"{_format_percent(float(codex_used))}%"
|
|
78
80
|
if not state.hide_claude:
|
|
79
81
|
title.appendAttributedString_(_menubar_text_string(app, " "))
|
|
80
82
|
title.appendAttributedString_(_menubar_icon_attachment_string(_codex_menubar_icon()))
|
|
81
83
|
title.appendAttributedString_(_menubar_text_string(app, f" {codex_percent}"))
|
|
82
|
-
agy_session_percent = state.agy_session.
|
|
84
|
+
agy_session_percent = state.agy_session.display_percent
|
|
83
85
|
agy_visible = not state.hide_agy and agy_session_percent is not None
|
|
84
86
|
if agy_visible:
|
|
85
87
|
assert agy_session_percent is not None
|
|
@@ -88,7 +90,7 @@ def _menubar_attributed_title(app: _TitleApp, state: PopoverState) -> Any:
|
|
|
88
90
|
title.appendAttributedString_(_menubar_text_string(app, " "))
|
|
89
91
|
title.appendAttributedString_(_menubar_icon_attachment_string(_agy_menubar_icon()))
|
|
90
92
|
title.appendAttributedString_(_menubar_text_string(app, f" {agy_percent}"))
|
|
91
|
-
grok_weekly_percent = state.grok_weekly.
|
|
93
|
+
grok_weekly_percent = state.grok_weekly.display_percent
|
|
92
94
|
grok_visible = not state.hide_grok and grok_weekly_percent is not None
|
|
93
95
|
if grok_visible:
|
|
94
96
|
assert grok_weekly_percent is not None
|
|
@@ -123,18 +125,22 @@ def _compose_title(app: _TitleApp, state: PopoverState) -> str:
|
|
|
123
125
|
if not state.hide_claude:
|
|
124
126
|
claude = (
|
|
125
127
|
"--"
|
|
126
|
-
if state.claude_session.
|
|
127
|
-
else f"{_format_percent(state.claude_session.
|
|
128
|
+
if state.claude_session.display_percent is None
|
|
129
|
+
else f"{_format_percent(state.claude_session.display_percent)}%"
|
|
128
130
|
)
|
|
129
131
|
parts.append(claude)
|
|
130
132
|
if not state.hide_codex and (app.codex_5h_pct is not None or state.hide_claude):
|
|
131
|
-
|
|
133
|
+
codex_row = (
|
|
134
|
+
state.codex_session if state.codex_session.percent is not None else state.codex_weekly
|
|
135
|
+
)
|
|
136
|
+
codex_used = 0.0 if codex_row.reset_done else app.codex_5h_pct
|
|
137
|
+
codex = "--" if codex_used is None else f"{_format_percent(float(codex_used))}%"
|
|
132
138
|
parts.append(codex)
|
|
133
|
-
if not state.hide_agy and state.agy_session.
|
|
134
|
-
agy = f"{_format_percent(state.agy_session.
|
|
139
|
+
if not state.hide_agy and state.agy_session.display_percent is not None:
|
|
140
|
+
agy = f"{_format_percent(state.agy_session.display_percent)}%"
|
|
135
141
|
parts.append(agy)
|
|
136
|
-
if not state.hide_grok and state.grok_weekly.
|
|
137
|
-
grok = f"{_format_percent(state.grok_weekly.
|
|
142
|
+
if not state.hide_grok and state.grok_weekly.display_percent is not None:
|
|
143
|
+
grok = f"{_format_percent(state.grok_weekly.display_percent)}%"
|
|
138
144
|
parts.append(grok)
|
|
139
145
|
# Both providers hidden: keep a recognizable, clickable status item.
|
|
140
146
|
return " · ".join(parts) if parts else "usage"
|
|
@@ -91,7 +91,7 @@ def _data_uri(asset_name: str) -> str:
|
|
|
91
91
|
|
|
92
92
|
def _row_payload(row: QuotaRowState) -> dict[str, object]:
|
|
93
93
|
return {
|
|
94
|
-
"percent": row.
|
|
94
|
+
"percent": row.display_percent,
|
|
95
95
|
"percentText": row.percent_text,
|
|
96
96
|
"resetText": row.reset_text,
|
|
97
97
|
"resetTextCompact": row.reset_text_compact or row.reset_text,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from i18n import _t
|
|
6
|
+
from menubar import manual_refresh
|
|
7
|
+
from menubar.state import _empty_state
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.parametrize("normal", ["status_synced", "data_stale_hint", "status_no_data"])
|
|
11
|
+
def test_manual_refresh_restores_new_status(normal: str) -> None:
|
|
12
|
+
previous = _empty_state()
|
|
13
|
+
manual_refresh.begin(previous)
|
|
14
|
+
assert previous.status_text == "Status: ↻ Refreshing…"
|
|
15
|
+
assert previous.status_long is False
|
|
16
|
+
state = _empty_state()
|
|
17
|
+
state.status_text = _t("en", "status_text", value=_t("en", normal))
|
|
18
|
+
state.status_long = True
|
|
19
|
+
assert manual_refresh.finish(previous, state) is state
|
|
20
|
+
assert state.status_text == _t("en", "status_text", value=_t("en", normal))
|
|
21
|
+
assert state.status_long is True
|
|
22
|
+
assert state.refresh_status is None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_auto_refresh_never_changes_status() -> None:
|
|
26
|
+
previous, state = _empty_state(), _empty_state()
|
|
27
|
+
expected = state.status_text
|
|
28
|
+
manual_refresh.finish(previous, state, queued=True)
|
|
29
|
+
assert state.status_text == expected
|
|
30
|
+
assert state.refresh_status is None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_manual_refresh_queued_behind_auto_waits_for_its_result() -> None:
|
|
34
|
+
previous = _empty_state()
|
|
35
|
+
manual_refresh.begin(previous, queued=True)
|
|
36
|
+
state = manual_refresh.finish(previous, _empty_state(), queued=True)
|
|
37
|
+
assert state.status_text == "Status: ↻ Refreshing…"
|
|
38
|
+
result = manual_refresh.finish(state, _empty_state())
|
|
39
|
+
assert result.status_text == _empty_state().status_text
|
|
40
|
+
assert result.refresh_status is None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_mac_manual_refresh_pushes_status_and_failure_restores_it() -> None:
|
|
44
|
+
state = _empty_state()
|
|
45
|
+
expected = state.status_text
|
|
46
|
+
pushed: list[str] = []
|
|
47
|
+
calls: list[bool] = []
|
|
48
|
+
app = SimpleNamespace(
|
|
49
|
+
latest_state=state,
|
|
50
|
+
_refresh_in_flight=False,
|
|
51
|
+
_refresh=lambda *, queue_if_busy: calls.append(queue_if_busy),
|
|
52
|
+
_panel_window_is_visible=lambda: True,
|
|
53
|
+
popover_controller=SimpleNamespace(
|
|
54
|
+
setState_=lambda state: pushed.append(state.status_text)
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
manual_refresh.start_mac(app)
|
|
58
|
+
assert calls == [True]
|
|
59
|
+
assert pushed == ["Status: ↻ Refreshing…"]
|
|
60
|
+
manual_refresh.fail_mac(app)
|
|
61
|
+
assert pushed == ["Status: ↻ Refreshing…", expected]
|
|
62
|
+
assert app._refresh_in_flight is False
|
|
@@ -19,6 +19,7 @@ import pytest
|
|
|
19
19
|
import panels
|
|
20
20
|
import quota.agy_window_keeper as agy_window_keeper
|
|
21
21
|
import quota.window_keeper as window_keeper
|
|
22
|
+
from i18n import _t
|
|
22
23
|
from installer import statusline_settings
|
|
23
24
|
from loaders import codex_loader, grok_loader, history_loader
|
|
24
25
|
from menubar import actions as menubar_actions
|
|
@@ -334,19 +335,21 @@ def test_quota_row_shows_imminent_reset_with_30_seconds_remaining() -> None:
|
|
|
334
335
|
assert row.warning is False
|
|
335
336
|
|
|
336
337
|
|
|
337
|
-
def
|
|
338
|
+
def test_quota_row_shows_done_reset_at_zero_seconds() -> None:
|
|
338
339
|
row = menubar._quota_row(
|
|
339
340
|
"Session", 50.0, 1_000.0, 1_000.0, menubar.CODEX_COLOR, language="zh-TW"
|
|
340
341
|
)
|
|
341
342
|
|
|
342
|
-
assert row.reset_text == "
|
|
343
|
+
assert row.reset_text == "已重置"
|
|
344
|
+
assert row.display_percent == 0.0
|
|
343
345
|
assert row.warning is False
|
|
344
346
|
|
|
345
347
|
|
|
346
|
-
def
|
|
348
|
+
def test_quota_row_shows_done_reset_after_reset_time() -> None:
|
|
347
349
|
row = menubar._quota_row("Session", 50.0, 970.0, 1_000.0, menubar.CODEX_COLOR, language="zh-TW")
|
|
348
350
|
|
|
349
|
-
assert row.reset_text == "
|
|
351
|
+
assert row.reset_text == "已重置"
|
|
352
|
+
assert row.display_percent == 0.0
|
|
350
353
|
assert row.warning is False
|
|
351
354
|
|
|
352
355
|
|
|
@@ -2690,9 +2693,13 @@ def test_refresh_now_queues_when_refresh_is_busy() -> None:
|
|
|
2690
2693
|
delegate._refresh_in_flight = True
|
|
2691
2694
|
delegate._refresh_queued = False
|
|
2692
2695
|
|
|
2696
|
+
delegate.popover = SimpleNamespace(isVisible=lambda: False)
|
|
2693
2697
|
delegate.refreshNow_(None)
|
|
2694
2698
|
|
|
2695
2699
|
assert delegate._refresh_queued is True
|
|
2700
|
+
assert delegate.latest_state.status_text == _t(
|
|
2701
|
+
delegate.language, "status_text", value=_t(delegate.language, "status_refreshing")
|
|
2702
|
+
)
|
|
2696
2703
|
|
|
2697
2704
|
|
|
2698
2705
|
def test_apply_refresh_result_clears_busy_flag_when_ui_update_fails() -> None:
|
|
@@ -3128,3 +3135,30 @@ def test_state_from_outcome_shows_setup_button_for_codex_only(
|
|
|
3128
3135
|
)
|
|
3129
3136
|
|
|
3130
3137
|
assert state.show_install_button is True
|
|
3138
|
+
|
|
3139
|
+
|
|
3140
|
+
def test_reset_title_keeps_notification_values(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
3141
|
+
from types import SimpleNamespace
|
|
3142
|
+
|
|
3143
|
+
state = menubar._empty_state("en")
|
|
3144
|
+
row = menubar._quota_row("Session", 100.0, 999.0, 1000.0, menubar.CLAUDE_COLOR)
|
|
3145
|
+
state.claude_session = state.codex_session = state.agy_session = state.grok_weekly = row
|
|
3146
|
+
state.hide_claude = state.hide_codex = state.hide_agy = state.hide_grok = False
|
|
3147
|
+
app = SimpleNamespace(codex_5h_pct=100.0, _menubar_text_cache={})
|
|
3148
|
+
assert menubar_title._compose_title(app, state) == "0% · 0% · 0% · 0%"
|
|
3149
|
+
texts: list[str] = []
|
|
3150
|
+
original_text_string = menubar_title._menubar_text_string
|
|
3151
|
+
|
|
3152
|
+
def record_text(app: Any, text: str) -> Any:
|
|
3153
|
+
texts.append(text)
|
|
3154
|
+
return original_text_string(app, text)
|
|
3155
|
+
|
|
3156
|
+
monkeypatch.setattr(menubar_title, "_menubar_text_string", record_text)
|
|
3157
|
+
menubar_title._menubar_attributed_title(app, state)
|
|
3158
|
+
assert texts.count(" 0%") == 4
|
|
3159
|
+
state.claude_session = menubar._missing_row("Session", menubar.CLAUDE_COLOR)
|
|
3160
|
+
state.codex_session = menubar._missing_row("", menubar.CODEX_COLOR)
|
|
3161
|
+
state.codex_weekly = row
|
|
3162
|
+
state.hide_claude = state.hide_agy = state.hide_grok = True
|
|
3163
|
+
assert menubar_title._compose_title(app, state) == "0%"
|
|
3164
|
+
assert row.percent == 100.0
|
|
@@ -288,7 +288,7 @@ def test_project_quota_counts_down_by_cache_age() -> None:
|
|
|
288
288
|
assert projection.session.reset_text == "Resets in 9m"
|
|
289
289
|
|
|
290
290
|
|
|
291
|
-
def
|
|
291
|
+
def test_project_quota_displays_zero_when_overdue() -> None:
|
|
292
292
|
now_dt = datetime(2026, 1, 1, 0, 0, tzinfo=UTC)
|
|
293
293
|
fetched = now_dt - timedelta(minutes=18)
|
|
294
294
|
quota = AgyQuotaResult(
|
|
@@ -307,7 +307,10 @@ def test_project_quota_countdown_clamps_to_one_minute_when_overdue() -> None:
|
|
|
307
307
|
projection = menubar_agy.project_quota(quota, "en", now=now_dt.timestamp())
|
|
308
308
|
|
|
309
309
|
assert projection is not None
|
|
310
|
-
assert projection.session.reset_text == "
|
|
310
|
+
assert projection.session.reset_text == "Reset"
|
|
311
|
+
assert projection.session.display_percent == 0.0
|
|
312
|
+
assert projection.session.percent == 50.0
|
|
313
|
+
assert not projection.session.warning
|
|
311
314
|
|
|
312
315
|
|
|
313
316
|
def test_project_quota_warns_when_session_will_empty_before_reset() -> None:
|
|
@@ -478,3 +481,17 @@ def test_project_quota_warns_for_weekly_window_after_thirty_minute_span() -> Non
|
|
|
478
481
|
|
|
479
482
|
assert projection is not None
|
|
480
483
|
assert projection.weekly.warning is True
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@pytest.mark.parametrize(
|
|
487
|
+
"age_minutes, text, used",
|
|
488
|
+
[(12.0, "Reset", 0.0), (11.5, "Reset imminent", 90.0), (11.0, "Resets in 1m", 90.0)],
|
|
489
|
+
)
|
|
490
|
+
def test_window_row_reset_boundary(age_minutes: float, text: str, used: float) -> None:
|
|
491
|
+
window = AgyQuotaWindow(10.0, None, 12)
|
|
492
|
+
row = menubar_agy._window_row("Session", window, "en", age_minutes, forecast_seconds=10.0)
|
|
493
|
+
assert row.display_percent == used
|
|
494
|
+
if age_minutes >= 11.5:
|
|
495
|
+
assert row.reset_text == text
|
|
496
|
+
assert not row.warning
|
|
497
|
+
assert window.remaining_percent == 10.0
|
|
@@ -92,3 +92,17 @@ def test_build_popover_state_includes_grok_row_and_visibility() -> None:
|
|
|
92
92
|
assert state.hide_grok is False
|
|
93
93
|
assert state.grok_weekly.percent == 18.0
|
|
94
94
|
assert state.card_order == ("claude", "codex", "agy", "grok")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.mark.parametrize(
|
|
98
|
+
"offset, text, used", [(0, "Reset", 0.0), (1, "Reset", 0.0), (-30, "Reset imminent", 18.0)]
|
|
99
|
+
)
|
|
100
|
+
def test_project_quota_reset_boundary(offset: int, text: str, used: float) -> None:
|
|
101
|
+
quota = _quota()
|
|
102
|
+
now = datetime(2026, 9, 1, 15, 50, 8, tzinfo=UTC).timestamp() + offset
|
|
103
|
+
projection = menubar_grok.project_quota(quota, "en", now=now)
|
|
104
|
+
assert projection is not None
|
|
105
|
+
assert projection.weekly.display_percent == used
|
|
106
|
+
assert projection.weekly.reset_text == text
|
|
107
|
+
assert not projection.weekly.warning
|
|
108
|
+
assert quota.used_percent == 18.0
|