comate-cli 0.7.6__tar.gz → 0.7.8__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 (203) hide show
  1. {comate_cli-0.7.6 → comate_cli-0.7.8}/PKG-INFO +1 -1
  2. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/main.py +10 -0
  3. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/app.py +196 -46
  4. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/event_renderer.py +4 -2
  5. comate_cli-0.7.8/comate_cli/terminal_agent/preflight.py +210 -0
  6. comate_cli-0.7.6/comate_cli/terminal_agent/preflight.py → comate_cli-0.7.8/comate_cli/terminal_agent/preflight_wizard.py +14 -202
  7. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/startup_profile.py +16 -3
  8. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/status_bar.py +3 -5
  9. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/statusline/model.py +1 -5
  10. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/statusline/picker_state.py +0 -1
  11. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tool_result_formatters.py +156 -28
  12. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tui_parts/history_sync.py +48 -10
  13. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/update_check.py +98 -3
  14. {comate_cli-0.7.6 → comate_cli-0.7.8}/pyproject.toml +1 -1
  15. comate_cli-0.7.8/tests/conftest.py +33 -0
  16. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/statusline/test_model.py +3 -5
  17. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/statusline/test_picker_state.py +3 -4
  18. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/statusline/test_store.py +17 -0
  19. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_app_preflight_gate.py +11 -9
  20. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_app_print_mode.py +12 -3
  21. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_app_shutdown.py +20 -7
  22. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_app_startup_latency.py +249 -38
  23. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_app_token_cost_config.py +22 -3
  24. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_event_renderer.py +63 -3
  25. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_history_sync.py +185 -3
  26. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_preflight.py +51 -6
  27. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_preflight_copilot.py +1 -1
  28. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_resume_picker.py +5 -0
  29. comate_cli-0.7.8/tests/test_startup_import_budget.py +118 -0
  30. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_startup_profile.py +87 -11
  31. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_status_bar.py +17 -2
  32. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tool_result_formatters.py +178 -0
  33. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_update_check.py +211 -55
  34. {comate_cli-0.7.6 → comate_cli-0.7.8}/uv.lock +2 -2
  35. comate_cli-0.7.6/tests/conftest.py +0 -14
  36. {comate_cli-0.7.6 → comate_cli-0.7.8}/.gitignore +0 -0
  37. {comate_cli-0.7.6 → comate_cli-0.7.8}/CHANGELOG.md +0 -0
  38. {comate_cli-0.7.6 → comate_cli-0.7.8}/README.md +0 -0
  39. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/__init__.py +0 -0
  40. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/__main__.py +0 -0
  41. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/mcp_cli.py +0 -0
  42. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/__init__.py +0 -0
  43. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/animations.py +0 -0
  44. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/assistant_render.py +0 -0
  45. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/codenames.py +0 -0
  46. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/config/__init__.py +0 -0
  47. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/config/model.py +0 -0
  48. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/config/picker.py +0 -0
  49. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/config/picker_state.py +0 -0
  50. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/config/store.py +0 -0
  51. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  52. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/env_utils.py +0 -0
  53. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/error_display.py +0 -0
  54. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/figures.py +0 -0
  55. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  56. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/goal_resume_view.py +0 -0
  57. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/history_printer.py +0 -0
  58. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/input_geometry.py +0 -0
  59. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  60. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/logging_adapter.py +0 -0
  61. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/logo.py +0 -0
  62. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/markdown_render.py +0 -0
  63. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/mention_completer.py +0 -0
  64. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/message_style.py +0 -0
  65. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/models.py +0 -0
  66. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/path_context_hint.py +0 -0
  67. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
  68. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
  69. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
  70. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
  71. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
  72. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
  73. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
  74. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
  75. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
  76. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
  77. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
  78. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
  79. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
  80. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/question_view.py +0 -0
  81. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/resume_picker.py +0 -0
  82. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/resume_preview.py +0 -0
  83. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/resume_selector.py +0 -0
  84. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  85. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  86. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/selection_menu.py +0 -0
  87. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/slash_commands.py +0 -0
  88. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/startup.py +0 -0
  89. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/statusline/__init__.py +0 -0
  90. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/statusline/picker.py +0 -0
  91. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/statusline/store.py +0 -0
  92. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/text_effects.py +0 -0
  93. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tips.py +0 -0
  94. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tool_fold.py +0 -0
  95. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tool_result_store.py +0 -0
  96. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
  97. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tool_view.py +0 -0
  98. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
  99. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tui.py +0 -0
  100. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  101. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
  102. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
  103. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
  104. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
  105. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
  106. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
  107. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  108. {comate_cli-0.7.6 → comate_cli-0.7.8}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  109. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/config/__init__.py +0 -0
  110. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/config/test_model.py +0 -0
  111. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/config/test_picker_state.py +0 -0
  112. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/config/test_picker_ui.py +0 -0
  113. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/config/test_roundtrip.py +0 -0
  114. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/config/test_store_load.py +0 -0
  115. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/config/test_store_save.py +0 -0
  116. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/fixtures/fake_mcp_misbehaving_stdout.py +0 -0
  117. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/statusline/__init__.py +0 -0
  118. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_animator_shuffle.py +0 -0
  119. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_app_mcp_preload.py +0 -0
  120. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_app_usage_line.py +0 -0
  121. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_btw_slash_command.py +0 -0
  122. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_cli_project_root.py +0 -0
  123. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_compact_command_semantics.py +0 -0
  124. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_completion_context_activation.py +0 -0
  125. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_completion_status_panel.py +0 -0
  126. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_context_command.py +0 -0
  127. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_custom_slash_commands.py +0 -0
  128. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_discover_tab.py +0 -0
  129. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_errors_tab.py +0 -0
  130. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_event_renderer_boundary.py +0 -0
  131. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_event_renderer_e2e.py +0 -0
  132. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_event_renderer_log_boundary.py +0 -0
  133. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_event_renderer_log_queue.py +0 -0
  134. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_event_renderer_streaming.py +0 -0
  135. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_event_renderer_tool_fold.py +0 -0
  136. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_format_error.py +0 -0
  137. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_goal_resume_tui.py +0 -0
  138. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_goal_resume_view.py +0 -0
  139. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_goal_slash_command.py +0 -0
  140. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_handle_error.py +0 -0
  141. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_history_printer.py +0 -0
  142. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_history_printer_log.py +0 -0
  143. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_history_printer_subtitle_position.py +0 -0
  144. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_history_printer_tool_fold.py +0 -0
  145. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_history_sync_tool_fold.py +0 -0
  146. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_input_behavior.py +0 -0
  147. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_input_history.py +0 -0
  148. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_installed_tab.py +0 -0
  149. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_interrupt_exit_semantics.py +0 -0
  150. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_layout_coordinator.py +0 -0
  151. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_logging_adapter.py +0 -0
  152. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_logo.py +0 -0
  153. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_main_args.py +0 -0
  154. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_markdown_render.py +0 -0
  155. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_marketplaces_tab.py +0 -0
  156. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_mcp_cli.py +0 -0
  157. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_mcp_slash_command.py +0 -0
  158. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_mention_completer.py +0 -0
  159. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_path_context_hint.py +0 -0
  160. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_plugin_slash_commands.py +0 -0
  161. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_plugin_tui_components.py +0 -0
  162. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_question_key_bindings.py +0 -0
  163. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_question_view.py +0 -0
  164. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_resume_preview.py +0 -0
  165. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_resume_selector.py +0 -0
  166. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_rewind_command_semantics.py +0 -0
  167. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_rpc_protocol.py +0 -0
  168. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_rpc_stdio_bridge.py +0 -0
  169. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_selection_menu.py +0 -0
  170. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_session_query_token_summary.py +0 -0
  171. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_shutdown_noise_guard.py +0 -0
  172. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_shutdown_noise_integration.py +0 -0
  173. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_skills_slash_command.py +0 -0
  174. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_slash_argument_hint.py +0 -0
  175. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_slash_clear.py +0 -0
  176. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_slash_completer.py +0 -0
  177. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_slash_registry.py +0 -0
  178. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_status_bar_transient.py +0 -0
  179. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_task_panel_format.py +0 -0
  180. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_task_panel_key_bindings.py +0 -0
  181. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_task_panel_rendering.py +0 -0
  182. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_task_poll.py +0 -0
  183. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tool_fold.py +0 -0
  184. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tool_fold_panel.py +0 -0
  185. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tool_result_store.py +0 -0
  186. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tool_result_viewer.py +0 -0
  187. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tool_result_viewer_key_bindings.py +0 -0
  188. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tool_view.py +0 -0
  189. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_transcript_viewer.py +0 -0
  190. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_transcript_viewer_tool_fold.py +0 -0
  191. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tui_elapsed_status.py +0 -0
  192. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tui_esc_queue.py +0 -0
  193. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tui_mcp_init_gate.py +0 -0
  194. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tui_paste_newline_guard.py +0 -0
  195. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tui_paste_placeholder.py +0 -0
  196. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tui_queue_preview.py +0 -0
  197. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tui_queue_sdk_source.py +0 -0
  198. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tui_split_invariance.py +0 -0
  199. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tui_startup_latency.py +0 -0
  200. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tui_team_messages.py +0 -0
  201. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tui_thinking_display.py +0 -0
  202. {comate_cli-0.7.6 → comate_cli-0.7.8}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
  203. {comate_cli-0.7.6 → comate_cli-0.7.8}/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.6
