comate-cli 0.7.4__tar.gz → 0.7.5__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 (200) hide show
  1. {comate_cli-0.7.4 → comate_cli-0.7.5}/PKG-INFO +2 -2
  2. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/app.py +98 -27
  3. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/status_bar.py +30 -10
  4. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tui.py +20 -2
  5. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/update_check.py +23 -0
  6. {comate_cli-0.7.4 → comate_cli-0.7.5}/pyproject.toml +2 -2
  7. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_app_startup_latency.py +9 -3
  8. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_completion_status_panel.py +1 -1
  9. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_startup_profile.py +124 -4
  10. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_status_bar.py +18 -4
  11. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tui_startup_latency.py +4 -2
  12. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_update_check.py +46 -0
  13. {comate_cli-0.7.4 → comate_cli-0.7.5}/.gitignore +0 -0
  14. {comate_cli-0.7.4 → comate_cli-0.7.5}/CHANGELOG.md +0 -0
  15. {comate_cli-0.7.4 → comate_cli-0.7.5}/README.md +0 -0
  16. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/__init__.py +0 -0
  17. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/__main__.py +0 -0
  18. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/main.py +0 -0
  19. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/mcp_cli.py +0 -0
  20. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/__init__.py +0 -0
  21. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/animations.py +0 -0
  22. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/assistant_render.py +0 -0
  23. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/codenames.py +0 -0
  24. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/config/__init__.py +0 -0
  25. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/config/model.py +0 -0
  26. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/config/picker.py +0 -0
  27. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/config/picker_state.py +0 -0
  28. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/config/store.py +0 -0
  29. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  30. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/env_utils.py +0 -0
  31. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/error_display.py +0 -0
  32. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/event_renderer.py +0 -0
  33. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/figures.py +0 -0
  34. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  35. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/goal_resume_view.py +0 -0
  36. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/history_printer.py +0 -0
  37. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/input_geometry.py +0 -0
  38. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  39. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/logging_adapter.py +0 -0
  40. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/logo.py +0 -0
  41. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/markdown_render.py +0 -0
  42. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/mention_completer.py +0 -0
  43. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/message_style.py +0 -0
  44. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/models.py +0 -0
  45. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/path_context_hint.py +0 -0
  46. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
  47. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
  48. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
  49. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
  50. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
  51. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
  52. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
  53. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
  54. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
  55. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
  56. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
  57. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
  58. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
  59. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/preflight.py +0 -0
  60. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/question_view.py +0 -0
  61. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/resume_picker.py +0 -0
  62. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/resume_preview.py +0 -0
  63. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/resume_selector.py +0 -0
  64. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  65. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  66. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/selection_menu.py +0 -0
  67. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/slash_commands.py +0 -0
  68. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/startup.py +0 -0
  69. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/startup_profile.py +0 -0
  70. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/statusline/__init__.py +0 -0
  71. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/statusline/model.py +0 -0
  72. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/statusline/picker.py +0 -0
  73. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/statusline/picker_state.py +0 -0
  74. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/statusline/store.py +0 -0
  75. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/text_effects.py +0 -0
  76. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tips.py +0 -0
  77. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tool_fold.py +0 -0
  78. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tool_result_formatters.py +0 -0
  79. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tool_result_store.py +0 -0
  80. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
  81. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tool_view.py +0 -0
  82. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
  83. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  84. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
  85. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
  86. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
  87. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
  88. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
  89. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
  90. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
  91. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  92. {comate_cli-0.7.4 → comate_cli-0.7.5}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  93. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/config/__init__.py +0 -0
  94. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/config/test_model.py +0 -0
  95. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/config/test_picker_state.py +0 -0
  96. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/config/test_picker_ui.py +0 -0
  97. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/config/test_roundtrip.py +0 -0
  98. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/config/test_store_load.py +0 -0
  99. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/config/test_store_save.py +0 -0
  100. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/conftest.py +0 -0
  101. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/fixtures/fake_mcp_misbehaving_stdout.py +0 -0
  102. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/statusline/__init__.py +0 -0
  103. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/statusline/test_model.py +0 -0
  104. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/statusline/test_picker_state.py +0 -0
  105. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/statusline/test_store.py +0 -0
  106. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_animator_shuffle.py +0 -0
  107. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_app_mcp_preload.py +0 -0
  108. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_app_preflight_gate.py +0 -0
  109. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_app_print_mode.py +0 -0
  110. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_app_shutdown.py +0 -0
  111. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_app_token_cost_config.py +0 -0
  112. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_app_usage_line.py +0 -0
  113. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_btw_slash_command.py +0 -0
  114. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_cli_project_root.py +0 -0
  115. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_compact_command_semantics.py +0 -0
  116. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_completion_context_activation.py +0 -0
  117. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_context_command.py +0 -0
  118. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_custom_slash_commands.py +0 -0
  119. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_discover_tab.py +0 -0
  120. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_errors_tab.py +0 -0
  121. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_event_renderer.py +0 -0
  122. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_event_renderer_boundary.py +0 -0
  123. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_event_renderer_e2e.py +0 -0
  124. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_event_renderer_log_boundary.py +0 -0
  125. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_event_renderer_log_queue.py +0 -0
  126. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_event_renderer_streaming.py +0 -0
  127. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_event_renderer_tool_fold.py +0 -0
  128. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_format_error.py +0 -0
  129. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_goal_resume_tui.py +0 -0
  130. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_goal_resume_view.py +0 -0
  131. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_goal_slash_command.py +0 -0
  132. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_handle_error.py +0 -0
  133. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_history_printer.py +0 -0
  134. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_history_printer_log.py +0 -0
  135. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_history_printer_subtitle_position.py +0 -0
  136. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_history_printer_tool_fold.py +0 -0
  137. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_history_sync.py +0 -0
  138. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_history_sync_tool_fold.py +0 -0
  139. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_input_behavior.py +0 -0
  140. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_input_history.py +0 -0
  141. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_installed_tab.py +0 -0
  142. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_interrupt_exit_semantics.py +0 -0
  143. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_layout_coordinator.py +0 -0
  144. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_logging_adapter.py +0 -0
  145. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_logo.py +0 -0
  146. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_main_args.py +0 -0
  147. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_markdown_render.py +0 -0
  148. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_marketplaces_tab.py +0 -0
  149. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_mcp_cli.py +0 -0
  150. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_mcp_slash_command.py +0 -0
  151. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_mention_completer.py +0 -0
  152. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_path_context_hint.py +0 -0
  153. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_plugin_slash_commands.py +0 -0
  154. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_plugin_tui_components.py +0 -0
  155. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_preflight.py +0 -0
  156. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_preflight_copilot.py +0 -0
  157. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_question_key_bindings.py +0 -0
  158. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_question_view.py +0 -0
  159. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_resume_picker.py +0 -0
  160. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_resume_preview.py +0 -0
  161. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_resume_selector.py +0 -0
  162. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_rewind_command_semantics.py +0 -0
  163. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_rpc_protocol.py +0 -0
  164. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_rpc_stdio_bridge.py +0 -0
  165. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_selection_menu.py +0 -0
  166. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_session_query_token_summary.py +0 -0
  167. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_shutdown_noise_guard.py +0 -0
  168. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_shutdown_noise_integration.py +0 -0
  169. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_skills_slash_command.py +0 -0
  170. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_slash_argument_hint.py +0 -0
  171. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_slash_clear.py +0 -0
  172. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_slash_completer.py +0 -0
  173. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_slash_registry.py +0 -0
  174. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_status_bar_transient.py +0 -0
  175. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_task_panel_format.py +0 -0
  176. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_task_panel_key_bindings.py +0 -0
  177. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_task_panel_rendering.py +0 -0
  178. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_task_poll.py +0 -0
  179. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tool_fold.py +0 -0
  180. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tool_fold_panel.py +0 -0
  181. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tool_result_formatters.py +0 -0
  182. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tool_result_store.py +0 -0
  183. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tool_result_viewer.py +0 -0
  184. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tool_result_viewer_key_bindings.py +0 -0
  185. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tool_view.py +0 -0
  186. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_transcript_viewer.py +0 -0
  187. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_transcript_viewer_tool_fold.py +0 -0
  188. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tui_elapsed_status.py +0 -0
  189. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tui_esc_queue.py +0 -0
  190. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tui_mcp_init_gate.py +0 -0
  191. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tui_paste_newline_guard.py +0 -0
  192. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tui_paste_placeholder.py +0 -0
  193. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tui_queue_preview.py +0 -0
  194. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tui_queue_sdk_source.py +0 -0
  195. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tui_split_invariance.py +0 -0
  196. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tui_team_messages.py +0 -0
  197. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tui_thinking_display.py +0 -0
  198. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
  199. {comate_cli-0.7.4 → comate_cli-0.7.5}/tests/test_usage_command.py +0 -0
  200. {comate_cli-0.7.4 → comate_cli-0.7.5}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comate-cli
