usage-cli 0.30.24__tar.gz → 0.31.0__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.0}/PKG-INFO +4 -4
- {usage_cli-0.30.24 → usage_cli-0.31.0}/README.md +3 -3
- {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/reporter.py +1 -1
- {usage_cli-0.30.24 → usage_cli-0.31.0}/i18n.json +5 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/agy_loader.py +39 -5
- {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/__init__.py +14 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/dynamic_height.py +26 -4
- {usage_cli-0.30.24 → usage_cli-0.31.0}/pyproject.toml +1 -1
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_agy_loader.py +98 -4
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_analyzer_pipeline.py +1 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_dynamic_height.py +2 -2
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_panel_footer_layout.py +4 -3
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_panels.py +3 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_wintray.py +217 -2
- {usage_cli-0.30.24 → usage_cli-0.31.0/usage_cli.egg-info}/PKG-INFO +4 -4
- {usage_cli-0.30.24 → usage_cli-0.31.0}/wintray/app.py +129 -31
- {usage_cli-0.30.24 → usage_cli-0.31.0}/LICENSE +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/agy.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/claude.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/codex.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/grok.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/muse.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/rate_limits.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/registry.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/types.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/aggregator.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/blocks.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/diagnoser.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/dispatch_ledger.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/insights.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/persona_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/subscription.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/usage_snapshot.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/doctor.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/fsevents_watch.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/i18n.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/installer/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/installer/login_item.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/installer/session_hooks.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/installer/setup_hook.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/installer/statusline_settings.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/agy_disk_cache.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/agy_quota_probe.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/cache_quarantine.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/claude_paths.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/codex_disk_cache.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/codex_events.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/codex_fork_replay.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/codex_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/codex_paths.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/disk_cache_common.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/disk_cache_lifecycle.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/grok_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/grok_quota_probe.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/history_disk_cache.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/history_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/jsonl_limits.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/jsonl_utils.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/muse_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/main.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/actions.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/agy.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/app.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/chrome.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/grok.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/menu.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/notify.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/popover.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/prefs.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/refresh.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/state.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/title.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/update.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/base.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/panel_scale.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/panel_window.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/panel_window_state.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/payload.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/web_panel.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/window_drag.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/prefs.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/pricing.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/project_resolver.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/quota/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/quota/agy_window_keeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/quota/burn_rate.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/quota/codex_window_keeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/quota/usage_rate.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/quota/window_keeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/service_status.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/setup.cfg +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/setup_app.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_adapters_claude.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_adapters_codex.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_adapters_rate_limits.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_aggregator.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_agy_adapter.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_agy_disk_cache.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_agy_quota_probe.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_agy_window_keeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_analyzer_blocks.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_burn_rate.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_cache_quarantine.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_claude_paths.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_codex_consistency.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_codex_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_codex_window_keeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_diagnoser.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_disk_cache_shards.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_dispatch_ledger.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_doctor.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_fsevents_watch.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_grok_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_grok_quota_probe.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_history_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_hook_scripts_are_standalone.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_html_report_snapshot.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_i18n_format_fallback.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_i18n_key_parity.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_i18n_packaged_path.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_insights.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_jsonl_utils.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_login_item.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_main.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_menubar.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_menubar_agy.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_menubar_grok.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_menubar_prefs.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_menubar_state.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_muse_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_packaged_resources.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_packaging.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_panel_scale.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_panel_window_state.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_panels_lazy_import.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_persona_loader.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_pricing.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_project_resolver.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_report_daily_chart.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_report_filter_js.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_reporter_build_data.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_service_status.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_session_resume.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_setup_app.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_setup_hook.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_setup_hook_coexistence.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_setup_hook_resume.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_setup_hook_terse.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_subprocess_utils.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_subscription.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_time_utils.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_tui.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_tui_sprite.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_ui_tables.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_update_checker.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_update_gate.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_update_release_notes.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_cli.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_client.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_dir_sweeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_lang.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_logging.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_notifications.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_rate.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_snapshot.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_snapshot_replay.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_statusline.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_statusline_agy.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_statusline_forwarder.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_statusline_grok.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_terse_mode.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_terse_reminder.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_web_panel_payload.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_wheel_packaging.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_win_login_item.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_window_keeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_windows_manifest.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tui/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tui/app.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/tui/sprite.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/html_report.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/report_charts.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/report_daily_chart.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/report_filter.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/report_ledger.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/report_scripts.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/report_styles.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/tables.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/updates/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/updates/checker.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/updates/gate.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/updates/release_notes.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_cli.egg-info/SOURCES.txt +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_cli.egg-info/dependency_links.txt +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_cli.egg-info/entry_points.txt +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_cli.egg-info/requires.txt +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_cli.egg-info/top_level.txt +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_cli.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_client.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_common/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_common/subprocess_utils.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_common/time_utils.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_common/usage_dir_sweeper.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_common/usage_lang.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_common/usage_logging.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_notifications.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_session_resume.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_statusline.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_statusline_agy.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_statusline_forwarder.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_statusline_grok.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_terse_mode.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_terse_reminder.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/wintray/__init__.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/wintray/login_item.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/wintray/menu.py +0 -0
- {usage_cli-0.30.24 → usage_cli-0.31.0}/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.0
|
|
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
|
+
- **15 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, 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 15 quota themes available on macOS (Default plus the other fourteen) 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 **15 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
|
+
- **15 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, 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 15 quota themes available on macOS (Default plus the other fourteen) 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 **15 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,7 @@
|
|
|
10
10
|
"panel_prism_arcade": "稜鏡街機",
|
|
11
11
|
"panel_stained_glass": "彩繪玻璃",
|
|
12
12
|
"panel_migration": "候鳥遷徙",
|
|
13
|
+
"panel_sketchbook": "手繪筆記",
|
|
13
14
|
"panel_origami": "摺紙",
|
|
14
15
|
"panel_catppuccin": "Catppuccin",
|
|
15
16
|
"panel_black_hole": "黑洞視界",
|
|
@@ -435,6 +436,7 @@
|
|
|
435
436
|
"panel_prism_arcade": "Prism Arcade",
|
|
436
437
|
"panel_stained_glass": "Stained Glass",
|
|
437
438
|
"panel_migration": "Migration",
|
|
439
|
+
"panel_sketchbook": "Sketchbook",
|
|
438
440
|
"panel_origami": "Origami",
|
|
439
441
|
"panel_catppuccin": "Catppuccin",
|
|
440
442
|
"panel_black_hole": "Black Hole",
|
|
@@ -860,6 +862,7 @@
|
|
|
860
862
|
"panel_prism_arcade": "棱镜街机",
|
|
861
863
|
"panel_stained_glass": "彩绘玻璃",
|
|
862
864
|
"panel_migration": "候鸟迁徙",
|
|
865
|
+
"panel_sketchbook": "手绘笔记",
|
|
863
866
|
"panel_origami": "折纸",
|
|
864
867
|
"panel_catppuccin": "Catppuccin",
|
|
865
868
|
"panel_black_hole": "黑洞视界",
|
|
@@ -1285,6 +1288,7 @@
|
|
|
1285
1288
|
"panel_prism_arcade": "プリズムアーケード",
|
|
1286
1289
|
"panel_stained_glass": "ステンドグラス",
|
|
1287
1290
|
"panel_migration": "渡り鳥",
|
|
1291
|
+
"panel_sketchbook": "手描きノート",
|
|
1288
1292
|
"panel_origami": "折り紙",
|
|
1289
1293
|
"panel_catppuccin": "Catppuccin",
|
|
1290
1294
|
"panel_black_hole": "ブラックホール",
|
|
@@ -1710,6 +1714,7 @@
|
|
|
1710
1714
|
"panel_prism_arcade": "프리즘 아케이드",
|
|
1711
1715
|
"panel_stained_glass": "스테인드글라스",
|
|
1712
1716
|
"panel_migration": "철새 이동",
|
|
1717
|
+
"panel_sketchbook": "손그림 노트",
|
|
1713
1718
|
"panel_origami": "종이접기",
|
|
1714
1719
|
"panel_catppuccin": "Catppuccin",
|
|
1715
1720
|
"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,20 @@ 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
|
+
),
|
|
133
147
|
# Reuse classic's measured values because the DOM structure is identical;
|
|
134
148
|
# remeasure if a future render shows clipping.
|
|
135
149
|
HTMLPanel(
|
|
@@ -15,6 +15,9 @@ CONTENT_HEIGHT_SCRIPT = """
|
|
|
15
15
|
if (typeof applyState !== "function") return;
|
|
16
16
|
var scheduled = false;
|
|
17
17
|
var lastPostedHeight = null;
|
|
18
|
+
var fittingHeight = 0;
|
|
19
|
+
var contentChanged = false;
|
|
20
|
+
var lastMeasuredHeight = null;
|
|
18
21
|
function naturalContentHeight() {
|
|
19
22
|
var root = document.documentElement;
|
|
20
23
|
// Measure in the displayed zoom: WebKit may enforce a minimum rendered
|
|
@@ -116,6 +119,17 @@ CONTENT_HEIGHT_SCRIPT = """
|
|
|
116
119
|
function reportContentHeight() {
|
|
117
120
|
scheduled = false;
|
|
118
121
|
var height = naturalContentHeight();
|
|
122
|
+
// A zoom-only fit may grow to accommodate wrapping, but must not shrink
|
|
123
|
+
// again: fractional WebKit metrics can otherwise alternate between two
|
|
124
|
+
// heights, each producing the other's scale. Real content changes can
|
|
125
|
+
// still contract; identical state refreshes keep the settled fit.
|
|
126
|
+
if (contentChanged && height !== lastMeasuredHeight) fittingHeight = 0;
|
|
127
|
+
contentChanged = false;
|
|
128
|
+
lastMeasuredHeight = height;
|
|
129
|
+
if (Number.isFinite(height) && height > 0) {
|
|
130
|
+
fittingHeight = Math.max(fittingHeight, height);
|
|
131
|
+
height = fittingHeight;
|
|
132
|
+
}
|
|
119
133
|
var bridge = window.webkit && window.webkit.messageHandlers
|
|
120
134
|
&& window.webkit.messageHandlers.usage;
|
|
121
135
|
if (Number.isFinite(height) && height > 0 && height !== lastPostedHeight && bridge
|
|
@@ -137,15 +151,23 @@ CONTENT_HEIGHT_SCRIPT = """
|
|
|
137
151
|
window.usageRequestContentHeight = requestContentHeight;
|
|
138
152
|
window.usageInvalidateContentHeight = function() {
|
|
139
153
|
lastPostedHeight = null;
|
|
154
|
+
fittingHeight = 0;
|
|
140
155
|
requestContentHeight();
|
|
141
156
|
};
|
|
142
|
-
|
|
157
|
+
function requestContentChange() {
|
|
158
|
+
contentChanged = true;
|
|
159
|
+
requestContentHeight();
|
|
160
|
+
}
|
|
161
|
+
window.usageApplyPanelZoom = function(scale, naturalHeight, scroll) {
|
|
143
162
|
var value = Number(scale);
|
|
144
163
|
var root = document.documentElement;
|
|
145
164
|
var body = document.body;
|
|
146
165
|
var height = Number(naturalHeight);
|
|
147
166
|
var scaled = Number.isFinite(value) && value > 0 && value !== 1;
|
|
167
|
+
if (!scaled) fittingHeight = 0;
|
|
148
168
|
root.style.zoom = scaled ? String(value) : "normal";
|
|
169
|
+
root.style.overflowY = scroll ? "auto" : "";
|
|
170
|
+
if (body) body.style.overflowY = scroll ? "auto" : "";
|
|
149
171
|
// Chromium's CSS zoom scales the paint output but leaves its layout box at
|
|
150
172
|
// the viewport height. Give that box the known natural content height so
|
|
151
173
|
// flex children are not shrunk and clipped before they are painted.
|
|
@@ -161,12 +183,12 @@ CONTENT_HEIGHT_SCRIPT = """
|
|
|
161
183
|
};
|
|
162
184
|
window.usageApplyState = function usageApplyStateWithDynamicHeight(state) {
|
|
163
185
|
var result = applyState.apply(this, arguments);
|
|
164
|
-
|
|
186
|
+
requestContentChange();
|
|
165
187
|
return result;
|
|
166
188
|
};
|
|
167
189
|
var wrap = document.querySelector(".wrap");
|
|
168
190
|
if (wrap && typeof MutationObserver === "function") {
|
|
169
|
-
new MutationObserver(
|
|
191
|
+
new MutationObserver(requestContentChange).observe(wrap, {
|
|
170
192
|
childList: true,
|
|
171
193
|
characterData: true,
|
|
172
194
|
subtree: true
|
|
@@ -176,7 +198,7 @@ CONTENT_HEIGHT_SCRIPT = """
|
|
|
176
198
|
new ResizeObserver(requestContentHeight).observe(wrap);
|
|
177
199
|
}
|
|
178
200
|
if (document.fonts && document.fonts.ready) {
|
|
179
|
-
document.fonts.ready.then(
|
|
201
|
+
document.fonts.ready.then(requestContentChange);
|
|
180
202
|
}
|
|
181
203
|
requestContentHeight();
|
|
182
204
|
})();
|
|
@@ -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
|
|
@@ -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,7 @@ CARD_PANEL_FILENAMES = (
|
|
|
32
32
|
"lepidoptera.html",
|
|
33
33
|
"matrix.html",
|
|
34
34
|
"migration.html",
|
|
35
|
+
"sketchbook.html",
|
|
35
36
|
"newspaper.html",
|
|
36
37
|
"origami.html",
|
|
37
38
|
"prism_arcade.html",
|
|
@@ -68,6 +69,7 @@ def test_registered_panel_ids_are_unique() -> None:
|
|
|
68
69
|
"prism_arcade",
|
|
69
70
|
"stained_glass",
|
|
70
71
|
"migration",
|
|
72
|
+
"sketchbook",
|
|
71
73
|
"origami",
|
|
72
74
|
"black_hole",
|
|
73
75
|
"lepidoptera",
|
|
@@ -90,6 +92,7 @@ def test_registered_panel_i18n_keys() -> None:
|
|
|
90
92
|
"panel_prism_arcade",
|
|
91
93
|
"panel_stained_glass",
|
|
92
94
|
"panel_migration",
|
|
95
|
+
"panel_sketchbook",
|
|
93
96
|
"panel_origami",
|
|
94
97
|
"panel_black_hole",
|
|
95
98
|
"panel_lepidoptera",
|