3
+ Version: 0.7.8
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
@@ -7,7 +7,12 @@ import os
7
7
  import signal
8
8
  import subprocess
9
9
  import sys
10
+ import time
10
11
  import warnings
12
+
13
+ # 进程启动锚点:在任何重三方库 import(anthropic/prompt_toolkit/rich)之前捕获,
14
+ # 交给 StartupProfiler 测量「run() 之前的 import」这段以往不可见的启动成本。
15
+ _PROCESS_START_PERF = time.perf_counter()
11
16
  try:
12
17
  import termios
13
18
  except ImportError:
@@ -268,7 +273,9 @@ def main(argv: list[str] | None = None) -> None:
268
273
  atexit.register(term_guard.restore, reason="atexit")
269
274
  atexit.register(noise_guard.begin_shutdown)
270
275
 
276
+ _app_import_start_perf = time.perf_counter()
271
277
  from comate_cli.terminal_agent.app import run
278
+ _app_import_done_perf = time.perf_counter()
272
279
 
273
280
  try:
274
281
  rpc_stdio, resume_session_id, resume_select, print_prompt = _parse_args(run_argv)
@@ -295,6 +302,9 @@ def main(argv: list[str] | None = None) -> None:
295
302
  resume_session_id=resume_session_id,
296
303
  resume_select=resume_select,
