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.
Files changed (224) hide show
  1. {usage_cli-0.30.24/usage_cli.egg-info → usage_cli-0.31.0}/PKG-INFO +4 -4
  2. {usage_cli-0.30.24 → usage_cli-0.31.0}/README.md +3 -3
  3. {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/reporter.py +1 -1
  4. {usage_cli-0.30.24 → usage_cli-0.31.0}/i18n.json +5 -0
  5. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/agy_loader.py +39 -5
  6. {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/__init__.py +14 -0
  7. {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/dynamic_height.py +26 -4
  8. {usage_cli-0.30.24 → usage_cli-0.31.0}/pyproject.toml +1 -1
  9. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_agy_loader.py +98 -4
  10. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_analyzer_pipeline.py +1 -0
  11. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_dynamic_height.py +2 -2
  12. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_panel_footer_layout.py +4 -3
  13. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_panels.py +3 -0
  14. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_wintray.py +217 -2
  15. {usage_cli-0.30.24 → usage_cli-0.31.0/usage_cli.egg-info}/PKG-INFO +4 -4
  16. {usage_cli-0.30.24 → usage_cli-0.31.0}/wintray/app.py +129 -31
  17. {usage_cli-0.30.24 → usage_cli-0.31.0}/LICENSE +0 -0
  18. {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/__init__.py +0 -0
  19. {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/agy.py +0 -0
  20. {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/claude.py +0 -0
  21. {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/codex.py +0 -0
  22. {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/grok.py +0 -0
  23. {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/muse.py +0 -0
  24. {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/rate_limits.py +0 -0
  25. {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/registry.py +0 -0
  26. {usage_cli-0.30.24 → usage_cli-0.31.0}/adapters/types.py +0 -0
  27. {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/__init__.py +0 -0
  28. {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/aggregator.py +0 -0
  29. {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/blocks.py +0 -0
  30. {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/diagnoser.py +0 -0
  31. {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/dispatch_ledger.py +0 -0
  32. {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/insights.py +0 -0
  33. {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/persona_loader.py +0 -0
  34. {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/subscription.py +0 -0
  35. {usage_cli-0.30.24 → usage_cli-0.31.0}/analyzer/usage_snapshot.py +0 -0
  36. {usage_cli-0.30.24 → usage_cli-0.31.0}/doctor.py +0 -0
  37. {usage_cli-0.30.24 → usage_cli-0.31.0}/fsevents_watch.py +0 -0
  38. {usage_cli-0.30.24 → usage_cli-0.31.0}/i18n.py +0 -0
  39. {usage_cli-0.30.24 → usage_cli-0.31.0}/installer/__init__.py +0 -0
  40. {usage_cli-0.30.24 → usage_cli-0.31.0}/installer/login_item.py +0 -0
  41. {usage_cli-0.30.24 → usage_cli-0.31.0}/installer/session_hooks.py +0 -0
  42. {usage_cli-0.30.24 → usage_cli-0.31.0}/installer/setup_hook.py +0 -0
  43. {usage_cli-0.30.24 → usage_cli-0.31.0}/installer/statusline_settings.py +0 -0
  44. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/__init__.py +0 -0
  45. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/agy_disk_cache.py +0 -0
  46. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/agy_quota_probe.py +0 -0
  47. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/cache_quarantine.py +0 -0
  48. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/claude_paths.py +0 -0
  49. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/codex_disk_cache.py +0 -0
  50. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/codex_events.py +0 -0
  51. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/codex_fork_replay.py +0 -0
  52. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/codex_loader.py +0 -0
  53. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/codex_paths.py +0 -0
  54. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/disk_cache_common.py +0 -0
  55. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/disk_cache_lifecycle.py +0 -0
  56. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/grok_loader.py +0 -0
  57. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/grok_quota_probe.py +0 -0
  58. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/history_disk_cache.py +0 -0
  59. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/history_loader.py +0 -0
  60. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/jsonl_limits.py +0 -0
  61. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/jsonl_utils.py +0 -0
  62. {usage_cli-0.30.24 → usage_cli-0.31.0}/loaders/muse_loader.py +0 -0
  63. {usage_cli-0.30.24 → usage_cli-0.31.0}/main.py +0 -0
  64. {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/__init__.py +0 -0
  65. {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/actions.py +0 -0
  66. {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/agy.py +0 -0
  67. {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/app.py +0 -0
  68. {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/chrome.py +0 -0
  69. {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/grok.py +0 -0
  70. {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/menu.py +0 -0
  71. {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/notify.py +0 -0
  72. {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/popover.py +0 -0
  73. {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/prefs.py +0 -0
  74. {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/refresh.py +0 -0
  75. {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/state.py +0 -0
  76. {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/title.py +0 -0
  77. {usage_cli-0.30.24 → usage_cli-0.31.0}/menubar/update.py +0 -0
  78. {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/base.py +0 -0
  79. {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/panel_scale.py +0 -0
  80. {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/panel_window.py +0 -0
  81. {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/panel_window_state.py +0 -0
  82. {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/payload.py +0 -0
  83. {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/web_panel.py +0 -0
  84. {usage_cli-0.30.24 → usage_cli-0.31.0}/panels/window_drag.py +0 -0
  85. {usage_cli-0.30.24 → usage_cli-0.31.0}/prefs.py +0 -0
  86. {usage_cli-0.30.24 → usage_cli-0.31.0}/pricing.py +0 -0
  87. {usage_cli-0.30.24 → usage_cli-0.31.0}/project_resolver.py +0 -0
  88. {usage_cli-0.30.24 → usage_cli-0.31.0}/quota/__init__.py +0 -0
  89. {usage_cli-0.30.24 → usage_cli-0.31.0}/quota/agy_window_keeper.py +0 -0
  90. {usage_cli-0.30.24 → usage_cli-0.31.0}/quota/burn_rate.py +0 -0
  91. {usage_cli-0.30.24 → usage_cli-0.31.0}/quota/codex_window_keeper.py +0 -0
  92. {usage_cli-0.30.24 → usage_cli-0.31.0}/quota/usage_rate.py +0 -0
  93. {usage_cli-0.30.24 → usage_cli-0.31.0}/quota/window_keeper.py +0 -0
  94. {usage_cli-0.30.24 → usage_cli-0.31.0}/service_status.py +0 -0
  95. {usage_cli-0.30.24 → usage_cli-0.31.0}/setup.cfg +0 -0
  96. {usage_cli-0.30.24 → usage_cli-0.31.0}/setup_app.py +0 -0
  97. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_adapters_claude.py +0 -0
  98. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_adapters_codex.py +0 -0
  99. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_adapters_rate_limits.py +0 -0
  100. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_aggregator.py +0 -0
  101. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_agy_adapter.py +0 -0
  102. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_agy_disk_cache.py +0 -0
  103. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_agy_quota_probe.py +0 -0
  104. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_agy_window_keeper.py +0 -0
  105. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_analyzer_blocks.py +0 -0
  106. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_burn_rate.py +0 -0
  107. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_cache_quarantine.py +0 -0
  108. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_claude_paths.py +0 -0
  109. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_codex_consistency.py +0 -0
  110. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_codex_loader.py +0 -0
  111. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_codex_window_keeper.py +0 -0
  112. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_diagnoser.py +0 -0
  113. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_disk_cache_shards.py +0 -0
  114. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_dispatch_ledger.py +0 -0
  115. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_doctor.py +0 -0
  116. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_fsevents_watch.py +0 -0
  117. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_grok_loader.py +0 -0
  118. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_grok_quota_probe.py +0 -0
  119. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_history_loader.py +0 -0
  120. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_hook_scripts_are_standalone.py +0 -0
  121. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_html_report_snapshot.py +0 -0
  122. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_i18n_format_fallback.py +0 -0
  123. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_i18n_key_parity.py +0 -0
  124. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_i18n_packaged_path.py +0 -0
  125. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_insights.py +0 -0
  126. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_jsonl_utils.py +0 -0
  127. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_login_item.py +0 -0
  128. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_main.py +0 -0
  129. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_menubar.py +0 -0
  130. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_menubar_agy.py +0 -0
  131. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_menubar_grok.py +0 -0
  132. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_menubar_prefs.py +0 -0
  133. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_menubar_state.py +0 -0
  134. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_muse_loader.py +0 -0
  135. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_packaged_resources.py +0 -0
  136. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_packaging.py +0 -0
  137. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_panel_scale.py +0 -0
  138. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_panel_window_state.py +0 -0
  139. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_panels_lazy_import.py +0 -0
  140. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_persona_loader.py +0 -0
  141. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_pricing.py +0 -0
  142. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_project_resolver.py +0 -0
  143. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_report_daily_chart.py +0 -0
  144. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_report_filter_js.py +0 -0
  145. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_reporter_build_data.py +0 -0
  146. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_service_status.py +0 -0
  147. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_session_resume.py +0 -0
  148. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_setup_app.py +0 -0
  149. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_setup_hook.py +0 -0
  150. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_setup_hook_coexistence.py +0 -0
  151. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_setup_hook_resume.py +0 -0
  152. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_setup_hook_terse.py +0 -0
  153. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_subprocess_utils.py +0 -0
  154. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_subscription.py +0 -0
  155. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_time_utils.py +0 -0
  156. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_tui.py +0 -0
  157. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_tui_sprite.py +0 -0
  158. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_ui_tables.py +0 -0
  159. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_update_checker.py +0 -0
  160. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_update_gate.py +0 -0
  161. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_update_release_notes.py +0 -0
  162. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_cli.py +0 -0
  163. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_client.py +0 -0
  164. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_diagnosis_snapshot.py +0 -0
  165. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_dir_sweeper.py +0 -0
  166. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_lang.py +0 -0
  167. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_logging.py +0 -0
  168. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_notifications.py +0 -0
  169. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_rate.py +0 -0
  170. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_snapshot.py +0 -0
  171. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_snapshot_replay.py +0 -0
  172. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_statusline.py +0 -0
  173. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_statusline_agy.py +0 -0
  174. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_statusline_forwarder.py +0 -0
  175. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_statusline_grok.py +0 -0
  176. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_terse_mode.py +0 -0
  177. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_usage_terse_reminder.py +0 -0
  178. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_web_panel_payload.py +0 -0
  179. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_wheel_packaging.py +0 -0
  180. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_win_login_item.py +0 -0
  181. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_window_keeper.py +0 -0
  182. {usage_cli-0.30.24 → usage_cli-0.31.0}/tests/test_windows_manifest.py +0 -0
  183. {usage_cli-0.30.24 → usage_cli-0.31.0}/tui/__init__.py +0 -0
  184. {usage_cli-0.30.24 → usage_cli-0.31.0}/tui/app.py +0 -0
  185. {usage_cli-0.30.24 → usage_cli-0.31.0}/tui/sprite.py +0 -0
  186. {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/__init__.py +0 -0
  187. {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/html_report.py +0 -0
  188. {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/report_charts.py +0 -0
  189. {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/report_daily_chart.py +0 -0
  190. {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/report_filter.py +0 -0
  191. {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/report_ledger.py +0 -0
  192. {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/report_scripts.py +0 -0
  193. {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/report_styles.py +0 -0
  194. {usage_cli-0.30.24 → usage_cli-0.31.0}/ui/tables.py +0 -0
  195. {usage_cli-0.30.24 → usage_cli-0.31.0}/updates/__init__.py +0 -0
  196. {usage_cli-0.30.24 → usage_cli-0.31.0}/updates/checker.py +0 -0
  197. {usage_cli-0.30.24 → usage_cli-0.31.0}/updates/gate.py +0 -0
  198. {usage_cli-0.30.24 → usage_cli-0.31.0}/updates/release_notes.py +0 -0
  199. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_cli.egg-info/SOURCES.txt +0 -0
  200. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_cli.egg-info/dependency_links.txt +0 -0
  201. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_cli.egg-info/entry_points.txt +0 -0
  202. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_cli.egg-info/requires.txt +0 -0
  203. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_cli.egg-info/top_level.txt +0 -0
  204. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_cli.py +0 -0
  205. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_client.py +0 -0
  206. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_common/__init__.py +0 -0
  207. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_common/subprocess_utils.py +0 -0
  208. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_common/time_utils.py +0 -0
  209. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_common/usage_dir_sweeper.py +0 -0
  210. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_common/usage_lang.py +0 -0
  211. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_common/usage_logging.py +0 -0
  212. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_diagnosis_snapshot.py +0 -0
  213. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_notifications.py +0 -0
  214. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_session_resume.py +0 -0
  215. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_statusline.py +0 -0
  216. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_statusline_agy.py +0 -0
  217. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_statusline_forwarder.py +0 -0
  218. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_statusline_grok.py +0 -0
  219. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_terse_mode.py +0 -0
  220. {usage_cli-0.30.24 → usage_cli-0.31.0}/usage_terse_reminder.py +0 -0
  221. {usage_cli-0.30.24 → usage_cli-0.31.0}/wintray/__init__.py +0 -0
  222. {usage_cli-0.30.24 → usage_cli-0.31.0}/wintray/login_item.py +0 -0
  223. {usage_cli-0.30.24 → usage_cli-0.31.0}/wintray/menu.py +0 -0
  224. {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.30.24
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
- - **14 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, and Catppuccin (official palette, all four flavors).
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 14 quota themes available on macOS (Default plus the other thirteen) in WebView2. Right-click provides Reset Panel Position and Quit; panel switching, refresh, launch at login, and update checks are in the panel menu.
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 **14 visual themes** directly from the UI:
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
- - **14 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, and Catppuccin (official palette, all four flavors).
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 14 quota themes available on macOS (Default plus the other thirteen) in WebView2. Right-click provides Reset Panel Position and Quit; panel switching, refresh, launch at login, and update checks are in the panel menu.
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 **14 visual themes** directly from the UI:
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 = 3
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 = 2
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
- rows = connection.execute("SELECT data FROM gen_metadata ORDER BY idx")
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 (blob,) in rows:
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 = _timestamp(_message_field(_message_field(chat_model, 9) or b"", 4))
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 or session_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
- window.usageApplyPanelZoom = function(scale, naturalHeight) {
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
- requestContentHeight();
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(requestContentHeight).observe(wrap, {
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(requestContentHeight);
201
+ document.fonts.ready.then(requestContentChange);
180
202
  }
181
203
  requestContentHeight();
182
204
  })();
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usage-cli"
3
- version = "0.30.24"
3
+ version = "0.31.0"
4
4
  description = "usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -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("CREATE TABLE steps (idx INTEGER PRIMARY KEY, step_payload BLOB);")
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=now, dedup_key="no-steps")],
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(
@@ -252,6 +252,7 @@ def test_html_panels_expose_analyze_action() -> None:
252
252
  "lepidoptera.html",
253
253
  "matrix.html",
254
254
  "migration.html",
255
+ "sketchbook.html",
255
256
  "newspaper.html",
256
257
  "origami.html",
257
258
  "prism_arcade.html",
@@ -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(requestContentHeight)" in CONTENT_HEIGHT_SCRIPT
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
- def test_footer_layout(tmp_path: Path) -> None:
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/panel_footer_layout.cjs"), str(tmp_path)],
38
+ [node, str(ROOT / "tests" / script), str(tmp_path)],
38
39
  cwd=ROOT,
39
40
  capture_output=True,
40
41
  text=True,
41
- timeout=240,
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",