usage-cli 0.30.24__tar.gz → 0.31.1__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.30.24/usage_cli.egg-info → usage_cli-0.31.1}/PKG-INFO +4 -4
- {usage_cli-0.30.24 → usage_cli-0.31.1}/README.md +3 -3
- {usage_cli-0.30.24 → usage_cli-0.31.1}/analyzer/reporter.py +1 -1
- {usage_cli-0.30.24 → usage_cli-0.31.1}/i18n.json +10 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/agy_loader.py +39 -5
- {usage_cli-0.30.24 → usage_cli-0.31.1}/panels/__init__.py +28 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/panels/dynamic_height.py +37 -6
- {usage_cli-0.30.24 → usage_cli-0.31.1}/pyproject.toml +1 -1
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_agy_loader.py +98 -4
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_analyzer_pipeline.py +2 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_dynamic_height.py +15 -2
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_panel_footer_layout.py +4 -3
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_panels.py +6 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_wintray.py +302 -2
- {usage_cli-0.30.24 → usage_cli-0.31.1/usage_cli.egg-info}/PKG-INFO +4 -4
- {usage_cli-0.30.24 → usage_cli-0.31.1}/wintray/app.py +177 -42
- {usage_cli-0.30.24 → usage_cli-0.31.1}/LICENSE +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/adapters/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/adapters/agy.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/adapters/claude.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/adapters/codex.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/adapters/grok.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/adapters/muse.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/adapters/rate_limits.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/adapters/registry.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/adapters/types.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/analyzer/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/analyzer/aggregator.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/analyzer/blocks.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/analyzer/diagnoser.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/analyzer/dispatch_ledger.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/analyzer/insights.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/analyzer/persona_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/analyzer/subscription.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/analyzer/usage_snapshot.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/doctor.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/fsevents_watch.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/i18n.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/installer/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/installer/login_item.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/installer/session_hooks.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/installer/setup_hook.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/installer/statusline_settings.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/agy_disk_cache.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/agy_quota_probe.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/cache_quarantine.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/claude_paths.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/codex_disk_cache.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/codex_events.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/codex_fork_replay.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/codex_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/codex_paths.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/disk_cache_common.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/disk_cache_lifecycle.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/grok_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/grok_quota_probe.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/history_disk_cache.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/history_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/jsonl_limits.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/jsonl_utils.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/loaders/muse_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/main.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/menubar/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/menubar/actions.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/menubar/agy.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/menubar/app.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/menubar/chrome.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/menubar/grok.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/menubar/menu.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/menubar/notify.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/menubar/popover.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/menubar/prefs.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/menubar/refresh.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/menubar/state.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/menubar/title.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/menubar/update.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/panels/base.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/panels/panel_scale.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/panels/panel_window.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/panels/panel_window_state.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/panels/payload.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/panels/web_panel.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/panels/window_drag.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/prefs.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/pricing.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/project_resolver.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/quota/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/quota/agy_window_keeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/quota/burn_rate.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/quota/codex_window_keeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/quota/usage_rate.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/quota/window_keeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/service_status.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/setup.cfg +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/setup_app.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_adapters_claude.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_adapters_codex.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_adapters_rate_limits.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_aggregator.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_agy_adapter.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_agy_disk_cache.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_agy_quota_probe.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_agy_window_keeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_analyzer_blocks.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_burn_rate.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_cache_quarantine.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_claude_paths.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_codex_consistency.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_codex_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_codex_window_keeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_diagnoser.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_disk_cache_shards.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_dispatch_ledger.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_doctor.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_fsevents_watch.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_grok_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_grok_quota_probe.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_history_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_hook_scripts_are_standalone.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_html_report_snapshot.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_i18n_format_fallback.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_i18n_key_parity.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_i18n_packaged_path.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_insights.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_jsonl_utils.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_login_item.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_main.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_menubar.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_menubar_agy.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_menubar_grok.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_menubar_prefs.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_menubar_state.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_muse_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_packaged_resources.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_packaging.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_panel_scale.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_panel_window_state.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_panels_lazy_import.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_persona_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_pricing.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_project_resolver.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_report_daily_chart.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_report_filter_js.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_reporter_build_data.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_service_status.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_session_resume.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_setup_app.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_setup_hook.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_setup_hook_coexistence.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_setup_hook_resume.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_setup_hook_terse.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_subprocess_utils.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_subscription.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_time_utils.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_tui.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_tui_sprite.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_ui_tables.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_update_checker.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_update_gate.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_update_release_notes.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_cli.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_client.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_dir_sweeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_lang.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_logging.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_notifications.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_rate.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_snapshot.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_snapshot_replay.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_statusline.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_statusline_agy.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_statusline_forwarder.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_statusline_grok.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_terse_mode.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_usage_terse_reminder.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_web_panel_payload.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_wheel_packaging.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_win_login_item.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_window_keeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tests/test_windows_manifest.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tui/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tui/app.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/tui/sprite.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/ui/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/ui/html_report.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/ui/report_charts.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/ui/report_daily_chart.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/ui/report_filter.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/ui/report_ledger.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/ui/report_scripts.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/ui/report_styles.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/ui/tables.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/updates/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/updates/checker.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/updates/gate.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/updates/release_notes.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_cli.egg-info/SOURCES.txt +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_cli.egg-info/dependency_links.txt +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_cli.egg-info/entry_points.txt +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_cli.egg-info/requires.txt +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_cli.egg-info/top_level.txt +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_cli.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_client.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_common/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_common/subprocess_utils.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_common/time_utils.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_common/usage_dir_sweeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_common/usage_lang.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_common/usage_logging.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_notifications.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_session_resume.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_statusline.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_statusline_agy.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_statusline_forwarder.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_statusline_grok.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_terse_mode.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/usage_terse_reminder.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/wintray/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/wintray/login_item.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/wintray/menu.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.1}/wintray/watch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: usage-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.31.1
|
|
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
|
|
@@ -87,7 +87,7 @@ It lands in your Applications folder automatically. Open it once; if macOS 15 or
|
|
|
87
87
|
|
|
88
88
|
### Experience & Customization
|
|
89
89
|
|
|
90
|
-
- **
|
|
90
|
+
- **16 Visual Themes:** Switch between panel styles including Default, Matrix, Windows 95, Vintage Newspaper, Cloud Observation, Midnight Aquarium, Prism Arcade, Black Hole, World Cup 2026, Lepidoptera, Migration, Stained Glass, Origami, Sketchbook, Heart Monitor, and Catppuccin (official palette, all four flavors).
|
|
91
91
|
- **Place the Panel Anywhere:** The panel is no longer pinned under the menu bar icon. Drag it from any empty spot to wherever you want it, and it reopens there next time. It stays put when another app takes focus — a second click on the menu bar icon, or Escape, closes it.
|
|
92
92
|
- **Drag to Reorder:** Grab any quota card and drag it up or down to swap the order — the arrangement is shared across every theme with quota cards (all except World Cup 2026) and survives restarts.
|
|
93
93
|
- **Automatic Localization:** UI text is available in Traditional Chinese, Simplified Chinese, English, Japanese, and Korean, automatically matching your system settings.
|
|
@@ -147,7 +147,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
|
|
|
147
147
|
|
|
148
148
|
Windows has the full core experience: the system-tray UI, Claude Code status-line hook, and Codex history parsing all work natively. Download `usage-windows.zip` from the [latest GitHub Release](https://github.com/aqua5230/usage/releases/latest), unzip it, then run `usage.exe`—no installer is needed. If SmartScreen shows **Windows protected your PC** on first launch, click **More info** → **Run anyway**. The tray UI requires Microsoft Edge WebView2 Runtime, which is normally included with Windows 10 and 11.
|
|
149
149
|
|
|
150
|
-
The system-tray icon updates with your Claude quota percentage; its tooltip summarizes the Claude and Codex windows. Left-click opens the same
|
|
150
|
+
The system-tray icon updates with your Claude quota percentage; its tooltip summarizes the Claude and Codex windows. Left-click opens the same 16 quota themes available on macOS (Default plus the other fifteen) in WebView2. Right-click provides Reset Panel Position and Quit; panel switching, refresh, launch at login, and update checks are in the panel menu.
|
|
151
151
|
|
|
152
152
|
Windows differences: the panel opens at the bottom-right of the working area rather than next to the tray icon; update prompts use a system Yes/No dialog.
|
|
153
153
|
|
|
@@ -164,7 +164,7 @@ Privacy policy: this program will not transfer any information to other networke
|
|
|
164
164
|
|
|
165
165
|
## Theme Gallery
|
|
166
166
|
|
|
167
|
-
Switch between **
|
|
167
|
+
Switch between **16 visual themes** directly from the UI:
|
|
168
168
|
|
|
169
169
|
<p align="center">
|
|
170
170
|
<img src="docs/classic.en.png" width="32%" alt="Classic theme" />
|
|
@@ -67,7 +67,7 @@ It lands in your Applications folder automatically. Open it once; if macOS 15 or
|
|
|
67
67
|
|
|
68
68
|
### Experience & Customization
|
|
69
69
|
|
|
70
|
-
- **
|
|
70
|
+
- **16 Visual Themes:** Switch between panel styles including Default, Matrix, Windows 95, Vintage Newspaper, Cloud Observation, Midnight Aquarium, Prism Arcade, Black Hole, World Cup 2026, Lepidoptera, Migration, Stained Glass, Origami, Sketchbook, Heart Monitor, and Catppuccin (official palette, all four flavors).
|
|
71
71
|
- **Place the Panel Anywhere:** The panel is no longer pinned under the menu bar icon. Drag it from any empty spot to wherever you want it, and it reopens there next time. It stays put when another app takes focus — a second click on the menu bar icon, or Escape, closes it.
|
|
72
72
|
- **Drag to Reorder:** Grab any quota card and drag it up or down to swap the order — the arrangement is shared across every theme with quota cards (all except World Cup 2026) and survives restarts.
|
|
73
73
|
- **Automatic Localization:** UI text is available in Traditional Chinese, Simplified Chinese, English, Japanese, and Korean, automatically matching your system settings.
|
|
@@ -127,7 +127,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
|
|
|
127
127
|
|
|
128
128
|
Windows has the full core experience: the system-tray UI, Claude Code status-line hook, and Codex history parsing all work natively. Download `usage-windows.zip` from the [latest GitHub Release](https://github.com/aqua5230/usage/releases/latest), unzip it, then run `usage.exe`—no installer is needed. If SmartScreen shows **Windows protected your PC** on first launch, click **More info** → **Run anyway**. The tray UI requires Microsoft Edge WebView2 Runtime, which is normally included with Windows 10 and 11.
|
|
129
129
|
|
|
130
|
-
The system-tray icon updates with your Claude quota percentage; its tooltip summarizes the Claude and Codex windows. Left-click opens the same
|
|
130
|
+
The system-tray icon updates with your Claude quota percentage; its tooltip summarizes the Claude and Codex windows. Left-click opens the same 16 quota themes available on macOS (Default plus the other fifteen) in WebView2. Right-click provides Reset Panel Position and Quit; panel switching, refresh, launch at login, and update checks are in the panel menu.
|
|
131
131
|
|
|
132
132
|
Windows differences: the panel opens at the bottom-right of the working area rather than next to the tray icon; update prompts use a system Yes/No dialog.
|
|
133
133
|
|
|
@@ -144,7 +144,7 @@ Privacy policy: this program will not transfer any information to other networke
|
|
|
144
144
|
|
|
145
145
|
## Theme Gallery
|
|
146
146
|
|
|
147
|
-
Switch between **
|
|
147
|
+
Switch between **16 visual themes** directly from the UI:
|
|
148
148
|
|
|
149
149
|
<p align="center">
|
|
150
150
|
<img src="docs/classic.en.png" width="32%" alt="Classic theme" />
|
|
@@ -43,7 +43,7 @@ _YEAR_WEEKS = 53
|
|
|
43
43
|
YEAR_CACHE_PATH = Path(os.path.expanduser("~/.usage/year_cache.json"))
|
|
44
44
|
YEAR_LEDGER_PATH = YEAR_CACHE_PATH.with_name("year_ledger.json")
|
|
45
45
|
YEAR_CACHE_TTL_SECONDS = 6 * 3600
|
|
46
|
-
_YEAR_CACHE_SCHEMA =
|
|
46
|
+
_YEAR_CACHE_SCHEMA = 4
|
|
47
47
|
_YEAR_LEDGER_SCHEMA = 1
|
|
48
48
|
_YEAR_LEDGER_TRIM_BUFFER_DAYS = 60
|
|
49
49
|
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"panel_prism_arcade": "稜鏡街機",
|
|
11
11
|
"panel_stained_glass": "彩繪玻璃",
|
|
12
12
|
"panel_migration": "候鳥遷徙",
|
|
13
|
+
"panel_sketchbook": "手繪筆記",
|
|
14
|
+
"panel_heart_monitor": "心電圖",
|
|
13
15
|
"panel_origami": "摺紙",
|
|
14
16
|
"panel_catppuccin": "Catppuccin",
|
|
15
17
|
"panel_black_hole": "黑洞視界",
|
|
@@ -435,6 +437,8 @@
|
|
|
435
437
|
"panel_prism_arcade": "Prism Arcade",
|
|
436
438
|
"panel_stained_glass": "Stained Glass",
|
|
437
439
|
"panel_migration": "Migration",
|
|
440
|
+
"panel_sketchbook": "Sketchbook",
|
|
441
|
+
"panel_heart_monitor": "Heart Monitor",
|
|
438
442
|
"panel_origami": "Origami",
|
|
439
443
|
"panel_catppuccin": "Catppuccin",
|
|
440
444
|
"panel_black_hole": "Black Hole",
|
|
@@ -860,6 +864,8 @@
|
|
|
860
864
|
"panel_prism_arcade": "棱镜街机",
|
|
861
865
|
"panel_stained_glass": "彩绘玻璃",
|
|
862
866
|
"panel_migration": "候鸟迁徙",
|
|
867
|
+
"panel_sketchbook": "手绘笔记",
|
|
868
|
+
"panel_heart_monitor": "心电图",
|
|
863
869
|
"panel_origami": "折纸",
|
|
864
870
|
"panel_catppuccin": "Catppuccin",
|
|
865
871
|
"panel_black_hole": "黑洞视界",
|
|
@@ -1285,6 +1291,8 @@
|
|
|
1285
1291
|
"panel_prism_arcade": "プリズムアーケード",
|
|
1286
1292
|
"panel_stained_glass": "ステンドグラス",
|
|
1287
1293
|
"panel_migration": "渡り鳥",
|
|
1294
|
+
"panel_sketchbook": "手描きノート",
|
|
1295
|
+
"panel_heart_monitor": "心電図モニター",
|
|
1288
1296
|
"panel_origami": "折り紙",
|
|
1289
1297
|
"panel_catppuccin": "Catppuccin",
|
|
1290
1298
|
"panel_black_hole": "ブラックホール",
|
|
@@ -1710,6 +1718,8 @@
|
|
|
1710
1718
|
"panel_prism_arcade": "프리즘 아케이드",
|
|
1711
1719
|
"panel_stained_glass": "스테인드글라스",
|
|
1712
1720
|
"panel_migration": "철새 이동",
|
|
1721
|
+
"panel_sketchbook": "손그림 노트",
|
|
1722
|
+
"panel_heart_monitor": "심전도 모니터",
|
|
1713
1723
|
"panel_origami": "종이접기",
|
|
1714
1724
|
"panel_catppuccin": "Catppuccin",
|
|
1715
1725
|
"panel_black_hole": "블랙홀",
|
|
@@ -38,7 +38,7 @@ AGY_SESSIONS_DIR = Path(os.path.expanduser("~/.gemini/antigravity-cli/conversati
|
|
|
38
38
|
# below that count evicts databases parsed during the prior refresh, causing a
|
|
39
39
|
# permanent full SQLite/protobuf reparse on every load (LRU thrashing).
|
|
40
40
|
_FILE_CACHE_MAXSIZE = 4096
|
|
41
|
-
_AGY_DB_CACHE_SCHEMA =
|
|
41
|
+
_AGY_DB_CACHE_SCHEMA = 3
|
|
42
42
|
AGY_CACHE_PATH = Path(os.path.expanduser("~/.usage/agy_db_cache.json"))
|
|
43
43
|
_disk_cache_seeded = False
|
|
44
44
|
_DISK_CACHE_FLUSH_INTERVAL_S = 300.0
|
|
@@ -239,10 +239,32 @@ def _parse_database(path: Path) -> tuple[list[AgyUsageEntry], int] | None:
|
|
|
239
239
|
with sqlite3.connect(_readonly_sqlite_uri(path), uri=True) as connection:
|
|
240
240
|
session_timestamp = _session_timestamp(connection, path)
|
|
241
241
|
project = _session_project(connection)
|
|
242
|
-
|
|
242
|
+
by_response_id: dict[str, datetime] = {}
|
|
243
|
+
by_gen_idx: dict[int, datetime] = {}
|
|
244
|
+
try:
|
|
245
|
+
for (metadata,) in connection.execute(
|
|
246
|
+
"SELECT metadata FROM steps WHERE step_type = 15 AND metadata IS NOT NULL"
|
|
247
|
+
):
|
|
248
|
+
if not isinstance(metadata, bytes):
|
|
249
|
+
continue
|
|
250
|
+
timestamp = _timestamp(_message_field(metadata, 1))
|
|
251
|
+
if timestamp is None:
|
|
252
|
+
continue
|
|
253
|
+
response_metadata = _message_field(metadata, 9)
|
|
254
|
+
response_id = _string_field(response_metadata or b"", 11)
|
|
255
|
+
if response_id and response_id.strip():
|
|
256
|
+
by_response_id[response_id.strip()] = timestamp
|
|
257
|
+
generation_metadata = _message_field(metadata, 20)
|
|
258
|
+
gen_idx = _varint_field(generation_metadata or b"", 3)
|
|
259
|
+
if gen_idx is not None:
|
|
260
|
+
by_gen_idx[gen_idx] = timestamp
|
|
261
|
+
except sqlite3.Error:
|
|
262
|
+
by_response_id.clear()
|
|
263
|
+
by_gen_idx.clear()
|
|
264
|
+
rows = connection.execute("SELECT idx, data FROM gen_metadata ORDER BY idx")
|
|
243
265
|
entries: list[AgyUsageEntry] = []
|
|
244
266
|
skipped_missing_dedup_key = 0
|
|
245
|
-
for
|
|
267
|
+
for gen_idx, blob in rows:
|
|
246
268
|
if not isinstance(blob, bytes):
|
|
247
269
|
continue
|
|
248
270
|
entry, missing_dedup_key = _parse_generation(
|
|
@@ -250,6 +272,9 @@ def _parse_database(path: Path) -> tuple[list[AgyUsageEntry], int] | None:
|
|
|
250
272
|
path.stem,
|
|
251
273
|
session_timestamp,
|
|
252
274
|
set(),
|
|
275
|
+
gen_idx=gen_idx,
|
|
276
|
+
by_response_id=by_response_id,
|
|
277
|
+
by_gen_idx=by_gen_idx,
|
|
253
278
|
project=project,
|
|
254
279
|
)
|
|
255
280
|
skipped_missing_dedup_key += missing_dedup_key
|
|
@@ -304,6 +329,10 @@ def _parse_generation(
|
|
|
304
329
|
session_id: str,
|
|
305
330
|
session_timestamp: datetime,
|
|
306
331
|
seen_dedup_keys: set[str],
|
|
332
|
+
*,
|
|
333
|
+
gen_idx: int,
|
|
334
|
+
by_response_id: dict[str, datetime],
|
|
335
|
+
by_gen_idx: dict[int, datetime],
|
|
307
336
|
project: str = "",
|
|
308
337
|
) -> tuple[AgyUsageEntry | None, int]:
|
|
309
338
|
chat_model = _message_field(blob, 1)
|
|
@@ -335,11 +364,16 @@ def _parse_generation(
|
|
|
335
364
|
return None, 0
|
|
336
365
|
seen_dedup_keys.add(dedup_key)
|
|
337
366
|
|
|
338
|
-
timestamp =
|
|
367
|
+
timestamp = (
|
|
368
|
+
_timestamp(_message_field(_message_field(chat_model, 9) or b"", 4))
|
|
369
|
+
or by_response_id.get(dedup_key.strip())
|
|
370
|
+
or by_gen_idx.get(gen_idx)
|
|
371
|
+
or session_timestamp
|
|
372
|
+
)
|
|
339
373
|
model = _string_field(chat_model, 19) or "unknown"
|
|
340
374
|
return (
|
|
341
375
|
AgyUsageEntry(
|
|
342
|
-
timestamp=timestamp
|
|
376
|
+
timestamp=timestamp,
|
|
343
377
|
model=model,
|
|
344
378
|
input_tokens=system_tokens + new_input_tokens,
|
|
345
379
|
output_tokens=output_tokens,
|
|
@@ -130,6 +130,34 @@ def all_panels() -> tuple[Panel, ...]:
|
|
|
130
130
|
status_wrap_extra_height=30.0,
|
|
131
131
|
service_alert_height=32.0,
|
|
132
132
|
),
|
|
133
|
+
# Use classic's measured values as initial estimates for the same DOM;
|
|
134
|
+
# dynamic_height.py measures sketchbook's actual rendered height.
|
|
135
|
+
HTMLPanel(
|
|
136
|
+
"sketchbook",
|
|
137
|
+
"panel_sketchbook",
|
|
138
|
+
"sketchbook.html",
|
|
139
|
+
height=1132.0,
|
|
140
|
+
claude_card_height=192.0,
|
|
141
|
+
codex_card_height=192.0,
|
|
142
|
+
agy_card_height=192.0,
|
|
143
|
+
grok_card_height=128.0,
|
|
144
|
+
status_wrap_extra_height=30.0,
|
|
145
|
+
service_alert_height=32.0,
|
|
146
|
+
),
|
|
147
|
+
# Use classic's measured values as initial estimates for the same DOM;
|
|
148
|
+
# dynamic_height.py measures heart_monitor's actual rendered height.
|
|
149
|
+
HTMLPanel(
|
|
150
|
+
"heart_monitor",
|
|
151
|
+
"panel_heart_monitor",
|
|
152
|
+
"heart_monitor.html",
|
|
153
|
+
height=1132.0,
|
|
154
|
+
claude_card_height=192.0,
|
|
155
|
+
codex_card_height=192.0,
|
|
156
|
+
agy_card_height=192.0,
|
|
157
|
+
grok_card_height=128.0,
|
|
158
|
+
status_wrap_extra_height=30.0,
|
|
159
|
+
service_alert_height=32.0,
|
|
160
|
+
),
|
|
133
161
|
# Reuse classic's measured values because the DOM structure is identical;
|
|
134
162
|
# remeasure if a future render shows clipping.
|
|
135
163
|
HTMLPanel(
|
|
@@ -15,6 +15,10 @@ CONTENT_HEIGHT_SCRIPT = """
|
|
|
15
15
|
if (typeof applyState !== "function") return;
|
|
16
16
|
var scheduled = false;
|
|
17
17
|
var lastPostedHeight = null;
|
|
18
|
+
var lastPostedDpr = null;
|
|
19
|
+
var fittingHeight = 0;
|
|
20
|
+
var contentChanged = false;
|
|
21
|
+
var lastMeasuredHeight = null;
|
|
18
22
|
function naturalContentHeight() {
|
|
19
23
|
var root = document.documentElement;
|
|
20
24
|
// Measure in the displayed zoom: WebKit may enforce a minimum rendered
|
|
@@ -116,12 +120,30 @@ CONTENT_HEIGHT_SCRIPT = """
|
|
|
116
120
|
function reportContentHeight() {
|
|
117
121
|
scheduled = false;
|
|
118
122
|
var height = naturalContentHeight();
|
|
123
|
+
// A zoom-only fit may grow to accommodate wrapping, but must not shrink
|
|
124
|
+
// again: fractional WebKit metrics can otherwise alternate between two
|
|
125
|
+
// heights, each producing the other's scale. Real content changes can
|
|
126
|
+
// still contract; identical state refreshes keep the settled fit.
|
|
127
|
+
if (contentChanged && height !== lastMeasuredHeight) fittingHeight = 0;
|
|
128
|
+
contentChanged = false;
|
|
129
|
+
lastMeasuredHeight = height;
|
|
130
|
+
if (Number.isFinite(height) && height > 0) {
|
|
131
|
+
fittingHeight = Math.max(fittingHeight, height);
|
|
132
|
+
height = fittingHeight;
|
|
133
|
+
}
|
|
119
134
|
var bridge = window.webkit && window.webkit.messageHandlers
|
|
120
135
|
&& window.webkit.messageHandlers.usage;
|
|
121
|
-
|
|
136
|
+
// The native side sizes its window from this height, so it also needs the
|
|
137
|
+
// ratio the page really renders at. On Windows that is the display scale
|
|
138
|
+
// times the "Text size" accessibility setting (125% x 109% = 1.3625), which
|
|
139
|
+
// GetDpiForWindow does not include.
|
|
140
|
+
var dpr = window.devicePixelRatio;
|
|
141
|
+
if (Number.isFinite(height) && height > 0
|
|
142
|
+
&& (height !== lastPostedHeight || dpr !== lastPostedDpr) && bridge
|
|
122
143
|
&& typeof bridge.postMessage === "function") {
|
|
123
144
|
lastPostedHeight = height;
|
|
124
|
-
|
|
145
|
+
lastPostedDpr = dpr;
|
|
146
|
+
bridge.postMessage(JSON.stringify({ action: "content_height", height: height, dpr: dpr }));
|
|
125
147
|
}
|
|
126
148
|
}
|
|
127
149
|
function requestContentHeight() {
|
|
@@ -137,15 +159,24 @@ CONTENT_HEIGHT_SCRIPT = """
|
|
|
137
159
|
window.usageRequestContentHeight = requestContentHeight;
|
|
138
160
|
window.usageInvalidateContentHeight = function() {
|
|
139
161
|
lastPostedHeight = null;
|
|
162
|
+
lastPostedDpr = null;
|
|
163
|
+
fittingHeight = 0;
|
|
140
164
|
requestContentHeight();
|
|
141
165
|
};
|
|
142
|
-
|
|
166
|
+
function requestContentChange() {
|
|
167
|
+
contentChanged = true;
|
|
168
|
+
requestContentHeight();
|
|
169
|
+
}
|
|
170
|
+
window.usageApplyPanelZoom = function(scale, naturalHeight, scroll) {
|
|
143
171
|
var value = Number(scale);
|
|
144
172
|
var root = document.documentElement;
|
|
145
173
|
var body = document.body;
|
|
146
174
|
var height = Number(naturalHeight);
|
|
147
175
|
var scaled = Number.isFinite(value) && value > 0 && value !== 1;
|
|
176
|
+
if (!scaled) fittingHeight = 0;
|
|
148
177
|
root.style.zoom = scaled ? String(value) : "normal";
|
|
178
|
+
root.style.overflowY = scroll ? "auto" : "";
|
|
179
|
+
if (body) body.style.overflowY = scroll ? "auto" : "";
|
|
149
180
|
// Chromium's CSS zoom scales the paint output but leaves its layout box at
|
|
150
181
|
// the viewport height. Give that box the known natural content height so
|
|
151
182
|
// flex children are not shrunk and clipped before they are painted.
|
|
@@ -161,12 +192,12 @@ CONTENT_HEIGHT_SCRIPT = """
|
|
|
161
192
|
};
|
|
162
193
|
window.usageApplyState = function usageApplyStateWithDynamicHeight(state) {
|
|
163
194
|
var result = applyState.apply(this, arguments);
|
|
164
|
-
|
|
195
|
+
requestContentChange();
|
|
165
196
|
return result;
|
|
166
197
|
};
|
|
167
198
|
var wrap = document.querySelector(".wrap");
|
|
168
199
|
if (wrap && typeof MutationObserver === "function") {
|
|
169
|
-
new MutationObserver(
|
|
200
|
+
new MutationObserver(requestContentChange).observe(wrap, {
|
|
170
201
|
childList: true,
|
|
171
202
|
characterData: true,
|
|
172
203
|
subtree: true
|
|
@@ -176,7 +207,7 @@ CONTENT_HEIGHT_SCRIPT = """
|
|
|
176
207
|
new ResizeObserver(requestContentHeight).observe(wrap);
|
|
177
208
|
}
|
|
178
209
|
if (document.fonts && document.fonts.ready) {
|
|
179
|
-
document.fonts.ready.then(
|
|
210
|
+
document.fonts.ready.then(requestContentChange);
|
|
180
211
|
}
|
|
181
212
|
requestContentHeight();
|
|
182
213
|
})();
|
|
@@ -72,11 +72,22 @@ def _trajectory_blob(timestamp: datetime) -> bytes:
|
|
|
72
72
|
return _message_field(2, _timestamp_blob(timestamp))
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
def _step_metadata_blob(timestamp: datetime, response_id: str, gen_idx: int) -> bytes:
|
|
76
|
+
return b"".join(
|
|
77
|
+
(
|
|
78
|
+
_message_field(1, _timestamp_blob(timestamp)),
|
|
79
|
+
_message_field(9, _message_field(11, response_id.encode())),
|
|
80
|
+
_message_field(20, _varint_field(3, gen_idx)),
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
75
85
|
def _write_database(
|
|
76
86
|
path: Path,
|
|
77
87
|
rows: list[bytes],
|
|
78
88
|
session_timestamp: datetime,
|
|
79
89
|
steps_payloads: list[bytes] | None = None,
|
|
90
|
+
steps_metadata: list[tuple[int, bytes]] | None = None,
|
|
80
91
|
) -> None:
|
|
81
92
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
93
|
with sqlite3.connect(path) as connection:
|
|
@@ -92,11 +103,23 @@ def _write_database(
|
|
|
92
103
|
"INSERT INTO trajectory_metadata_blob (data) VALUES (?)",
|
|
93
104
|
(_trajectory_blob(session_timestamp),),
|
|
94
105
|
)
|
|
95
|
-
if steps_payloads is not None:
|
|
96
|
-
connection.execute(
|
|
106
|
+
if steps_payloads is not None or steps_metadata is not None:
|
|
107
|
+
connection.execute(
|
|
108
|
+
"CREATE TABLE steps "
|
|
109
|
+
"(idx INTEGER PRIMARY KEY, step_payload BLOB, step_type INTEGER, metadata BLOB);"
|
|
110
|
+
)
|
|
97
111
|
connection.executemany(
|
|
98
112
|
"INSERT INTO steps (idx, step_payload) VALUES (?, ?)",
|
|
99
|
-
[(index, payload) for index, payload in enumerate(steps_payloads)],
|
|
113
|
+
[(index, payload) for index, payload in enumerate(steps_payloads or [])],
|
|
114
|
+
)
|
|
115
|
+
connection.executemany(
|
|
116
|
+
"INSERT INTO steps (idx, step_type, metadata) VALUES (?, ?, ?)",
|
|
117
|
+
[
|
|
118
|
+
(index, step_type, metadata)
|
|
119
|
+
for index, (step_type, metadata) in enumerate(
|
|
120
|
+
steps_metadata or [], start=len(steps_payloads or [])
|
|
121
|
+
)
|
|
122
|
+
],
|
|
100
123
|
)
|
|
101
124
|
|
|
102
125
|
|
|
@@ -181,6 +204,76 @@ def test_load_entries_filters_on_generation_timestamp_and_falls_back_to_session_
|
|
|
181
204
|
assert entries[0].timestamp == now
|
|
182
205
|
|
|
183
206
|
|
|
207
|
+
def test_load_entries_uses_step_response_id_timestamp(sessions_dir: Path) -> None:
|
|
208
|
+
session_timestamp = datetime(2026, 1, 1, tzinfo=UTC)
|
|
209
|
+
step_timestamp = datetime(2026, 1, 2, 3, 4, 5, 123_456, tzinfo=UTC)
|
|
210
|
+
_write_database(
|
|
211
|
+
sessions_dir / "response_id.db",
|
|
212
|
+
[_generation_blob(timestamp=None, dedup_key=" response-id ")],
|
|
213
|
+
session_timestamp,
|
|
214
|
+
steps_metadata=[(15, _step_metadata_blob(step_timestamp, "response-id", 99))],
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
entries = agy_loader.load_entries()
|
|
218
|
+
|
|
219
|
+
assert len(entries) == 1
|
|
220
|
+
assert entries[0].timestamp == step_timestamp
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_load_entries_uses_step_generation_index_when_response_id_differs(
|
|
224
|
+
sessions_dir: Path,
|
|
225
|
+
) -> None:
|
|
226
|
+
session_timestamp = datetime(2026, 1, 1, tzinfo=UTC)
|
|
227
|
+
step_timestamp = datetime(2026, 1, 2, tzinfo=UTC)
|
|
228
|
+
_write_database(
|
|
229
|
+
sessions_dir / "generation_index.db",
|
|
230
|
+
[_generation_blob(timestamp=None, dedup_key="generation")],
|
|
231
|
+
session_timestamp,
|
|
232
|
+
steps_metadata=[(15, _step_metadata_blob(step_timestamp, "other", 0))],
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
entries = agy_loader.load_entries()
|
|
236
|
+
|
|
237
|
+
assert len(entries) == 1
|
|
238
|
+
assert entries[0].timestamp == step_timestamp
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_load_entries_falls_back_when_step_does_not_match(sessions_dir: Path) -> None:
|
|
242
|
+
session_timestamp = datetime(2026, 1, 1, tzinfo=UTC)
|
|
243
|
+
step_timestamp = datetime(2026, 1, 2, tzinfo=UTC)
|
|
244
|
+
_write_database(
|
|
245
|
+
sessions_dir / "unmatched.db",
|
|
246
|
+
[_generation_blob(timestamp=None, dedup_key="generation")],
|
|
247
|
+
session_timestamp,
|
|
248
|
+
steps_metadata=[
|
|
249
|
+
(14, _step_metadata_blob(step_timestamp, "generation", 0)),
|
|
250
|
+
(15, _step_metadata_blob(step_timestamp, "other", 99)),
|
|
251
|
+
],
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
entries = agy_loader.load_entries()
|
|
255
|
+
|
|
256
|
+
assert len(entries) == 1
|
|
257
|
+
assert entries[0].timestamp == session_timestamp
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_load_entries_prefers_legacy_generation_timestamp(sessions_dir: Path) -> None:
|
|
261
|
+
session_timestamp = datetime(2026, 1, 1, tzinfo=UTC)
|
|
262
|
+
step_timestamp = datetime(2026, 1, 2, tzinfo=UTC)
|
|
263
|
+
generation_timestamp = datetime(2026, 1, 3, tzinfo=UTC)
|
|
264
|
+
_write_database(
|
|
265
|
+
sessions_dir / "legacy.db",
|
|
266
|
+
[_generation_blob(timestamp=generation_timestamp, dedup_key="generation")],
|
|
267
|
+
session_timestamp,
|
|
268
|
+
steps_metadata=[(15, _step_metadata_blob(step_timestamp, "generation", 0))],
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
entries = agy_loader.load_entries()
|
|
272
|
+
|
|
273
|
+
assert len(entries) == 1
|
|
274
|
+
assert entries[0].timestamp == generation_timestamp
|
|
275
|
+
|
|
276
|
+
|
|
184
277
|
def test_recent_input_output_tokens_excludes_cache_and_thinking(sessions_dir: Path) -> None:
|
|
185
278
|
now = datetime.now(UTC)
|
|
186
279
|
_write_database(
|
|
@@ -325,12 +418,13 @@ def test_load_entries_falls_back_to_empty_project_when_steps_table_missing(
|
|
|
325
418
|
now = datetime.now(UTC)
|
|
326
419
|
_write_database(
|
|
327
420
|
sessions_dir / "no_steps.db",
|
|
328
|
-
[_generation_blob(timestamp=
|
|
421
|
+
[_generation_blob(timestamp=None, dedup_key="no-steps")],
|
|
329
422
|
now,
|
|
330
423
|
)
|
|
331
424
|
entries = agy_loader.load_entries()
|
|
332
425
|
assert len(entries) == 1
|
|
333
426
|
assert entries[0].project == ""
|
|
427
|
+
assert entries[0].timestamp == now
|
|
334
428
|
|
|
335
429
|
|
|
336
430
|
def test_load_entries_takes_first_cwd_and_handles_escapes(
|
|
@@ -30,7 +30,7 @@ def test_script_wraps_state_application_and_measures_without_height_constraints(
|
|
|
30
30
|
assert "requestAnimationFrame" in CONTENT_HEIGHT_SCRIPT
|
|
31
31
|
assert "MutationObserver" in CONTENT_HEIGHT_SCRIPT
|
|
32
32
|
assert "ResizeObserver" in CONTENT_HEIGHT_SCRIPT
|
|
33
|
-
assert "document.fonts.ready.then(
|
|
33
|
+
assert "document.fonts.ready.then(requestContentChange)" in CONTENT_HEIGHT_SCRIPT
|
|
34
34
|
assert "height !== lastPostedHeight" in CONTENT_HEIGHT_SCRIPT
|
|
35
35
|
assert "requestContentHeight();" in CONTENT_HEIGHT_SCRIPT
|
|
36
36
|
assert "window.usageInvalidateContentHeight" in CONTENT_HEIGHT_SCRIPT
|
|
@@ -40,7 +40,7 @@ def test_script_wraps_state_application_and_measures_without_height_constraints(
|
|
|
40
40
|
assert "wrap.getBoundingClientRect().height / scale" in CONTENT_HEIGHT_SCRIPT
|
|
41
41
|
assert "element.getBoundingClientRect().height / scale" in CONTENT_HEIGHT_SCRIPT
|
|
42
42
|
assert "window.usageApplyPanelZoom" in CONTENT_HEIGHT_SCRIPT
|
|
43
|
-
assert "function(scale, naturalHeight)" in CONTENT_HEIGHT_SCRIPT
|
|
43
|
+
assert "function(scale, naturalHeight, scroll)" in CONTENT_HEIGHT_SCRIPT
|
|
44
44
|
assert 'body.style.height = ""' in CONTENT_HEIGHT_SCRIPT
|
|
45
45
|
assert 'String(height) + "px"' in CONTENT_HEIGHT_SCRIPT
|
|
46
46
|
assert "innerHeight /" not in CONTENT_HEIGHT_SCRIPT
|
|
@@ -59,6 +59,19 @@ def test_script_wraps_state_application_and_measures_without_height_constraints(
|
|
|
59
59
|
assert "body.style.height = bodyHeight" in CONTENT_HEIGHT_SCRIPT
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
def test_script_reports_the_rendered_device_pixel_ratio_with_the_height() -> None:
|
|
63
|
+
# The window is sized from the reported height, so the native side needs the
|
|
64
|
+
# ratio the page really renders at: on Windows it includes the "Text size"
|
|
65
|
+
# setting that GetDpiForWindow leaves out.
|
|
66
|
+
assert "var dpr = window.devicePixelRatio;" in CONTENT_HEIGHT_SCRIPT
|
|
67
|
+
assert 'action: "content_height", height: height, dpr: dpr' in CONTENT_HEIGHT_SCRIPT
|
|
68
|
+
# A ratio change alone is worth reporting again, and invalidating the
|
|
69
|
+
# height must not leave a stale ratio behind.
|
|
70
|
+
assert "dpr !== lastPostedDpr" in CONTENT_HEIGHT_SCRIPT
|
|
71
|
+
assert "lastPostedDpr = dpr;" in CONTENT_HEIGHT_SCRIPT
|
|
72
|
+
assert "lastPostedDpr = null;" in CONTENT_HEIGHT_SCRIPT
|
|
73
|
+
|
|
74
|
+
|
|
62
75
|
def test_world_cup_declares_its_pitch_height_floor() -> None:
|
|
63
76
|
html = (PANEL_ASSETS / "world_cup.html").read_text(encoding="utf-8")
|
|
64
77
|
|
|
@@ -23,7 +23,8 @@ ROOT = Path(__file__).resolve().parents[1]
|
|
|
23
23
|
os.environ.get("USAGE_BROWSER_TESTS") != "1" or sys.platform != "darwin",
|
|
24
24
|
reason="opt-in browser test using the macOS demo generator",
|
|
25
25
|
)
|
|
26
|
-
|
|
26
|
+
@pytest.mark.parametrize("script", ["panel_footer_layout.cjs", "panel_height_feedback.cjs"])
|
|
27
|
+
def test_footer_layout(tmp_path: Path, script: str) -> None:
|
|
27
28
|
node = shutil.which("node")
|
|
28
29
|
assert node, "USAGE_BROWSER_TESTS requires Node and an existing playwright installation"
|
|
29
30
|
subprocess.run(
|
|
@@ -34,10 +35,10 @@ def test_footer_layout(tmp_path: Path) -> None:
|
|
|
34
35
|
text=True,
|
|
35
36
|
)
|
|
36
37
|
result = subprocess.run(
|
|
37
|
-
[node, str(ROOT / "tests/
|
|
38
|
+
[node, str(ROOT / "tests" / script), str(tmp_path)],
|
|
38
39
|
cwd=ROOT,
|
|
39
40
|
capture_output=True,
|
|
40
41
|
text=True,
|
|
41
|
-
timeout=
|
|
42
|
+
timeout=600,
|
|
42
43
|
)
|
|
43
44
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
@@ -32,6 +32,8 @@ CARD_PANEL_FILENAMES = (
|
|
|
32
32
|
"lepidoptera.html",
|
|
33
33
|
"matrix.html",
|
|
34
34
|
"migration.html",
|
|
35
|
+
"sketchbook.html",
|
|
36
|
+
"heart_monitor.html",
|
|
35
37
|
"newspaper.html",
|
|
36
38
|
"origami.html",
|
|
37
39
|
"prism_arcade.html",
|
|
@@ -68,6 +70,8 @@ def test_registered_panel_ids_are_unique() -> None:
|
|
|
68
70
|
"prism_arcade",
|
|
69
71
|
"stained_glass",
|
|
70
72
|
"migration",
|
|
73
|
+
"sketchbook",
|
|
74
|
+
"heart_monitor",
|
|
71
75
|
"origami",
|
|
72
76
|
"black_hole",
|
|
73
77
|
"lepidoptera",
|
|
@@ -90,6 +94,8 @@ def test_registered_panel_i18n_keys() -> None:
|
|
|
90
94
|
"panel_prism_arcade",
|
|
91
95
|
"panel_stained_glass",
|
|
92
96
|
"panel_migration",
|
|
97
|
+
"panel_sketchbook",
|
|
98
|
+
"panel_heart_monitor",
|
|
93
99
|
"panel_origami",
|
|
94
100
|
"panel_black_hole",
|
|
95
101
|
"panel_lepidoptera",
|