297
304
  print_message=print_message,
305
+ process_start_perf=_PROCESS_START_PERF,
306
+ app_import_start_perf=_app_import_start_perf,
307
+ app_import_done_perf=_app_import_done_perf,
298
308
  )
299
309
  )
300
310
  except KeyboardInterrupt:
@@ -11,34 +11,18 @@ import time
11
11
  from collections.abc import Iterator
12
12
  from contextlib import contextmanager, suppress
13
13
  from pathlib import Path
14
+ from typing import TYPE_CHECKING
14
15
 
15
16
  from rich.console import Console
16
17
 
17
- from comate_agent_sdk.agent import Agent, AgentConfig, ChatSession, TextEvent
18
- from comate_agent_sdk.context import EnvOptions
19
- from comate_agent_sdk.tools import tool
20
-
21
- from comate_cli.terminal_agent.event_renderer import EventRenderer
22
- from comate_cli.terminal_agent.logo import print_logo
23
- from comate_cli.terminal_agent.preflight import run_preflight_if_needed
24
- from comate_cli.terminal_agent.resume_selector import select_resume_session_id
25
- from comate_cli.terminal_agent.rpc_stdio import StdioRPCBridge
26
18
  from comate_cli.terminal_agent.startup_profile import StartupProfiler
27
- from comate_cli.terminal_agent.status_bar import StatusBar
28
- from comate_cli.terminal_agent.tui import TerminalAgentTUI
29
- from comate_cli.terminal_agent.update_check import (
30
- UpdateInfo,
31
- UpdatePromptChoice,
32
- UpdatePromptDecision,
33
- check_update,
34
- decide_update_prompt,
35
- format_update_hint,
36
- get_pending_update_prompt_info,
37
- mark_update_seen,
38
- record_skip_until_next_version,
39
- run_update_command,
40
- show_update_prompt,
41
- )
19
+
20
+ if TYPE_CHECKING:
21
+ from comate_agent_sdk.agent.chat_session import ChatSession
22
+ from comate_agent_sdk.agent.core.template import AgentTemplate as Agent
23
+ from comate_agent_sdk.agent.events import TextEvent
24
+ from comate_cli.terminal_agent.event_renderer import EventRenderer
25
+ from comate_cli.terminal_agent.update_check import UpdateInfo
42
26
 
43
27
  console = Console()
44
28
  logger = logging.getLogger(__name__)
@@ -54,6 +38,7 @@ async def _check_update(*, profiler: StartupProfiler | None = None) -> UpdateInf
54
38
  if profiler is not None:
55
39
  profiler.mark("impl.start")
56
40
  try:
41
+ check_update, _, _, _ = _import_update_check_core()
57
42
  return await check_update(log=logger, profiler=profiler)
58
43
  finally:
59
44
  if profiler is not None:
@@ -65,6 +50,16 @@ async def _handle_update_on_launch(info: UpdateInfo) -> bool:
65
50
 
66
51
  Returns True when launch should stop after a successful update.