3
- Version: 0.7.4
3
+ Version: 0.7.5
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
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
- Requires-Python: >=3.11
16
+ Requires-Python: <3.14,>=3.11
17
17
  Requires-Dist: charset-normalizer==3.4.7
18
18
  Requires-Dist: comate-agent-sdk<0.9.0,>=0.8.0a1
19
19
  Requires-Dist: concurrent-log-handler>=0.9.25
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import logging
5
5
  import os
6
- import random
6
+ import random # noqa: F401 - tests patch this module-level import for deterministic startup tips.
7
7
  import signal
8
8
  import sys
9
9
  import threading
@@ -49,8 +49,14 @@ def _resolve_cli_project_root() -> Path:
49
49
  return Path.cwd().expanduser().resolve()
50
50
 
51
51
 
52
- async def _check_update() -> UpdateInfo | None:
53
- return await check_update(log=logger)
52
+ async def _check_update(*, profiler: StartupProfiler | None = None) -> UpdateInfo | None:
53
+ if profiler is not None:
54
+ profiler.mark("impl.start")
55
+ try:
56
+ return await check_update(log=logger, profiler=profiler)
57
+ finally:
58
+ if profiler is not None:
59
+ profiler.mark("impl.done")
54
60
 
55
61
 
56
62
  async def _handle_update_on_launch(info: UpdateInfo) -> bool:
