comate-cli 0.7.3__tar.gz → 0.7.4__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 (201) hide show
  1. {comate_cli-0.7.3 → comate_cli-0.7.4}/PKG-INFO +1 -1
  2. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/status_bar.py +16 -3
  3. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui.py +55 -10
  4. {comate_cli-0.7.3 → comate_cli-0.7.4}/pyproject.toml +1 -1
  5. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_status_bar.py +79 -8
  6. comate_cli-0.7.4/tests/test_tui_startup_latency.py +274 -0
  7. {comate_cli-0.7.3 → comate_cli-0.7.4}/uv.lock +2 -2
  8. comate_cli-0.7.3/tests/test_tui_startup_latency.py +0 -104
  9. {comate_cli-0.7.3 → comate_cli-0.7.4}/.gitignore +0 -0
  10. {comate_cli-0.7.3 → comate_cli-0.7.4}/CHANGELOG.md +0 -0
  11. {comate_cli-0.7.3 → comate_cli-0.7.4}/README.md +0 -0
  12. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/__init__.py +0 -0
  13. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/__main__.py +0 -0
  14. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/main.py +0 -0
  15. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/mcp_cli.py +0 -0
  16. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/__init__.py +0 -0
  17. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/animations.py +0 -0
  18. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/app.py +0 -0
  19. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/assistant_render.py +0 -0
  20. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/codenames.py +0 -0
  21. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/config/__init__.py +0 -0
  22. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/config/model.py +0 -0
  23. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/config/picker.py +0 -0
  24. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/config/picker_state.py +0 -0
  25. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/config/store.py +0 -0
  26. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  27. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/env_utils.py +0 -0
  28. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/error_display.py +0 -0
  29. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/event_renderer.py +0 -0
  30. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/figures.py +0 -0
  31. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  32. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/goal_resume_view.py +0 -0
  33. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/history_printer.py +0 -0
  34. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/input_geometry.py +0 -0
  35. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  36. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/logging_adapter.py +0 -0
  37. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/logo.py +0 -0
  38. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/markdown_render.py +0 -0
  39. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/mention_completer.py +0 -0
  40. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/message_style.py +0 -0
  41. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/models.py +0 -0
  42. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/path_context_hint.py +0 -0
  43. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
  44. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
  45. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
  46. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
  47. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
  48. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
  49. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
  50. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
  51. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
  52. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
  53. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
  54. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
  55. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
  56. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/preflight.py +0 -0
  57. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/question_view.py +0 -0
  58. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/resume_picker.py +0 -0
  59. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/resume_preview.py +0 -0
  60. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/resume_selector.py +0 -0
  61. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  62. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  63. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/selection_menu.py +0 -0
  64. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/slash_commands.py +0 -0
  65. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/startup.py +0 -0
  66. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/startup_profile.py +0 -0
  67. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/statusline/__init__.py +0 -0
  68. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/statusline/model.py +0 -0
  69. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/statusline/picker.py +0 -0
  70. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/statusline/picker_state.py +0 -0
  71. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/statusline/store.py +0 -0
  72. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/text_effects.py +0 -0
  73. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tips.py +0 -0
  74. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tool_fold.py +0 -0
  75. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tool_result_formatters.py +0 -0
  76. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tool_result_store.py +0 -0
  77. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
  78. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tool_view.py +0 -0
  79. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
  80. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  81. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
  82. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
  83. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
  84. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
  85. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
  86. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
  87. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
  88. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  89. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  90. {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/update_check.py +0 -0
  91. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/config/__init__.py +0 -0
  92. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/config/test_model.py +0 -0
  93. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/config/test_picker_state.py +0 -0
  94. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/config/test_picker_ui.py +0 -0
  95. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/config/test_roundtrip.py +0 -0
  96. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/config/test_store_load.py +0 -0
  97. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/config/test_store_save.py +0 -0
  98. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/conftest.py +0 -0
  99. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/fixtures/fake_mcp_misbehaving_stdout.py +0 -0
  100. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/statusline/__init__.py +0 -0
  101. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/statusline/test_model.py +0 -0
  102. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/statusline/test_picker_state.py +0 -0
  103. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/statusline/test_store.py +0 -0
  104. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_animator_shuffle.py +0 -0
  105. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_app_mcp_preload.py +0 -0
  106. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_app_preflight_gate.py +0 -0
  107. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_app_print_mode.py +0 -0
  108. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_app_shutdown.py +0 -0
  109. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_app_startup_latency.py +0 -0
  110. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_app_token_cost_config.py +0 -0
  111. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_app_usage_line.py +0 -0
  112. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_btw_slash_command.py +0 -0
  113. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_cli_project_root.py +0 -0
  114. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_compact_command_semantics.py +0 -0
  115. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_completion_context_activation.py +0 -0
  116. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_completion_status_panel.py +0 -0
  117. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_context_command.py +0 -0
  118. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_custom_slash_commands.py +0 -0
  119. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_discover_tab.py +0 -0
  120. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_errors_tab.py +0 -0
  121. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_event_renderer.py +0 -0
  122. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_event_renderer_boundary.py +0 -0
  123. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_event_renderer_e2e.py +0 -0
  124. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_event_renderer_log_boundary.py +0 -0
  125. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_event_renderer_log_queue.py +0 -0
  126. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_event_renderer_streaming.py +0 -0
  127. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_event_renderer_tool_fold.py +0 -0
  128. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_format_error.py +0 -0
  129. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_goal_resume_tui.py +0 -0
  130. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_goal_resume_view.py +0 -0
  131. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_goal_slash_command.py +0 -0
  132. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_handle_error.py +0 -0
  133. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_history_printer.py +0 -0
  134. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_history_printer_log.py +0 -0
  135. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_history_printer_subtitle_position.py +0 -0
  136. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_history_printer_tool_fold.py +0 -0
  137. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_history_sync.py +0 -0
  138. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_history_sync_tool_fold.py +0 -0
  139. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_input_behavior.py +0 -0
  140. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_input_history.py +0 -0
  141. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_installed_tab.py +0 -0
  142. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_interrupt_exit_semantics.py +0 -0
  143. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_layout_coordinator.py +0 -0
  144. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_logging_adapter.py +0 -0
  145. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_logo.py +0 -0
  146. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_main_args.py +0 -0
  147. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_markdown_render.py +0 -0
  148. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_marketplaces_tab.py +0 -0
  149. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_mcp_cli.py +0 -0
  150. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_mcp_slash_command.py +0 -0
  151. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_mention_completer.py +0 -0
  152. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_path_context_hint.py +0 -0
  153. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_plugin_slash_commands.py +0 -0
  154. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_plugin_tui_components.py +0 -0
  155. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_preflight.py +0 -0
  156. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_preflight_copilot.py +0 -0
  157. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_question_key_bindings.py +0 -0
  158. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_question_view.py +0 -0
  159. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_resume_picker.py +0 -0
  160. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_resume_preview.py +0 -0
  161. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_resume_selector.py +0 -0
  162. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_rewind_command_semantics.py +0 -0
  163. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_rpc_protocol.py +0 -0
  164. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_rpc_stdio_bridge.py +0 -0
  165. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_selection_menu.py +0 -0
  166. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_session_query_token_summary.py +0 -0
  167. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_shutdown_noise_guard.py +0 -0
  168. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_shutdown_noise_integration.py +0 -0
  169. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_skills_slash_command.py +0 -0
  170. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_slash_argument_hint.py +0 -0
  171. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_slash_clear.py +0 -0
  172. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_slash_completer.py +0 -0
  173. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_slash_registry.py +0 -0
  174. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_startup_profile.py +0 -0
  175. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_status_bar_transient.py +0 -0
  176. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_task_panel_format.py +0 -0
  177. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_task_panel_key_bindings.py +0 -0
  178. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_task_panel_rendering.py +0 -0
  179. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_task_poll.py +0 -0
  180. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tool_fold.py +0 -0
  181. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tool_fold_panel.py +0 -0
  182. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tool_result_formatters.py +0 -0
  183. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tool_result_store.py +0 -0
  184. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tool_result_viewer.py +0 -0
  185. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tool_result_viewer_key_bindings.py +0 -0
  186. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tool_view.py +0 -0
  187. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_transcript_viewer.py +0 -0
  188. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_transcript_viewer_tool_fold.py +0 -0
  189. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_elapsed_status.py +0 -0
  190. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_esc_queue.py +0 -0
  191. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_mcp_init_gate.py +0 -0
  192. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_paste_newline_guard.py +0 -0
  193. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_paste_placeholder.py +0 -0
  194. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_queue_preview.py +0 -0
  195. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_queue_sdk_source.py +0 -0
  196. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_split_invariance.py +0 -0
  197. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_team_messages.py +0 -0
  198. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_thinking_display.py +0 -0
  199. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
  200. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_update_check.py +0 -0
  201. {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_usage_command.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comate-cli
3
- Version: 0.7.3
3
+ Version: 0.7.4
4
4
  Summary: Comate terminal CLI built on comate-agent-sdk
5
5
  Project-URL: Homepage, https://github.com/AndyLee1024/agent-sdk
6
6
  Project-URL: Repository, https://github.com/AndyLee1024/agent-sdk
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  import logging
4
5
  import subprocess
5
6
  import time
@@ -40,7 +41,7 @@ class StatusBar:
40
41
  self._session = session
41
42
  self._model_name: str = self._resolve_model_name(session)
42
43
  self._mode: str = "act"
43
- self._git_branch: str = self._resolve_git_branch()
44
+ self._git_branch: str = "N/A"
44
45
  self._context_used_pct: float = 0.0
45
46
  self._context_left_pct: float = 100.0
46
47
  self._git_diff_stats: GitDiffStats | None = None
@@ -274,8 +275,20 @@ class StatusBar:
274
275
  self._context_used_pct = normalized
275
276
  self._context_left_pct = max(0.0, 100.0 - normalized)
276
277
 
277
- self._git_branch = self._resolve_git_branch()
278
- self._ensure_git_diff_stats()
278
+ branch_task = asyncio.to_thread(self._resolve_git_branch)
279
+ now = time.monotonic()
280
+ diff_is_stale = (
281
+ self._git_diff_stats is None
282
+ or now - self._git_diff_cache_time >= self._GIT_DIFF_CACHE_SECONDS
283
+ )
284
+ if diff_is_stale:
285
+ diff_task = asyncio.to_thread(self._resolve_git_diff_stats)
286
+ branch, diff_stats = await asyncio.gather(branch_task, diff_task)
287
+ self._git_diff_stats = diff_stats
288
+ self._git_diff_cache_time = time.monotonic()
289
+ else:
290
+ branch = await branch_task
291
+ self._git_branch = branch
279
292
 
280
293
  # Refresh token usage cache if any token-related items are enabled.
281
294
  cfg = self._config
@@ -139,6 +139,16 @@ def _truncate_file_history(path: Path, max_entries: int = 200) -> None:
139
139
  path.write_bytes(rewritten_bytes)
140
140
 
141
141
 
142
+ def _load_plugins_blocking(project_path: Path) -> tuple[list[Any], list[Any]]:
143
+ from comate_agent_sdk.plugins import PluginLoader, create_plugin_registry
144
+
145
+ loader = PluginLoader()
146
+ registry = create_plugin_registry()
147
+ data_dir = registry.base_dir / "data"
148
+ data_dir.mkdir(parents=True, exist_ok=True)
149
+ return loader.load_all(registry=registry, project_path=project_path)
150
+
151
+
142
152
  class TerminalAgentTUI(
143
153
  KeyBindingsMixin,
144
154
  InputBehaviorMixin,
@@ -266,6 +276,7 @@ class TerminalAgentTUI(
266
276
  self._ui_tick_task: asyncio.Task[None] | None = None
267
277
  self._mcp_init_task: asyncio.Task[None] | None = None
268
278
  self._plugin_init_task: asyncio.Task[None] | None = None
279
+ self._startup_status_refresh_task: asyncio.Task[None] | None = None
269
280
  self._plugin_init_cancel_timeout_s = 1.0
270
281
  self._interrupt_requested_at: float | None = None
271
282
  self._interrupt_force_window_seconds = 1.5
@@ -1801,6 +1812,24 @@ class TerminalAgentTUI(
1801
1812
  self._ui_tick(),
1802
1813
  name="terminal-ui-tick",
1803
1814
  )
1815
+ status_bar = getattr(self, "_status_bar", None)
1816
+ if status_bar is not None:
1817
+ async def _do_startup_status_refresh() -> None:
1818
+ try:
1819
+ await status_bar.refresh()
1820
+ except asyncio.CancelledError:
1821
+ raise
1822
+ except Exception:
1823
+ logger.debug("startup status refresh failed", exc_info=True)
1824
+ finally:
1825
+ if not self._closing:
1826
+ self._refresh_layers()
1827
+ self._invalidate()
1828
+
1829
+ self._startup_status_refresh_task = asyncio.create_task(
1830
+ _do_startup_status_refresh(),
1831
+ name="terminal-startup-status-refresh",
1832
+ )
1804
1833
  if hasattr(self, "_session") and self._session is not None:
1805
1834
  self._event_pump_task = asyncio.create_task(
1806
1835
  self._consume_event_stream(),
@@ -1841,6 +1870,16 @@ class TerminalAgentTUI(
1841
1870
  self._ui_tick_task.cancel()
1842
1871
  with suppress(asyncio.CancelledError):
1843
1872
  await self._ui_tick_task
1873
+ startup_status_refresh_task = getattr(
1874
+ self,
1875
+ "_startup_status_refresh_task",
1876
+ None,
1877
+ )
1878
+ if startup_status_refresh_task is not None:
1879
+ startup_status_refresh_task.cancel()
1880
+ with suppress(asyncio.CancelledError):
1881
+ await startup_status_refresh_task
1882
+ self._startup_status_refresh_task = None
1844
1883
  event_pump_task = getattr(self, "_event_pump_task", None)
1845
1884
  if event_pump_task is not None:
1846
1885
  event_pump_task.cancel()
@@ -1848,19 +1887,25 @@ class TerminalAgentTUI(
1848
1887
  await event_pump_task
1849
1888
  self._renderer.close()
1850
1889
 
1890
+ def _plugin_project_path(self) -> Path:
1891
+ session = getattr(self, "_session", None)
1892
+ session_cwd = getattr(session, "_cwd", None)
1893
+ if session_cwd:
1894
+ return Path(session_cwd).expanduser().resolve()
1895
+ return Path.cwd().expanduser().resolve()
1896
+
1851
1897
  async def _init_plugins(self) -> None:
1852
1898
  """Load and inject plugin resources at startup."""
1853
- from comate_agent_sdk.plugins import create_plugin_registry, PluginLoader
1854
-
1855
- loader = PluginLoader()
1856
- registry = create_plugin_registry()
1857
- # Ensure plugin data directory exists
1858
- data_dir = registry.base_dir / "data"
1859
- data_dir.mkdir(parents=True, exist_ok=True)
1860
- self._loaded_plugins, self._plugin_errors = loader.load_all(
1861
- registry=registry,
1862
- project_path=Path.cwd(),
1899
+ loaded_plugins, plugin_errors = await asyncio.to_thread(
1900
+ _load_plugins_blocking,
1901
+ self._plugin_project_path(),
1863
1902
  )
1903
+ if self._closing:
1904
+ logger.debug("Plugin load completed after TUI close; discarding results")
1905
+ return
1906
+
1907
+ self._loaded_plugins = loaded_plugins
1908
+ self._plugin_errors = plugin_errors
1864
1909
  self._plugin_command_names: set[str] = set()
1865
1910
  self._inject_plugin_resources(self._loaded_plugins)
1866
1911
  logger.info(
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "comate-cli"
7
- version = "0.7.3"
7
+ version = "0.7.4"
8
8
  description = "Comate terminal CLI built on comate-agent-sdk"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,13 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  import subprocess
5
+ import time
4
6
  import unittest
5
7
  from pathlib import Path
6
8
  from types import SimpleNamespace
7
9
  from unittest.mock import patch
8
10
 
9
11
  from comate_agent_sdk.agent.goal_state import GoalSnapshot, GoalStatus
10
- from comate_cli.terminal_agent.status_bar import StatusBar
12
+ from comate_cli.terminal_agent.status_bar import GitDiffStats, StatusBar
11
13
  from comate_cli.terminal_agent.statusline.model import StatuslineConfig
12
14
 
13
15
 
@@ -74,15 +76,14 @@ def _goal_snapshot(
74
76
 
75
77
  class TestStatusBar(unittest.IsolatedAsyncioTestCase):
76
78
  @patch("comate_cli.terminal_agent.status_bar.subprocess.run")
77
- async def test_constructor_resolves_git_branch_immediately(self, mock_run) -> None:
78
- mock_run.return_value = SimpleNamespace(returncode=0, stdout="my-branch\n")
79
+ async def test_constructor_does_not_resolve_git_branch_immediately(self, mock_run) -> None:
79
80
  session = _FakeSession(utilization_percent=0.0)
80
81
 
81
82
  status_bar = StatusBar(session)
82
83
 
83
- # 构造时立即解析 git branch,不再等待首次 refresh
84
- self.assertEqual(status_bar._git_branch, "my-branch")
85
- self.assertIn("~my-branch", status_bar.footer_status_text())
84
+ mock_run.assert_not_called()
85
+ self.assertEqual(status_bar._git_branch, "N/A")
86
+ self.assertIn("~N/A", status_bar.footer_status_text())
86
87
 
87
88
  @patch("comate_cli.terminal_agent.status_bar.subprocess.run")
88
89
  async def test_refresh_updates_cached_context_percent(self, mock_run) -> None:
@@ -157,7 +158,6 @@ class TestStatusBar(unittest.IsolatedAsyncioTestCase):
157
158
  @patch("comate_cli.terminal_agent.status_bar.subprocess.run")
158
159
  async def test_render_uses_cached_git_diff_stats_without_new_subprocess(self, mock_run) -> None:
159
160
  mock_run.side_effect = [
160
- SimpleNamespace(returncode=0, stdout="main\n"),
161
161
  SimpleNamespace(returncode=0, stdout="main\n"),
162
162
  SimpleNamespace(
163
163
  returncode=0,
@@ -169,7 +169,7 @@ class TestStatusBar(unittest.IsolatedAsyncioTestCase):
169
169
  status_bar.apply_config(StatuslineConfig(show_git_diff=True))
170
170
 
171
171
  await status_bar.refresh()
172
- self.assertEqual(mock_run.call_count, 3)
172
+ self.assertEqual(mock_run.call_count, 2)
173
173
 
174
174
  mock_run.reset_mock()
175
175
  footer = _join_fragments(status_bar.footer_toolbar())
@@ -178,6 +178,77 @@ class TestStatusBar(unittest.IsolatedAsyncioTestCase):
178
178
  self.assertIn("-1", footer)
179
179
  mock_run.assert_not_called()
180
180
 
181
+ async def test_refresh_git_commands_do_not_block_event_loop(self) -> None:
182
+ session = _FakeSession(utilization_percent=10.0)
183
+ status_bar = StatusBar(session)
184
+ git_done = False
185
+ ticked_before_git_done = False
186
+
187
+ def slow_branch() -> str:
188
+ time.sleep(0.05)
189
+ return "main"
190
+
191
+ def slow_diff() -> GitDiffStats | None:
192
+ nonlocal git_done
193
+ time.sleep(0.05)
194
+ git_done = True
195
+ return GitDiffStats(added=3, removed=1)
196
+
197
+ ticked = asyncio.Event()
198
+
199
+ async def ticker() -> None:
200
+ nonlocal ticked_before_git_done
201
+ await asyncio.sleep(0)
202
+ ticked_before_git_done = not git_done
203
+ ticked.set()
204
+
205
+ with (
206
+ patch.object(StatusBar, "_resolve_git_branch", side_effect=slow_branch),
207
+ patch.object(StatusBar, "_resolve_git_diff_stats", side_effect=slow_diff),
208
+ ):
209
+ await asyncio.gather(status_bar.refresh(), ticker())
210
+
211
+ self.assertTrue(ticked.is_set())
212
+ self.assertTrue(ticked_before_git_done)
213
+ self.assertEqual(status_bar._git_branch, "main")
214
+ self.assertEqual(status_bar._git_diff_stats, GitDiffStats(added=3, removed=1))
215
+
216
+ async def test_refresh_does_not_recompute_git_diff_when_cache_is_fresh(self) -> None:
217
+ session = _FakeSession(utilization_percent=10.0)
218
+ status_bar = StatusBar(session)
219
+ status_bar._git_diff_stats = GitDiffStats(added=1, removed=0)
220
+ status_bar._git_diff_cache_time = time.monotonic()
221
+
222
+ with (
223
+ patch.object(StatusBar, "_resolve_git_branch", return_value="main"),
224
+ patch.object(StatusBar, "_resolve_git_diff_stats") as diff_mock,
225
+ ):
226
+ await status_bar.refresh()
227
+
228
+ diff_mock.assert_not_called()
229
+ self.assertEqual(status_bar._git_diff_stats, GitDiffStats(added=1, removed=0))
230
+
231
+ async def test_refresh_recomputes_git_diff_when_cache_is_stale(self) -> None:
232
+ session = _FakeSession(utilization_percent=10.0)
233
+ status_bar = StatusBar(session)
234
+ status_bar._git_diff_stats = GitDiffStats(added=1, removed=0)
235
+ status_bar._git_diff_cache_time = (
236
+ time.monotonic() - StatusBar._GIT_DIFF_CACHE_SECONDS - 1
237
+ )
238
+
239
+ with (
240
+ patch.object(StatusBar, "_resolve_git_branch", return_value="main"),
241
+ patch.object(
242
+ StatusBar,
243
+ "_resolve_git_diff_stats",
244
+ return_value=GitDiffStats(added=2, removed=1),
245
+ ) as diff_mock,
246
+ ):
247
+ await status_bar.refresh()
248
+
249
+ diff_mock.assert_called_once()
250
+ self.assertEqual(status_bar._git_diff_stats, GitDiffStats(added=2, removed=1))
251
+
181
252
  @patch("comate_cli.terminal_agent.status_bar.subprocess.run")
182
253
  async def test_goal_budget_item_uses_cached_snapshot(self, mock_run) -> None:
183
254
  mock_run.return_value = SimpleNamespace(returncode=0, stdout="main\n")
@@ -0,0 +1,274 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import threading
5
+ import unittest
6
+ from contextlib import contextmanager
7
+ from pathlib import Path
8
+ from types import SimpleNamespace
9
+ from typing import Any
10
+ from unittest.mock import patch
11
+
12
+ from comate_cli.terminal_agent.tui import TerminalAgentTUI
13
+
14
+
15
+ class _FakeRenderer:
16
+ def __init__(self) -> None:
17
+ self.closed = False
18
+
19
+ def close(self) -> None:
20
+ self.closed = True
21
+
22
+
23
+ class _FakeApp:
24
+ def __init__(self, *, wait_for: asyncio.Event | None = None) -> None:
25
+ self.entered = asyncio.Event()
26
+ self._wait_for = wait_for
27
+ self.invalidated = False
28
+
29
+ async def run_async(self) -> None:
30
+ self.entered.set()
31
+ if self._wait_for is not None:
32
+ await asyncio.wait_for(self._wait_for.wait(), timeout=0.1)
33
+ else:
34
+ await asyncio.sleep(0)
35
+
36
+ def invalidate(self) -> None:
37
+ self.invalidated = True
38
+
39
+
40
+ @contextmanager
41
+ def _noop_patch_stdout(*args: Any, **kwargs: Any):
42
+ del args, kwargs
43
+ yield
44
+
45
+
46
+ def _make_tui(app: _FakeApp) -> TerminalAgentTUI:
47
+ tui = TerminalAgentTUI.__new__(TerminalAgentTUI)
48
+ tui._app = app
49
+ tui._renderer = _FakeRenderer()
50
+ tui._refresh_layers = lambda: None
51
+ tui._busy = False
52
+ tui._closing = False
53
+ tui._mcp_init_cancel_timeout_s = 0.05
54
+ tui._mcp_init_task = None
55
+ tui._plugin_init_task = None
56
+ tui._plugin_init_cancel_timeout_s = 0.05
57
+ tui._ui_tick_task = None
58
+ tui._event_pump_task = None
59
+
60
+ async def _fake_ui_tick() -> None:
61
+ await asyncio.Event().wait()
62
+
63
+ async def _fake_event_pump() -> None:
64
+ await asyncio.Event().wait()
65
+
66
+ tui._ui_tick = _fake_ui_tick
67
+ tui._consume_event_stream = _fake_event_pump
68
+ return tui
69
+
70
+
71
+ class TestTUIStartupLatency(unittest.IsolatedAsyncioTestCase):
72
+ async def test_plugin_init_pending_does_not_block_run_async(self) -> None:
73
+ plugin_started = asyncio.Event()
74
+ plugin_cancelled = asyncio.Event()
75
+ app = _FakeApp(wait_for=plugin_started)
76
+ tui = _make_tui(app)
77
+
78
+ async def _stuck_plugin_init() -> None:
79
+ plugin_started.set()
80
+ try:
81
+ await asyncio.Event().wait()
82
+ finally:
83
+ plugin_cancelled.set()
84
+
85
+ tui._init_plugins = _stuck_plugin_init
86
+
87
+ with patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout):
88
+ await asyncio.wait_for(tui.run(), timeout=0.2)
89
+
90
+ self.assertTrue(plugin_started.is_set())
91
+ self.assertTrue(app.entered.is_set())
92
+ self.assertTrue(plugin_cancelled.is_set())
93
+ self.assertIsNone(tui._plugin_init_task)
94
+ self.assertTrue(tui._renderer.closed)
95
+
96
+ async def test_plugin_init_exception_is_logged_and_run_async_still_runs(self) -> None:
97
+ app = _FakeApp()
98
+ tui = _make_tui(app)
99
+
100
+ async def _failing_plugin_init() -> None:
101
+ raise RuntimeError("plugin boom")
102
+
103
+ tui._init_plugins = _failing_plugin_init
104
+
105
+ with (
106
+ patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout),
107
+ self.assertLogs("comate_cli.terminal_agent.tui", level="ERROR") as logs,
108
+ ):
109
+ await asyncio.wait_for(tui.run(), timeout=0.2)
110
+
111
+ self.assertTrue(app.entered.is_set())
112
+ self.assertIn("Plugin init failed at startup", "\n".join(logs.output))
113
+
114
+ async def test_sync_plugin_loader_runs_off_event_loop(self) -> None:
115
+ load_started = threading.Event()
116
+ release_load = threading.Event()
117
+ app_release = asyncio.Event()
118
+ app = _FakeApp(wait_for=app_release)
119
+ tui = _make_tui(app)
120
+ tui._inject_plugin_resources = lambda plugins: None
121
+
122
+ def slow_load_all(*args, **kwargs): # noqa: ANN001, ANN202
123
+ del args, kwargs
124
+ load_started.set()
125
+ release_load.wait(timeout=1.0)
126
+ return [], []
127
+
128
+ fake_registry = SimpleNamespace(base_dir=Path("/tmp/plugin-registry"))
129
+
130
+ with (
131
+ patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout),
132
+ patch("comate_agent_sdk.plugins.create_plugin_registry", return_value=fake_registry),
133
+ patch("comate_agent_sdk.plugins.PluginLoader.load_all", side_effect=slow_load_all),
134
+ ):
135
+ run_task = asyncio.create_task(tui.run())
136
+ self.assertTrue(await asyncio.to_thread(load_started.wait, 0.2))
137
+ await asyncio.wait_for(app.entered.wait(), timeout=0.1)
138
+ self.assertFalse(run_task.done())
139
+ release_load.set()
140
+ app_release.set()
141
+ await asyncio.wait_for(run_task, timeout=0.5)
142
+
143
+ async def test_plugin_loader_uses_session_cwd_as_project_path(self) -> None:
144
+ app_release = asyncio.Event()
145
+ app = _FakeApp(wait_for=app_release)
146
+ tui = _make_tui(app)
147
+ tui._session = SimpleNamespace(_cwd="/tmp/session-project")
148
+ tui._inject_plugin_resources = lambda plugins: None
149
+ seen_project_paths: list[Path] = []
150
+ load_called = threading.Event()
151
+
152
+ def load_all(*, registry, project_path, **kwargs): # noqa: ANN001, ANN202
153
+ del registry, kwargs
154
+ seen_project_paths.append(Path(project_path))
155
+ load_called.set()
156
+ return [], []
157
+
158
+ fake_registry = SimpleNamespace(base_dir=Path("/tmp/plugin-registry"))
159
+
160
+ with (
161
+ patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout),
162
+ patch("comate_agent_sdk.plugins.create_plugin_registry", return_value=fake_registry),
163
+ patch("comate_agent_sdk.plugins.PluginLoader.load_all", side_effect=load_all),
164
+ ):
165
+ run_task = asyncio.create_task(tui.run())
166
+ self.assertTrue(await asyncio.to_thread(load_called.wait, 0.2))
167
+ app_release.set()
168
+ await asyncio.wait_for(run_task, timeout=0.5)
169
+
170
+ self.assertEqual(seen_project_paths, [Path("/tmp/session-project").resolve()])
171
+
172
+ async def test_cancelled_plugin_loader_does_not_inject_after_close(self) -> None:
173
+ load_started = threading.Event()
174
+ release_load = threading.Event()
175
+ app_release = asyncio.Event()
176
+ app = _FakeApp(wait_for=app_release)
177
+ tui = _make_tui(app)
178
+ injected: list[object] = []
179
+ tui._inject_plugin_resources = lambda plugins: injected.extend(plugins)
180
+
181
+ loaded_plugin = SimpleNamespace(
182
+ namespace="slow",
183
+ skills=[],
184
+ agents=[],
185
+ commands=[],
186
+ mcp_servers={},
187
+ )
188
+
189
+ def slow_load_all(*args, **kwargs): # noqa: ANN001, ANN202
190
+ del args, kwargs
191
+ load_started.set()
192
+ release_load.wait(timeout=1.0)
193
+ return [loaded_plugin], []
194
+
195
+ fake_registry = SimpleNamespace(base_dir=Path("/tmp/plugin-registry"))
196
+
197
+ with (
198
+ patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout),
199
+ patch("comate_agent_sdk.plugins.create_plugin_registry", return_value=fake_registry),
200
+ patch("comate_agent_sdk.plugins.PluginLoader.load_all", side_effect=slow_load_all),
201
+ ):
202
+ run_task = asyncio.create_task(tui.run())
203
+ self.assertTrue(await asyncio.to_thread(load_started.wait, 0.2))
204
+ await asyncio.wait_for(app.entered.wait(), timeout=0.1)
205
+ app_release.set()
206
+ await asyncio.sleep(0)
207
+ release_load.set()
208
+ await asyncio.wait_for(run_task, timeout=0.5)
209
+
210
+ self.assertEqual(injected, [])
211
+
212
+ async def test_startup_status_refresh_does_not_block_run_async(self) -> None:
213
+ refresh_started = asyncio.Event()
214
+ release_refresh = asyncio.Event()
215
+ app_release = asyncio.Event()
216
+ app = _FakeApp(wait_for=app_release)
217
+ tui = _make_tui(app)
218
+ tui._init_plugins = lambda: asyncio.sleep(0)
219
+
220
+ class _StatusBar:
221
+ def __init__(self) -> None:
222
+ self.refreshed = False
223
+
224
+ async def refresh(self) -> None:
225
+ refresh_started.set()
226
+ await release_refresh.wait()
227
+ self.refreshed = True
228
+
229
+ status_bar = _StatusBar()
230
+ tui._status_bar = status_bar
231
+
232
+ with patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout):
233
+ run_task = asyncio.create_task(tui.run())
234
+ await asyncio.wait_for(app.entered.wait(), timeout=0.1)
235
+ await asyncio.wait_for(refresh_started.wait(), timeout=0.1)
236
+ self.assertFalse(run_task.done())
237
+ release_refresh.set()
238
+ app_release.set()
239
+ await asyncio.wait_for(run_task, timeout=0.5)
240
+
241
+ self.assertTrue(status_bar.refreshed)
242
+ self.assertTrue(app.invalidated)
243
+
244
+ async def test_pending_startup_status_refresh_is_cancelled_on_shutdown(self) -> None:
245
+ refresh_started = asyncio.Event()
246
+ refresh_cancelled = asyncio.Event()
247
+ app_release = asyncio.Event()
248
+ app = _FakeApp(wait_for=app_release)
249
+ tui = _make_tui(app)
250
+ tui._init_plugins = lambda: asyncio.sleep(0)
251
+
252
+ class _StatusBar:
253
+ async def refresh(self) -> None:
254
+ refresh_started.set()
255
+ try:
256
+ await asyncio.Event().wait()
257
+ finally:
258
+ refresh_cancelled.set()
259
+
260
+ tui._status_bar = _StatusBar()
261
+
262
+ with patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout):
263
+ run_task = asyncio.create_task(tui.run())
264
+ await asyncio.wait_for(app.entered.wait(), timeout=0.1)
265
+ await asyncio.wait_for(refresh_started.wait(), timeout=0.1)
266
+ app_release.set()
267
+ await asyncio.wait_for(run_task, timeout=0.5)
268
+
269
+ self.assertTrue(refresh_cancelled.is_set())
270
+ self.assertIsNone(tui._startup_status_refresh_task)
271
+
272
+
273
+ if __name__ == "__main__":
274
+ unittest.main(verbosity=2)
@@ -389,7 +389,7 @@ wheels = [
389
389
 
390
390
  [[package]]
391
391
  name = "comate-agent-sdk"
392
- version = "0.8.3"
392
+ version = "0.8.4"
393
393
  source = { editable = "../../" }
394
394
  dependencies = [
395
395
  { name = "aiohttp" },
@@ -463,7 +463,7 @@ dev = [
463
463
 
464
464
  [[package]]
465
465
  name = "comate-cli"
466
- version = "0.7.2"
466
+ version = "0.7.3"
467
467
  source = { editable = "." }
468
468
  dependencies = [
469
469
  { name = "charset-normalizer" },
@@ -1,104 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- import unittest
5
- from contextlib import contextmanager
6
- from typing import Any
7
- from unittest.mock import patch
8
-
9
- from comate_cli.terminal_agent.tui import TerminalAgentTUI
10
-
11
-
12
- class _FakeRenderer:
13
- def __init__(self) -> None:
14
- self.closed = False
15
-
16
- def close(self) -> None:
17
- self.closed = True
18
-
19
-
20
- class _FakeApp:
21
- def __init__(self, *, wait_for: asyncio.Event | None = None) -> None:
22
- self.entered = asyncio.Event()
23
- self._wait_for = wait_for
24
-
25
- async def run_async(self) -> None:
26
- self.entered.set()
27
- if self._wait_for is not None:
28
- await asyncio.wait_for(self._wait_for.wait(), timeout=0.1)
29
- else:
30
- await asyncio.sleep(0)
31
-
32
-
33
- @contextmanager
34
- def _noop_patch_stdout(*args: Any, **kwargs: Any):
35
- del args, kwargs
36
- yield
37
-
38
-
39
- def _make_tui(app: _FakeApp) -> TerminalAgentTUI:
40
- tui = TerminalAgentTUI.__new__(TerminalAgentTUI)
41
- tui._app = app
42
- tui._renderer = _FakeRenderer()
43
- tui._refresh_layers = lambda: None
44
- tui._busy = False
45
- tui._closing = False
46
- tui._mcp_init_cancel_timeout_s = 0.05
47
- tui._mcp_init_task = None
48
- tui._plugin_init_task = None
49
- tui._plugin_init_cancel_timeout_s = 0.05
50
- tui._ui_tick_task = None
51
-
52
- async def _fake_ui_tick() -> None:
53
- await asyncio.Event().wait()
54
-
55
- tui._ui_tick = _fake_ui_tick
56
- return tui
57
-
58
-
59
- class TestTUIStartupLatency(unittest.IsolatedAsyncioTestCase):
60
- async def test_plugin_init_pending_does_not_block_run_async(self) -> None:
61
- plugin_started = asyncio.Event()
62
- plugin_cancelled = asyncio.Event()
63
- app = _FakeApp(wait_for=plugin_started)
64
- tui = _make_tui(app)
65
-
66
- async def _stuck_plugin_init() -> None:
67
- plugin_started.set()
68
- try:
69
- await asyncio.Event().wait()
70
- finally:
71
- plugin_cancelled.set()
72
-
73
- tui._init_plugins = _stuck_plugin_init
74
-
75
- with patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout):
76
- await asyncio.wait_for(tui.run(), timeout=0.2)
77
-
78
- self.assertTrue(plugin_started.is_set())
79
- self.assertTrue(app.entered.is_set())
80
- self.assertTrue(plugin_cancelled.is_set())
81
- self.assertIsNone(tui._plugin_init_task)
82
- self.assertTrue(tui._renderer.closed)
83
-
84
- async def test_plugin_init_exception_is_logged_and_run_async_still_runs(self) -> None:
85
- app = _FakeApp()
86
- tui = _make_tui(app)
87
-
88
- async def _failing_plugin_init() -> None:
89
- raise RuntimeError("plugin boom")
90
-
91
- tui._init_plugins = _failing_plugin_init
92
-
93
- with (
94
- patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout),
95
- self.assertLogs("comate_cli.terminal_agent.tui", level="ERROR") as logs,
96
- ):
97
- await asyncio.wait_for(tui.run(), timeout=0.2)
98
-
99
- self.assertTrue(app.entered.is_set())
100
- self.assertIn("Plugin init failed at startup", "\n".join(logs.output))
101
-
102
-
103
- if __name__ == "__main__":
104
- unittest.main(verbosity=2)
File without changes
File without changes