67
52
  """
53
+ (
54
+ UpdatePromptChoice,
55
+ UpdatePromptDecision,
56
+ decide_update_prompt,
57
+ format_update_hint,
58
+ mark_update_seen,
59
+ record_skip_until_next_version,
60
+ run_update_command,
61
+ show_update_prompt,
62
+ ) = _import_update_prompt_components()
68
63
  try:
69
64
  decision = decide_update_prompt(info)
70
65
  if decision == UpdatePromptDecision.SKIP:
@@ -87,21 +82,31 @@ async def _handle_update_on_launch(info: UpdateInfo) -> bool:
87
82
  async def _handle_background_update_on_launch(
88
83
  info: UpdateInfo,
89
84
  *,
90
- status_bar: StatusBar,
85
+ renderer: EventRenderer,
91
86
  profiler: StartupProfiler,
92
87
  ) -> None:
88
+ (
89
+ _UpdatePromptChoice,
90
+ UpdatePromptDecision,
91
+ decide_update_prompt,
92
+ format_update_hint,
93
+ mark_update_seen,
94
+ _record_skip_until_next_version,
95
+ _run_update_command,
96
+ _show_update_prompt,
97
+ ) = _import_update_prompt_components()
93
98
  try:
94
99
  decision = decide_update_prompt(info)
95
100
  if decision == UpdatePromptDecision.SKIP:
96
101
  profiler.mark("update_check.skip")
97
102
  return
98
103
  if decision == UpdatePromptDecision.SHOW_HINT:
99
- status_bar.show_transient(format_update_hint(info), duration_s=8.0)
104
+ renderer.append_system_message(format_update_hint(info))
100
105
  mark_update_seen(info)
101
106
  profiler.mark("update_check.hint")
102
107
  return
103
108
 
104
- status_bar.show_transient(format_update_hint(info), duration_s=8.0)
109
+ renderer.append_system_message(format_update_hint(info))
105
110
  profiler.mark("update_check.prompt_deferred")
106
111
  except Exception:
107
112
  logger.debug("startup background update handling failed", exc_info=True)
@@ -110,7 +115,7 @@ async def _handle_background_update_on_launch(
110
115
 
111
116
  def _schedule_update_check_on_launch(
112
117
  *,
113
- status_bar: StatusBar,
118
+ renderer: EventRenderer,
114
119
  profiler: StartupProfiler,
115
120
  ) -> asyncio.Task[None]:
116
121
  async def _run() -> None:
@@ -124,7 +129,7 @@ def _schedule_update_check_on_launch(
124
129
  if update_info is not None:
125
130
  await _handle_background_update_on_launch(
126
131
  update_info,
127
- status_bar=status_bar,
132
+ renderer=renderer,
128
133
  profiler=profiler,
129
134
  )
130
135
  except asyncio.CancelledError:
@@ -196,6 +201,121 @@ def _sigint_guard() -> Iterator[None]:
196
201
  logger.warning(f"Failed to restore SIGINT handler: {exc}", exc_info=True)
197
202
 
198
203
 
204
+ @contextmanager
205
+ def _profile_section(
206
+ profiler: StartupProfiler | None,
207
+ phase: str,
208
+ ) -> Iterator[None]:
209
+ if profiler is not None:
210
+ profiler.mark(f"{phase}.start")
211
+ try:
212
+ yield
213
+ finally:
214
+ if profiler is not None:
215
+ profiler.mark(f"{phase}.done")
216
+
217
+
218
+ def _import_preflight_runner():
219
+ from comate_cli.terminal_agent.preflight import run_preflight_if_needed
220
+
221
+ return run_preflight_if_needed
222
+
223
+
224
+ def print_logo(*args, **kwargs) -> None:
225
+ from comate_cli.terminal_agent.logo import print_logo as _print_logo
226
+
227
+ _print_logo(*args, **kwargs)
228
+
229
+
230
+ def _import_sdk_agent_components(
231
+ profiler: StartupProfiler | None = None,
232
+ ) -> tuple[type, type, type, type]:
233
+ with _profile_section(profiler, "main.import.sdk_agent"):
234
+ from comate_agent_sdk.agent.compaction import CompactionConfig
235
+ from comate_agent_sdk.agent.options import AgentConfig
236
+ from comate_agent_sdk.agent.service import Agent
237
+ from comate_agent_sdk.context import EnvOptions
238
+
239
+ return Agent, AgentConfig, CompactionConfig, EnvOptions
240
+
241
+
242
+ def _import_chat_session() -> type:
243
+ from comate_agent_sdk.agent.chat_session import ChatSession
244
+
245
+ return ChatSession
246
+
247
+
248
+ def _import_print_mode_components() -> tuple[type, type]:
249
+ from comate_agent_sdk.agent.chat_session import ChatSession
250
+ from comate_agent_sdk.agent.events import TextEvent
251
+
252
+ return ChatSession, TextEvent
253
+
254
+
255
+ def _import_rpc_bridge() -> type:
256
+ from comate_cli.terminal_agent.rpc_stdio import StdioRPCBridge
257
+
258
+ return StdioRPCBridge
259
+
260
+
261
+ def _import_resume_selector():
262
+ from comate_cli.terminal_agent.resume_selector import select_resume_session_id
263
+
264
+ return select_resume_session_id
265
+
266
+
267
+ def _import_interactive_tui_components(
268
+ profiler: StartupProfiler | None = None,
269
+ ):
270
+ with _profile_section(profiler, "main.import.tui"):
271
+ from comate_cli.terminal_agent.event_renderer import EventRenderer
272
+ from comate_cli.terminal_agent.logging_adapter import setup_tui_logging
273
+ from comate_cli.terminal_agent.status_bar import StatusBar
274
+ from comate_cli.terminal_agent.tui import TerminalAgentTUI
275
+
276
+ return EventRenderer, setup_tui_logging, StatusBar, TerminalAgentTUI
277
+
278
+
279
+ def _import_update_check_core():
280
+ from comate_cli.terminal_agent.update_check import (
281
+ check_update,
282
+ get_pending_update_prompt_info,
283
+ record_update_check_attempt,
284
+ should_check_for_update,
285
+ )
286
+
287
+ return (
288
+ check_update,
289
+ get_pending_update_prompt_info,
290
+ record_update_check_attempt,
291
+ should_check_for_update,
292
+ )
293
+
294
+
295
+ def _import_update_prompt_components():
296
+ from comate_cli.terminal_agent.update_check import (
297
+ UpdatePromptChoice,
298
+ UpdatePromptDecision,
299
+ decide_update_prompt,
300
+ format_update_hint,
301
+ mark_update_seen,
302
+ record_skip_until_next_version,
303
+ run_update_command,
304
+ show_update_prompt,
305
+ )
306
+
307
+ return (
308
+ UpdatePromptChoice,
309
+ UpdatePromptDecision,
310
+ decide_update_prompt,
311
+ format_update_hint,
312
+ mark_update_seen,
313
+ record_skip_until_next_version,
314
+ run_update_command,
315
+ show_update_prompt,
316
+ )
317
+
318
+
199
319
  async def _shutdown_session(session: ChatSession, *, label: str) -> None:
200
320
  start = time.monotonic()
201
321
  try:
@@ -233,17 +353,12 @@ async def _graceful_shutdown(*sessions: ChatSession) -> None:
233
353
  logger.info(f"Graceful shutdown completed in {elapsed:.3f}s")
234
354
 
235
355
 
236
- @tool("Add two numbers 涉及到加法运算 必须使用这个工具")
237
- async def add(a: int, b: int) -> int:
238
- return a + b
239
-
240
-
241
356
  def _build_agent(
242
357
  *,
243
358
  project_root: Path | None = None,
244
359
  profiler: StartupProfiler | None = None,
245
360
  ) -> Agent:
246
- from comate_agent_sdk.agent.compaction import CompactionConfig
361
+ Agent, AgentConfig, CompactionConfig, EnvOptions = _import_sdk_agent_components(profiler)
247
362
  from comate_cli.terminal_agent.config import store
248
363
 
249
364
  resolved_project_root = project_root or _resolve_cli_project_root()
@@ -281,6 +396,7 @@ def _resolve_session(
281
396
  cwd: Path | None = None,
282
397
  profiler: StartupProfiler | None = None,
283
398
  ) -> tuple[ChatSession, str]:
399
+ ChatSession = _import_chat_session()
284
400
  if resume_session_id:
285
401
  if profiler is not None:
286
402
  profiler.mark("session.resume.construct.start")
@@ -387,6 +503,7 @@ async def _run_print_mode(
387
503
  *,
388
504
  project_root: Path,
389
505
  ) -> None:
506
+ ChatSession, TextEvent = _import_print_mode_components()
390
507
  session = ChatSession(agent, cwd=project_root, persistent=False)
391
508
  try:
392
509
  await _preload_mcp_in_tui(session)
@@ -440,13 +557,25 @@ async def run(
440
557
  resume_session_id: str | None = None,
441
558
  resume_select: bool = False,
442
559
  print_message: str | None = None,
560
+ process_start_perf: float | None = None,
561
+ app_import_start_perf: float | None = None,
562
+ app_import_done_perf: float | None = None,
443
563
  ) -> None:
444
564
  _install_event_loop_exception_handler()
445
- profiler = StartupProfiler.from_env(logger=logger)
565
+ # process_start_perf 由入口 main() 在模块 import 之前捕获,使 profiler 能测到
566
+ # 「run() 之前的重三方库 import」这段以往不可见的启动成本(柱子 A)。
567
+ profiler = StartupProfiler.from_env(logger=logger, started_at=process_start_perf)
568
+ if app_import_start_perf is not None:
569
+ profiler.mark_at("main.app_import.start", app_import_start_perf)
570
+ profiler.mark_at("main.import.app.start", app_import_start_perf)
571
+ if app_import_done_perf is not None:
572
+ profiler.mark_at("main.app_import.done", app_import_done_perf)
573
+ profiler.mark_at("main.import.app.done", app_import_done_perf)
446
574
  profiler.mark("cli_project_root.start")
447
575
  project_root = _resolve_cli_project_root()
448
576
  profiler.mark("cli_project_root.done")
449
577
  profiler.mark("preflight.start")
578
+ run_preflight_if_needed = _import_preflight_runner()
450
579
  preflight_result = await run_preflight_if_needed(
451
580
  console=console,
452
581
  project_root=project_root,
@@ -462,6 +591,12 @@ async def run(
462
591
  print_logo(console, project_root=project_root)
463
592
  profiler.mark("logo.printed")
464
593
 
594
+ (
595
+ _check_update_func,
596
+ get_pending_update_prompt_info,
597
+ _record_update_check_attempt,
598
+ _should_check_for_update,
599
+ ) = _import_update_check_core()
465
600
  pending_info = get_pending_update_prompt_info()
466
601
  if pending_info is not None:
467
602
  should_stop = await _handle_update_on_launch(pending_info)
@@ -483,6 +618,7 @@ async def run(
483
618
  cwd=project_root,
484
619
  profiler=profiler,
485
620
  )
621
+ StdioRPCBridge = _import_rpc_bridge()
486
622
  bridge = StdioRPCBridge(session)
487
623
  try:
488
624
  await bridge.run()
@@ -496,6 +632,7 @@ async def run(
496
632
  return
497
633
 
498
634
  if resume_select and not resume_session_id:
635
+ select_resume_session_id = _import_resume_selector()
499
636
  selected_session_id = await select_resume_session_id(console, cwd=project_root)
500
637
  if not selected_session_id:
501
638
  return
@@ -504,8 +641,8 @@ async def run(
504
641
  # 在 _resolve_session 前安装 TUILoggingHandler,确保 session 初始化期间
505
642
  # 的 logger.warning()(如 user_instruction token 超限)能被 TUI 系统捕获,
506
643
  # 而非 fallthrough 到 Python lastResort StreamHandler 以原始文本输出到 stderr。
644
+ EventRenderer, setup_tui_logging, StatusBar, TerminalAgentTUI = _import_interactive_tui_components(profiler)
507
645
  renderer = EventRenderer(project_root=project_root)
508
- from comate_cli.terminal_agent.logging_adapter import setup_tui_logging
509
646
  profiler.mark("logging.setup.start")
510
647
  logging_session = setup_tui_logging(renderer, project_root=project_root)
511
648
  profiler.mark("logging.setup.done")
@@ -538,10 +675,22 @@ async def run(
538
675
  tui = TerminalAgentTUI(session, status_bar, renderer)
539
676
  profiler.mark("tui.init.done")
540
677
  tui.add_resume_history(mode)
541
- update_check_task = _schedule_update_check_on_launch(
542
- status_bar=status_bar,
543
- profiler=profiler,
544
- )
678
+ update_check_task: asyncio.Task[None] | None = None
679
+ (
680
+ _check_update_func,
681
+ _get_pending_update_prompt_info,
682
+ record_update_check_attempt,
683
+ should_check_for_update,
684
+ ) = _import_update_check_core()
685
+ if should_check_for_update():
686
+ # attempt 语义:先记录尝试时间,确保 interval 内最多发起一次网络检查
687
+ record_update_check_attempt()
688
+ update_check_task = _schedule_update_check_on_launch(
689
+ renderer=renderer,
690
+ profiler=profiler,
691
+ )
692
+ else:
693
+ profiler.mark("update_check.throttled")
545
694
 
546
695
  async def _mcp_loader() -> None:
547
696
  await _preload_mcp_in_tui(session, profiler=profiler.child("mcp"))
@@ -569,11 +718,12 @@ async def run(
569
718
  )
570
719
  finally:
571
720
  try:
572
- await _cancel_background_task(
573
- update_check_task,
574
- timeout_s=0.1,
575
- task_name="terminal-update-check",
576
- )
721
+ if update_check_task is not None:
722
+ await _cancel_background_task(
723
+ update_check_task,
724
+ timeout_s=0.1,
725
+ task_name="terminal-update-check",
726
+ )
577
727
  if active_session is session:
578
728
  await _graceful_shutdown(active_session)
579
729
  else:
@@ -1128,15 +1128,17 @@ class EventRenderer:
1128
1128
  diff_lines: list[str] | None = None,
1129
1129
  model_name: str = "",
1130
1130
  subtitle: str | None = None,
1131
+ file_path: str | None = None,
1131
1132
  ) -> None:
1132
1133
  """Append a tool result to history as a static entry (no timer).