@@ -109,7 +115,7 @@ def _schedule_update_check_on_launch(
109
115
  async def _run() -> None:
110
116
  try:
111
117
  profiler.mark("update_check.start")
112
- update_info = await _check_update()
118
+ update_info = await _check_update(profiler=profiler.child("update_check"))
113
119
  profiler.mark("update_check.done")
114
120
  if update_info is not None:
115
121
  await _handle_background_update_on_launch(
@@ -228,35 +234,64 @@ async def add(a: int, b: int) -> int:
228
234
  return a + b
229
235
 
230
236
 
231
- def _build_agent(*, project_root: Path | None = None) -> Agent:
237
+ def _build_agent(
238
+ *,
239
+ project_root: Path | None = None,
240
+ profiler: StartupProfiler | None = None,
241
+ ) -> Agent:
232
242
  from comate_agent_sdk.agent.compaction import CompactionConfig
233
243
  from comate_cli.terminal_agent.config import store
234
244
 
235
245
  resolved_project_root = project_root or _resolve_cli_project_root()
246
+ if profiler is not None:
247
+ profiler.mark("agent.config.load.start")
236
248
  snapshot = store.load()
237
-
238
- return Agent(
239
- config=AgentConfig(
240
- role="software_engineering",
241
- cwd=resolved_project_root,
242
- env_options=EnvOptions(system_env=True, git_env=True),
243
- use_streaming_task=True,
244
- include_cost=bool(getattr(snapshot, "token_cost_enabled", False)),
245
- memory_background_enabled=snapshot.memory_background_enabled,
246
- memory_dreaming_enabled=snapshot.memory_dreaming_enabled,
247
- compaction=CompactionConfig(
248
- threshold_ratio=snapshot.compaction_threshold_ratio,
249
- ),
250
- )
249
+ if profiler is not None:
250
+ profiler.mark("agent.config.load.done")
251
+
252
+ agent_config = AgentConfig(
253
+ role="software_engineering",
254
+ cwd=resolved_project_root,
255
+ env_options=EnvOptions(system_env=True, git_env=True),
256
+ use_streaming_task=True,
257
+ include_cost=bool(getattr(snapshot, "token_cost_enabled", False)),
258
+ memory_background_enabled=snapshot.memory_background_enabled,
259
+ memory_dreaming_enabled=snapshot.memory_dreaming_enabled,
260
+ compaction=CompactionConfig(
261
+ threshold_ratio=snapshot.compaction_threshold_ratio,
262
+ ),
251
263
  )
264
+ if profiler is not None:
265
+ profiler.mark("agent.construct.start")
266
+ try:
267
+ return Agent(config=agent_config)
268
+ finally:
269
+ if profiler is not None:
270
+ profiler.mark("agent.construct.done")
252
271
 
253
272
 
254
273
  def _resolve_session(
255
- agent: Agent, resume_session_id: str | None, *, cwd: Path | None = None
274
+ agent: Agent,
275
+ resume_session_id: str | None,
276
+ *,
277
+ cwd: Path | None = None,
278
+ profiler: StartupProfiler | None = None,
256
279
  ) -> tuple[ChatSession, str]:
257
280
  if resume_session_id:
258
- return ChatSession.resume(agent, session_id=resume_session_id, cwd=cwd), "resume"
259
- return ChatSession(agent, cwd=cwd), "new"
281
+ if profiler is not None:
282
+ profiler.mark("session.resume.construct.start")
283
+ try:
284
+ return ChatSession.resume(agent, session_id=resume_session_id, cwd=cwd), "resume"
285
+ finally:
286
+ if profiler is not None:
287
+ profiler.mark("session.resume.construct.done")
288
+ if profiler is not None:
289
+ profiler.mark("session.new.construct.start")
290
+ try:
291
+ return ChatSession(agent, cwd=cwd), "new"
292
+ finally:
293
+ if profiler is not None:
294
+ profiler.mark("session.new.construct.done")
260
295
 
261
296
 
262
297
  def _format_exit_usage_line(usage: object) -> str:
@@ -287,23 +322,47 @@ def _format_resume_hint(session_id: str | None) -> str | None:
287
322
  )
288
323
 
289
324
 
290
- async def _preload_mcp_in_tui(session: ChatSession) -> None:
325
+ async def _preload_mcp_in_tui(
326
+ session: ChatSession,
327
+ *,
328
+ profiler: StartupProfiler | None = None,
329
+ ) -> None:
291
330
  """在 TUI 内异步加载 MCP,初始化阶段不输出 scrollback 文案。"""
331
+ if profiler is not None:
332
+ profiler.mark("preload.start")
292
333
  runtime = session.runtime
293
334
  if not bool(runtime.config.mcp_enabled):
335
+ if profiler is not None:
336
+ profiler.mark("preload.skip_disabled")
294
337
  return
295
338
 
296
339
  try:
340
+ if profiler is not None:
341
+ profiler.mark("start_preload.start")
297
342
  preload_task = runtime.start_mcp_preload()
343
+ if profiler is not None:
344
+ profiler.mark("start_preload.done")
298
345
  if preload_task is None:
346
+ if profiler is not None:
347
+ profiler.mark("preload.no_task")
299
348
  return
349
+ if profiler is not None:
350
+ profiler.mark("await_preload.start")
300
351
  await preload_task
352
+ if profiler is not None:
353
+ profiler.mark("await_preload.done")
301
354
  except Exception as e:
302
355
  logger.debug(f"MCP init failed: {e}", exc_info=True)
356
+ if profiler is not None:
357
+ profiler.mark("preload.failed")
303
358
  return
304
359
 
360
+ if profiler is not None:
361
+ profiler.mark("inspect_manager.start")
305
362
  mgr = runtime._mcp_manager
306
363
  if mgr is None:
364
+ if profiler is not None:
365
+ profiler.mark("inspect_manager.no_manager")
307
366
  return
308
367
 
309
368
  for alias, reason in mgr.failed_servers:
@@ -314,6 +373,8 @@ async def _preload_mcp_in_tui(session: ChatSession) -> None:
314
373
  count = len(loaded)
315
374
  aliases = sorted({i.server_alias for i in loaded})
316
375
  logger.info(f"MCP Server loaded: {', '.join(aliases)} ({count} tools)")
376
+ if profiler is not None:
377
+ profiler.mark("inspect_manager.done")
317
378
 
318
379
 
319
380
  async def _run_print_mode(
@@ -404,7 +465,7 @@ async def run(
404
465
  return
405
466
 
406
467
  profiler.mark("agent.build.start")
407
- agent = _build_agent(project_root=project_root)
468
+ agent = _build_agent(project_root=project_root, profiler=profiler)
408
469
  profiler.mark("agent.build.done")
409
470
 
410
471
  if rpc_stdio and resume_select and not resume_session_id:
@@ -412,7 +473,12 @@ async def run(
412
473
  return
413
474
 
414
475
  if rpc_stdio:
415
- session, _mode = _resolve_session(agent, resume_session_id, cwd=project_root)
476
+ session, _mode = _resolve_session(
477
+ agent,
478
+ resume_session_id,
479
+ cwd=project_root,
480
+ profiler=profiler,
481
+ )
416
482
  bridge = StdioRPCBridge(session)
417
483
  try:
418
484
  await bridge.run()
@@ -441,7 +507,12 @@ async def run(
441
507
  profiler.mark("logging.setup.done")
442
508
 
443
509
  profiler.mark("session.resolve.start")
444
- session, mode = _resolve_session(agent, resume_session_id, cwd=project_root)
510
+ session, mode = _resolve_session(
511
+ agent,
512
+ resume_session_id,
513
+ cwd=project_root,
514
+ profiler=profiler,
515
+ )
445
516
  profiler.mark("session.resolve.done")
446
517
  # setup_tui_logging 在 _resolve_session 前安装,用于捕获 session 初始化期 warning/error。
447
518
  # 这些日志没有交互 anchor,需在恢复历史或首次用户输入前落为 standalone log。
@@ -469,7 +540,7 @@ async def run(
469
540
  )
470
541
 
471
542
  async def _mcp_loader() -> None:
472
- await _preload_mcp_in_tui(session)
543
+ await _preload_mcp_in_tui(session, profiler=profiler.child("mcp"))
473
544
  mgr = session.runtime._mcp_manager
474
545
  if mgr and mgr.failed_servers:
475
546
  aliases = [alias for alias, _ in mgr.failed_servers]
@@ -206,6 +206,25 @@ class StatusBar:
206
206
  turn = getattr(agent, "_turn_number", None)
207
207
  return f"#{turn}" if turn is not None else "-"
208
208
 
209
+ def _context_percent_refresh_started(self) -> bool:
210
+ for owner in (
211
+ self._session,
212
+ getattr(self._session, "_agent", None),
213
+ getattr(getattr(self._session, "_agent", None), "_context", None),
214
+ ):
215
+ if owner is None:
216
+ continue
217
+ turn = getattr(owner, "_turn_number", None)
218
+ if turn is None:
219
+ turn = getattr(owner, "turn_number", None)
220
+ if turn is None:
221
+ continue
222
+ try:
223
+ return int(turn) > 0
224
+ except (TypeError, ValueError):
225
+ continue
226
+ return True
227
+
209
228
  def _resolve_project_cwd(self) -> str:
210
229
  cwd = getattr(self._session, "_cwd", None)
211
230
  if cwd is None:
@@ -260,21 +279,22 @@ class StatusBar:
260
279
  return " ".join(parts) if parts else "-"
261
280
 
262
281
  async def refresh(self) -> None:
263
- try:
264
- ctx_info = await self._session.get_context_info()
265
- utilization = float(getattr(ctx_info, "utilization_percent", 0.0))
266
- except Exception:
267
- return
282
+ if self._context_percent_refresh_started():
283
+ try:
284
+ ctx_info = await self._session.get_context_info()
285
+ utilization = float(getattr(ctx_info, "utilization_percent", 0.0))
286
+ except Exception:
287
+ return
288
+
289
+ normalized = max(0.0, min(utilization, 100.0))
290
+ self._context_used_pct = normalized
291
+ self._context_left_pct = max(0.0, 100.0 - normalized)
268
292
 
269
293
  try:
270
294
  self._mode = str(self._session.get_mode()).strip().lower() or "act"
271
295
  except Exception:
272
296
  pass
273
297
 
274
- normalized = max(0.0, min(utilization, 100.0))
275
- self._context_used_pct = normalized
276
- self._context_left_pct = max(0.0, 100.0 - normalized)
277
-
278
298
  branch_task = asyncio.to_thread(self._resolve_git_branch)
279
299
  now = time.monotonic()
280
300
  diff_is_stale = (
@@ -338,7 +358,7 @@ class StatusBar:
338
358
  return max(24, min(width - 6, 72))
339
359
 
340
360
  def context_left_text(self) -> str:
341
- return f"{self._context_left_pct:.0f}% left"
361
+ return f"Context {self._context_left_pct:.0f}% left"
342
362
 
343
363
  def _status_text_for_width(self, width: int) -> str:
344
364
  mode_text = f"[{self._mode}]"
@@ -1770,7 +1770,7 @@ class TerminalAgentTUI(
1770
1770
  try:
1771
1771
  if profiler is not None:
1772
1772
  profiler.mark("plugins.init.start")
1773
- await self._init_plugins()
1773
+ await self._init_plugins(profiler=profiler.child("plugins") if profiler is not None else None)
1774
1774
  if profiler is not None:
1775
1775
  profiler.mark("plugins.init.done")
1776
1776
  except asyncio.CancelledError:
@@ -1784,6 +1784,8 @@ class TerminalAgentTUI(
1784
1784
  finally:
1785
1785
  self._refresh_layers()
1786
1786
 
1787
+ if profiler is not None:
1788
+ profiler.mark("plugins.task.create")
1787
1789
  self._plugin_init_task = asyncio.create_task(
1788
1790
  _do_plugin_init(),
1789
1791
  name="terminal-plugin-init",
@@ -1803,11 +1805,15 @@ class TerminalAgentTUI(
1803
1805
  finally:
1804
1806
  self._refresh_layers()
1805
1807
 
1808
+ if profiler is not None:
1809
+ profiler.mark("mcp.task.create")
1806
1810
  self._mcp_init_task = asyncio.create_task(
1807
1811
  _do_init(),
1808
1812
  name="terminal-mcp-init",
1809
1813
  )
1810
1814
 
1815
+ if profiler is not None:
1816
+ profiler.mark("ui_tick.task.create")
1811
1817
  self._ui_tick_task = asyncio.create_task(
1812
1818
  self._ui_tick(),
1813
1819
  name="terminal-ui-tick",
@@ -1826,11 +1832,15 @@ class TerminalAgentTUI(
1826
1832
  self._refresh_layers()
1827
1833
  self._invalidate()
1828
1834
 
1835
+ if profiler is not None:
1836
+ profiler.mark("status_bar.refresh.task.create")
1829
1837
  self._startup_status_refresh_task = asyncio.create_task(
1830
1838
  _do_startup_status_refresh(),
1831
1839
  name="terminal-startup-status-refresh",
1832
1840
  )
1833
1841
  if hasattr(self, "_session") and self._session is not None:
1842
+ if profiler is not None:
1843
+ profiler.mark("event_pump.task.create")
1834
1844
  self._event_pump_task = asyncio.create_task(
1835
1845
  self._consume_event_stream(),
1836
1846
  name="terminal-session-event-pump",
@@ -1894,12 +1904,16 @@ class TerminalAgentTUI(
1894
1904
  return Path(session_cwd).expanduser().resolve()
1895
1905
  return Path.cwd().expanduser().resolve()
1896
1906
 
1897
- async def _init_plugins(self) -> None:
1907
+ async def _init_plugins(self, *, profiler: Any | None = None) -> None:
1898
1908
  """Load and inject plugin resources at startup."""
1909
+ if profiler is not None:
1910
+ profiler.mark("load_thread.start")
1899
1911
  loaded_plugins, plugin_errors = await asyncio.to_thread(
1900
1912
  _load_plugins_blocking,
1901
1913
  self._plugin_project_path(),
1902
1914
  )
1915
+ if profiler is not None:
1916
+ profiler.mark("load_thread.done")
1903
1917
  if self._closing:
1904
1918
  logger.debug("Plugin load completed after TUI close; discarding results")
1905
1919
  return
@@ -1907,7 +1921,11 @@ class TerminalAgentTUI(
1907
1921
  self._loaded_plugins = loaded_plugins
1908
1922
  self._plugin_errors = plugin_errors
1909
1923
  self._plugin_command_names: set[str] = set()
1924
+ if profiler is not None:
1925
+ profiler.mark("inject.start")
1910
1926
  self._inject_plugin_resources(self._loaded_plugins)
1927
+ if profiler is not None:
1928
+ profiler.mark("inject.done")
1911
1929
  logger.info(
1912
1930
  "Loaded %d plugins (%d errors)",
1913
1931
  len(self._loaded_plugins),
@@ -70,33 +70,53 @@ async def check_update(
70
70
  package: str = PACKAGE_NAME,
71
71
  release_notes_url: str = RELEASE_NOTES_URL,
72
72
  log: logging.Logger | None = None,
73
+ profiler: Any | None = None,
73
74
  ) -> UpdateInfo | None:
74
75
  """异步检查 comate-cli 是否有新版本,返回结构化版本信息或 None。"""
75
76
  active_logger = log or logger
76
77
  try:
78
+ if profiler is not None:
79
+ profiler.mark("current_version.start")
77
80
  current = importlib.metadata.version(package)
78
81
  except importlib.metadata.PackageNotFoundError:
79
82
  return None
83
+ finally:
84
+ if profiler is not None:
85
+ profiler.mark("current_version.done")
80
86
 
87
+ if profiler is not None:
88
+ profiler.mark("locale.start")
81
89
  if _is_chinese_locale():
82
90
  url = f"https://mirrors.tuna.tsinghua.edu.cn/pypi/{package}/json"
83
91
  source_label = "tuna"
84
92
  else:
85
93
  url = f"https://pypi.org/pypi/{package}/json"
86
94
  source_label = "pypi"
95
+ if profiler is not None:
96
+ profiler.mark("locale.done")
87
97
 
88
98
  try:
99
+ if profiler is not None:
100
+ profiler.mark("httpx_import.start")
89
101
  import httpx
102
+ if profiler is not None:
103
+ profiler.mark("httpx_import.done")
90
104
 
91
105
  async with httpx.AsyncClient(timeout=3.0) as client:
106
+ if profiler is not None:
107
+ profiler.mark("http_get.start")
92
108
  resp = await client.get(url)
93
109
  resp.raise_for_status()
94
110
  latest = resp.json()["info"]["version"]
111
+ if profiler is not None:
112
+ profiler.mark("http_get.done")
95
113
  except Exception:
96
114
  active_logger.debug(f"update check failed (source={source_label})", exc_info=True)
97
115
  return None
98
116
 
99
117
  try:
118
+ if profiler is not None:
119
+ profiler.mark("version_compare.start")
100
120
  from packaging.version import Version
101
121
 
102
122
  if Version(latest) > Version(current):
@@ -113,6 +133,9 @@ async def check_update(
113
133
  latest,
114
134
  exc_info=True,
115
135
  )
136
+ finally:
137
+ if profiler is not None:
138
+ profiler.mark("version_compare.done")
116
139
  return None
117
140
 
118
141
 
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "comate-cli"
7
- version = "0.7.4"
7
+ version = "0.7.5"
8
8
  description = "Comate terminal CLI built on comate-agent-sdk"
9
9
  readme = "README.md"
10
- requires-python = ">=3.11"
10
+ requires-python = ">=3.11,<3.14"
11
11
  authors = [
12
12
  { name = "Andy", email = "andy.dev@aliyun.com" }
13
13
  ]
@@ -64,7 +64,11 @@ class TestAppStartupLatency(unittest.IsolatedAsyncioTestCase):
64
64
  "run_preflight_if_needed",
65
65
  AsyncMock(return_value=PreflightResult(status="configured")),
66
66
  ),
67
- patch.object(app_module, "_build_agent", side_effect=lambda *, project_root: order.append("build_agent") or object()),
67
+ patch.object(
68
+ app_module,
69
+ "_build_agent",
70
+ side_effect=lambda *, project_root, profiler=None: order.append("build_agent") or object(),
71
+ ),
68
72
  patch.object(app_module, "print_logo", side_effect=lambda *args, **kwargs: order.append("print_logo")),
69
73
  patch.object(app_module, "_check_update", AsyncMock(return_value=None)),
70
74
  patch.object(app_module, "EventRenderer", return_value=MagicMock()),
@@ -149,7 +153,8 @@ class TestAppStartupLatency(unittest.IsolatedAsyncioTestCase):
149
153
  await asyncio.wait_for(update_started.wait(), timeout=0.1)
150
154
  self.run_called.set()
151
155
 
152
- async def _never_finishes() -> UpdateInfo | None:
156
+ async def _never_finishes(*, profiler=None) -> UpdateInfo | None:
157
+ del profiler
153
158
  update_started.set()
154
159
  try:
155
160
  await asyncio.Event().wait()
@@ -190,7 +195,8 @@ class TestAppStartupLatency(unittest.IsolatedAsyncioTestCase):
190
195
  super().__init__(session, status_bar, renderer)
191
196
  created_tuis.append(self)
192
197
 
193
- async def _raises() -> UpdateInfo | None:
198
+ async def _raises(*, profiler=None) -> UpdateInfo | None:
199
+ del profiler
194
200
  raise RuntimeError("network down")
195
201
 
196
202
  with (
@@ -34,7 +34,7 @@ class _FakeStatusBar:
34
34
  return "act"
35
35
 
36
36
  def info_status_text(self) -> str:
37
- return "model | ~main / 100% left"
37
+ return "model | ~main / Context 100% left"
38
38
 
39
39
  def info_status_fragments(self) -> list[tuple[str, str]]:
40
40
  return [("class:status", self.info_status_text())]
@@ -15,11 +15,23 @@ from comate_cli.terminal_agent.tui import TerminalAgentTUI
15
15
 
16
16
 
17
17
  class _RecordingProfiler:
18
- def __init__(self) -> None:
19
- self.phases: list[str] = []
18
+ def __init__(self, prefix: str = "", phases: list[str] | None = None) -> None:
19
+ self._prefix = prefix.strip(".")
20
+ self.phases = phases if phases is not None else []
20
21
 
21
22
  def mark(self, phase: str) -> None:
22
- self.phases.append(phase)
23
+ normalized = phase.strip(".")
24
+ if self._prefix:
25
+ normalized = f"{self._prefix}.{normalized}"
26
+ self.phases.append(normalized)
27
+
28
+ def child(self, prefix: str) -> "_RecordingProfiler":
29
+ normalized = prefix.strip(".")
30
+ if self._prefix and normalized:
31
+ normalized = f"{self._prefix}.{normalized}"
32
+ elif self._prefix:
33
+ normalized = self._prefix
34
+ return _RecordingProfiler(prefix=normalized, phases=self.phases)
23
35
 
24
36
 
25
37
  class _FakeRenderer:
@@ -129,7 +141,7 @@ class TestStartupProfilerTUI(unittest.IsolatedAsyncioTestCase):
129
141
  tui._mcp_init_cancel_timeout_s = 0.05
130
142
  tui._mcp_init_task = None
131
143
  tui._ui_tick_task = None
132
- tui._init_plugins = lambda: asyncio.sleep(0)
144
+ tui._init_plugins = lambda *, profiler=None: asyncio.sleep(0)
133
145
 
134
146
  async def _fake_ui_tick() -> None:
135
147
  await asyncio.Event().wait()
@@ -144,8 +156,116 @@ class TestStartupProfilerTUI(unittest.IsolatedAsyncioTestCase):
144
156
  self.assertIn("tui.run_async.enter", profiler.phases)
145
157
  self.assertIn("tui.run_async.done", profiler.phases)
146
158
 
159
+ async def test_tui_run_marks_background_task_creation_boundaries(self) -> None:
160
+ tui = TerminalAgentTUI.__new__(TerminalAgentTUI)
161
+ tui._app = _FakeApp()
162
+ tui._renderer = _FakeRenderer()
163
+ tui._refresh_layers = lambda: None
164
+ tui._busy = False
165
+ tui._closing = False
166
+ tui._mcp_init_cancel_timeout_s = 0.05
167
+ tui._mcp_init_task = None
168
+ tui._ui_tick_task = None
169
+ tui._init_plugins = lambda *, profiler=None: asyncio.sleep(0)
170
+ tui._status_bar = SimpleNamespace(refresh=AsyncMock())
171
+ tui._session = object()
172
+ tui._consume_event_stream = lambda: asyncio.sleep(0)
173
+
174
+ async def _fake_ui_tick() -> None:
175
+ await asyncio.Event().wait()
176
+
177
+ async def _fake_mcp_init() -> None:
178
+ return None
179
+
180
+ tui._ui_tick = _fake_ui_tick
181
+ profiler = _RecordingProfiler()
182
+
183
+ with patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout):
184
+ await tui.run(mcp_init=_fake_mcp_init, profiler=profiler)
185
+
186
+ self.assertIn("plugins.task.create", profiler.phases)
187
+ self.assertIn("mcp.task.create", profiler.phases)
188
+ self.assertIn("ui_tick.task.create", profiler.phases)
189
+ self.assertIn("status_bar.refresh.task.create", profiler.phases)
190
+ self.assertIn("event_pump.task.create", profiler.phases)
191
+
192
+ async def test_init_plugins_marks_load_and_inject_boundaries(self) -> None:
193
+ tui = TerminalAgentTUI.__new__(TerminalAgentTUI)
194
+ tui._closing = False
195
+ tui._loaded_plugins = []
196
+ tui._plugin_errors = []
197
+ tui._plugin_project_path = lambda: Path("/tmp/project")
198
+ tui._inject_plugin_resources = lambda plugins: None
199
+ profiler = _RecordingProfiler()
200
+
201
+ with patch(
202
+ "comate_cli.terminal_agent.tui._load_plugins_blocking",
203
+ return_value=([], []),
204
+ ):
205
+ await tui._init_plugins(profiler=profiler.child("plugins"))
206
+
207
+ self.assertIn("plugins.load_thread.start", profiler.phases)
208
+ self.assertIn("plugins.load_thread.done", profiler.phases)
209
+ self.assertIn("plugins.inject.start", profiler.phases)
210
+ self.assertIn("plugins.inject.done", profiler.phases)
211
+
147
212
 
148
213
  class TestStartupProfilerApp(unittest.IsolatedAsyncioTestCase):
214
+ async def test_build_agent_marks_config_and_construct_boundaries(self) -> None:
215
+ profiler = _RecordingProfiler()
216
+
217
+ class _FakeAgent:
218
+ def __init__(self, *, config) -> None:
219
+ self.config = config
220
+
221
+ with (
222
+ patch.object(app_module, "_resolve_cli_project_root", return_value=Path("/tmp/project")),
223
+ patch("comate_cli.terminal_agent.config.store.load") as load_config,
224
+ patch.object(app_module, "Agent", _FakeAgent),
225
+ ):
226
+ load_config.return_value = SimpleNamespace(
227
+ token_cost_enabled=False,
228
+ memory_background_enabled=True,
229
+ memory_dreaming_enabled=True,
230
+ compaction_threshold_ratio=0.9,
231
+ )
232
+ app_module._build_agent(project_root=Path("/tmp/project"), profiler=profiler)
233
+
234
+ self.assertIn("agent.config.load.start", profiler.phases)
235
+ self.assertIn("agent.config.load.done", profiler.phases)
236
+ self.assertIn("agent.construct.start", profiler.phases)
237
+ self.assertIn("agent.construct.done", profiler.phases)
238
+
239
+ async def test_background_update_check_marks_internal_boundaries(self) -> None:
240
+ profiler = _RecordingProfiler()
241
+
242
+ with patch.object(app_module, "check_update", AsyncMock(return_value=None)):
243
+ await app_module._check_update(profiler=profiler.child("update_check"))
244
+
245
+ self.assertIn("update_check.impl.start", profiler.phases)
246
+ self.assertIn("update_check.impl.done", profiler.phases)
247
+
248
+ async def test_mcp_preload_marks_start_task_and_await_boundaries(self) -> None:
249
+ preload_task = asyncio.create_task(asyncio.sleep(0))
250
+ manager = SimpleNamespace(failed_servers=[], tool_infos=[])
251
+ runtime = SimpleNamespace(
252
+ config=SimpleNamespace(mcp_enabled=True),
253
+ start_mcp_preload=MagicMock(return_value=preload_task),
254
+ _mcp_manager=manager,
255
+ )
256
+ session = SimpleNamespace(runtime=runtime)
257
+ profiler = _RecordingProfiler()
258
+
259
+ await app_module._preload_mcp_in_tui(session, profiler=profiler.child("mcp")) # type: ignore[arg-type]
260
+
261
+ self.assertIn("mcp.preload.start", profiler.phases)
262
+ self.assertIn("mcp.start_preload.start", profiler.phases)
263
+ self.assertIn("mcp.start_preload.done", profiler.phases)
264
+ self.assertIn("mcp.await_preload.start", profiler.phases)
265
+ self.assertIn("mcp.await_preload.done", profiler.phases)
266
+ self.assertIn("mcp.inspect_manager.start", profiler.phases)
267
+ self.assertIn("mcp.inspect_manager.done", profiler.phases)
268
+
149
269
  async def test_app_run_emits_startup_phase_logs_when_enabled(self) -> None:
150
270
  fake_session = MagicMock()
151
271
  fake_session.session_id = "session-1"