1133
1134
 
1134
1135
  Args:
1135
1136
  signature: 工具签名,例如 "Read(path=xxx)"
1136
1137
  is_error: 是否为错误结果
1137
- diff_lines: optional diff lines for Edit
1138
+ diff_lines: optional diff lines for Edit/Write
1138
1139
  model_name: model name for Agent tools (rendered dim)
1139
1140
  subtitle: optional subtitle rendered as `⎿ ...`
1141
+ file_path: optional source path used for syntax highlighting diff body
1140
1142
  """
1141
1143
  sev: Literal["info", "warning", "error"] = "error" if is_error else "info"
1142
1144
  sub_sty: Literal["error", "warning", "dim"] = "dim"
@@ -1145,7 +1147,7 @@ class EventRenderer:
1145
1147
  if model_name:
1146
1148
  text_obj.append(f" {BULLET_OPERATOR} {model_name}", style="dim")
1147
1149
  text_obj.append("\n")
1148
- text_obj.append(render_diff_text(diff_lines))
1150
+ text_obj.append(render_diff_text(diff_lines, file_path=file_path))
1149
1151
  self._append_history_entry(
1150
1152
  HistoryEntry(entry_type="tool_result", text=text_obj, severity="info", subtitle=subtitle)
1151
1153
  )
@@ -0,0 +1,210 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from copy import deepcopy
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Literal, Mapping, Sequence
8
+
9
+ from rich.console import Console
10
+
11
+ from comate_agent_sdk.agent.llm_levels import ALL_LEVELS
12
+ from comate_agent_sdk.agent.settings import (
13
+ USER_SETTINGS_PATH,
14
+ read_settings_json_object,
15
+ write_settings_json_atomic,
16
+ )
17
+ from comate_agent_sdk.facade.config.providers import (
18
+ PROVIDER_PRESETS,
19
+ LevelConfigDraft,
20
+ PreflightCheckResult,
21
+ ProviderPreset,
22
+ build_preview_payload,
23
+ evaluate_preflight_state,
24
+ get_provider_env_var,
25
+ merge_user_settings,
26
+ resolve_provider_presets,
27
+ )
28
+
29
+ from comate_cli.terminal_agent.figures import BULLET_OPERATOR
30
+ from comate_cli.terminal_agent.config.store import (
31
+ apply_cli_config_snapshot_to_settings,
32
+ snapshot_from_settings_dict,
33
+ )
34
+ from comate_cli.terminal_agent.statusline.model import StatuslineConfig
35
+ from comate_cli.terminal_agent.statusline.store import build_statusline_settings
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ _LEVEL_LABEL_WIDTH = 6
40
+ _FIELD_LABEL_WIDTH = 9
41
+ _VALUE_MAX_WIDTH = 96
42
+ @dataclass(frozen=True, slots=True)
43
+ class PreflightResult:
44
+ status: Literal[
45
+ "skipped",
46
+ "configured",
47
+ "cancelled",
48
+ "blocked_non_interactive",
49
+ "failed",
50
+ ]
51
+ detail: str | None = None
52
+
53
+ @property
54
+ def should_abort_launch(self) -> bool:
55
+ return self.status in {"cancelled", "blocked_non_interactive", "failed"}
56
+
57
+ async def run_preflight_if_needed(
58
+ *,
59
+ console: Console,
60
+ project_root: Path,
61
+ interactive: bool,
62
+ provider_presets: Sequence[ProviderPreset] | None = None,
63
+ ) -> PreflightResult:
64
+ check = evaluate_preflight_state(project_root=project_root)
65
+ if not check.needs_preflight:
66
+ return PreflightResult(status="skipped")
67
+
68
+ if not interactive:
69
+ _print_preflight_block_message(console=console, check=check)
70
+ return PreflightResult(status="blocked_non_interactive", detail="missing_llm_config")
71
+
72
+ from comate_cli.terminal_agent.preflight_wizard import _run_preflight_wizard
73
+
74
+ flow = await _run_preflight_wizard(
75
+ check=check,
76
+ project_root=project_root,
77
+ provider_presets=provider_presets,
78
+ )
79
+ if flow.cancel_detail is not None:
80
+ console.print("[yellow]Setup cancelled. Session was not started.[/]")
81
+ return PreflightResult(status="cancelled", detail=flow.cancel_detail)
82
+ draft_with_keys = flow.levels
83
+ if draft_with_keys is None:
84
+ console.print("[yellow]Setup cancelled. Session was not started.[/]")
85
+ return PreflightResult(status="cancelled", detail="unknown_cancelled")
86
+
87
+ settings_path = USER_SETTINGS_PATH.expanduser()
88
+ try:
89
+ merged = merge_user_settings(
90
+ existing=_load_user_settings_json(settings_path),
91
+ levels=draft_with_keys,
92
+ )
93
+ merged = _hydrate_cli_default_settings(merged)
94
+ write_user_settings_atomic(path=settings_path, data=merged)
95
+ except Exception as exc:
96
+ logger.exception("Failed to save pre-flight settings")
97
+ console.print(f"[red]Failed to save provider settings: {exc}[/]")
98
+ return PreflightResult(status="failed", detail="write_failed")
99
+
100
+ recheck = evaluate_preflight_state(project_root=project_root)
101
+ if recheck.needs_preflight:
102
+ console.print(
103
+ "[red]Settings were saved to user scope, but effective configuration is still incomplete. "
104
+ "Please check project/local overrides.[/]"
105
+ )
106
+ return PreflightResult(status="failed", detail="still_missing_after_write")
107
+
108
+ console.print(f"[green]Provider settings saved: {settings_path}[/]")
109
+ return PreflightResult(status="configured")
110
+
111
+
112
+ def write_user_settings_atomic(*, path: Path, data: Mapping[str, Any]) -> None:
113
+ write_settings_json_atomic(path=path, data=data)
114
+
115
+
116
+ def _load_user_settings_json(path: Path) -> dict[str, Any]:
117
+ return read_settings_json_object(path)
118
+
119
+
120
+ def _deep_merge_missing_settings(
121
+ existing: Mapping[str, Any] | dict[str, Any],
122
+ defaults: Mapping[str, Any],
123
+ ) -> dict[str, Any]:
124
+ result: dict[str, Any] = deepcopy(dict(existing))
125
+ for key, default_value in defaults.items():
126
+ current_value = result.get(key)
127
+ if key not in result:
128
+ result[key] = deepcopy(default_value)
129
+ elif isinstance(current_value, dict) and isinstance(default_value, dict):
130
+ result[key] = _deep_merge_missing_settings(current_value, default_value)
131
+ return result
132
+
133
+
134
+ def _hydrate_cli_default_settings(data: Mapping[str, Any]) -> dict[str, Any]:
135
+ """Fill missing CLI-owned defaults during settings writes.
136
+
137
+ This is write-path-only. `store.load()` remains read-only, so CLI startup
138
+ does not create ~/.agent/settings.json unless preflight or a picker saves.
139
+ """
140
+ hydrated: dict[str, Any] = deepcopy(dict(data))
141
+ cli_raw = hydrated.get("comate_cli")
142
+ cli = deepcopy(cli_raw) if isinstance(cli_raw, dict) else {}
143
+
144
+ statusline_raw = cli.get("statusline")
145
+ if isinstance(statusline_raw, dict):
146
+ statusline_existing = statusline_raw
147
+ else:
148
+ legacy_statusline = hydrated.get("statusline")
149
+ statusline_existing = legacy_statusline if isinstance(legacy_statusline, dict) else {}
150
+ cli["statusline"] = _deep_merge_missing_settings(
151
+ statusline_existing,
152
+ build_statusline_settings(StatuslineConfig.default()),
153
+ )
154
+
155
+ hydrated["comate_cli"] = cli
156
+ hydrated.pop("statusline", None)
157
+ snapshot = snapshot_from_settings_dict(hydrated)
158
+ return apply_cli_config_snapshot_to_settings(hydrated, snapshot)
159
+
160
+
161
+ def _truncate_value(value: str, max_width: int = _VALUE_MAX_WIDTH) -> str:
162
+ text = value.strip()
163
+ if len(text) <= max_width:
164
+ return text
165
+ if max_width <= 3:
166
+ return text[:max_width]
167
+ return f"{text[:max_width - 3]}..."
168
+
169
+
170
+ def _format_level_summary(*, level: str, entry: LevelConfigDraft) -> str:
171
+ model_value = _truncate_value(f"{entry.provider}:{entry.model}")
172
+ endpoint_value = _truncate_value(entry.base_url or "(default)")
173
+ level_cell = f"{level:<{_LEVEL_LABEL_WIDTH}}"
174
+ model_label = f"{'Model':<{_FIELD_LABEL_WIDTH}}"
175
+ endpoint_label = f"{'Endpoint':<{_FIELD_LABEL_WIDTH}}"
176
+ return (
177
+ f"{level_cell}{model_label}{model_value}\n"
178
+ f"{'':<{_LEVEL_LABEL_WIDTH}}{endpoint_label}{endpoint_value}"
179
+ )
180
+
181
+
182
+ def _format_missing_reasons(reasons: tuple[str, ...], *, bullet_indent: str = " ") -> str:
183
+ if not reasons:
184
+ return "What's missing:\n - Unknown issue"
185
+ lines = ["What's missing:"]
186
+ lines.extend(f"{bullet_indent}- {reason}" for reason in reasons)
187
+ return "\n".join(lines)
188
+
189
+
190
+ def _print_preflight_block_message(*, console: Console, check: PreflightCheckResult) -> None:
191
+ reason_lines = _format_missing_reasons(check.reasons)
192
+ console.print("[red]Provider setup required[/]")
193
+ console.print("[dim]You need to configure an API provider before you can continue.[/]")
194
+ console.print(f"[dim]{reason_lines}[/]")
195
+ console.print("[dim]Run `comate` in interactive mode to complete provider setup.[/]")
196
+
197
+
198
+ def _build_preflight_entry_options() -> list[dict[str, str]]:
199
+ return [
200
+ {
201
+ "value": "continue",
202
+ "label": "Start setup now",
203
+ "description": "",
204
+ },
205
+ {
206
+ "value": "cancel",
207
+ "label": "Exit",
208
+ "description": "",
209
+ },
210
+ ]