comate-cli 0.8.3__tar.gz → 0.8.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 (233) hide show
  1. {comate_cli-0.8.3 → comate_cli-0.8.4}/PKG-INFO +1 -1
  2. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/event_renderer.py +103 -1
  3. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/render_panels.py +49 -14
  4. {comate_cli-0.8.3 → comate_cli-0.8.4}/docs/cli-rpc-stdio-guide.md +6 -1
  5. {comate_cli-0.8.3 → comate_cli-0.8.4}/pyproject.toml +1 -1
  6. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/recap/test_idle_recap_controller.py +55 -0
  7. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_completion_status_panel.py +62 -0
  8. comate_cli-0.8.4/tests/test_event_renderer_tool_args_progress.py +193 -0
  9. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_rpc_protocol.py +41 -1
  10. comate_cli-0.8.4/tests/test_tui_provider_error_shield.py +427 -0
  11. {comate_cli-0.8.3 → comate_cli-0.8.4}/.gitignore +0 -0
  12. {comate_cli-0.8.3 → comate_cli-0.8.4}/CHANGELOG.md +0 -0
  13. {comate_cli-0.8.3 → comate_cli-0.8.4}/CONTEXT.md +0 -0
  14. {comate_cli-0.8.3 → comate_cli-0.8.4}/README.md +0 -0
  15. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/__init__.py +0 -0
  16. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/__main__.py +0 -0
  17. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/main.py +0 -0
  18. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/mcp_cli.py +0 -0
  19. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/__init__.py +0 -0
  20. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/animations.py +0 -0
  21. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/app.py +0 -0
  22. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/assistant_render.py +0 -0
  23. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/builtin_commands/.keep +0 -0
  24. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/codenames.py +0 -0
  25. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/config/__init__.py +0 -0
  26. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/config/model.py +0 -0
  27. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/config/picker.py +0 -0
  28. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/config/picker_state.py +0 -0
  29. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/config/store.py +0 -0
  30. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  31. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/env_utils.py +0 -0
  32. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/error_display.py +0 -0
  33. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/figures.py +0 -0
  34. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/file_ref_hint.py +0 -0
  35. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  36. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/goal_resume_view.py +0 -0
  37. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/history_printer.py +0 -0
  38. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/input_geometry.py +0 -0
  39. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  40. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/logging_adapter.py +0 -0
  41. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/logo.py +0 -0
  42. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/markdown_render.py +0 -0
  43. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/mention_completer.py +0 -0
  44. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/message_style.py +0 -0
  45. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/models.py +0 -0
  46. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/path_context_hint.py +0 -0
  47. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
  48. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
  49. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
  50. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
  51. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
  52. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
  53. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
  54. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/operation_state.py +0 -0
  55. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
  56. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
  57. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
  58. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
  59. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
  60. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
  61. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/preflight.py +0 -0
  62. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/preflight_wizard.py +0 -0
  63. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/print_mode.py +0 -0
  64. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/question_view.py +0 -0
  65. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/recap/__init__.py +0 -0
  66. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/recap/controller.py +0 -0
  67. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/resume_picker.py +0 -0
  68. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/resume_preview.py +0 -0
  69. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/resume_selector.py +0 -0
  70. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  71. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  72. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/selection_menu.py +0 -0
  73. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/slash_commands.py +0 -0
  74. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/startup.py +0 -0
  75. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/startup_profile.py +0 -0
  76. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/status_bar.py +0 -0
  77. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/statusline/__init__.py +0 -0
  78. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/statusline/model.py +0 -0
  79. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/statusline/picker.py +0 -0
  80. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/statusline/picker_state.py +0 -0
  81. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/statusline/store.py +0 -0
  82. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/text_effects.py +0 -0
  83. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tips.py +0 -0
  84. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tool_fold.py +0 -0
  85. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tool_result_formatters.py +0 -0
  86. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tool_result_store.py +0 -0
  87. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
  88. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tool_view.py +0 -0
  89. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
  90. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui.py +0 -0
  91. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  92. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
  93. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
  94. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
  95. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
  96. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
  97. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
  98. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  99. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  100. {comate_cli-0.8.3 → comate_cli-0.8.4}/comate_cli/terminal_agent/update_check.py +0 -0
  101. {comate_cli-0.8.3 → comate_cli-0.8.4}/dist-comate/.gitignore +0 -0
  102. {comate_cli-0.8.3 → comate_cli-0.8.4}/dist-comate/comate_cli-0.7.10-py3-none-any.whl +0 -0
  103. {comate_cli-0.8.3 → comate_cli-0.8.4}/dist-comate/comate_cli-0.7.10.tar.gz +0 -0
  104. {comate_cli-0.8.3 → comate_cli-0.8.4}/dist-hico/.gitignore +0 -0
  105. {comate_cli-0.8.3 → comate_cli-0.8.4}/dist-hico/hico_cli-0.7.17-py3-none-any.whl +0 -0
  106. {comate_cli-0.8.3 → comate_cli-0.8.4}/dist-hico/hico_cli-0.7.17.tar.gz +0 -0
  107. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/config/__init__.py +0 -0
  108. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/config/test_model.py +0 -0
  109. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/config/test_picker_state.py +0 -0
  110. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/config/test_picker_ui.py +0 -0
  111. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/config/test_roundtrip.py +0 -0
  112. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/config/test_store_load.py +0 -0
  113. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/config/test_store_save.py +0 -0
  114. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/conftest.py +0 -0
  115. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/fixtures/fake_mcp_misbehaving_stdout.py +0 -0
  116. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/recap/test_recap_history_rendering.py +0 -0
  117. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/recap/test_tui_idle_recap_integration.py +0 -0
  118. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/statusline/__init__.py +0 -0
  119. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/statusline/test_model.py +0 -0
  120. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/statusline/test_picker_state.py +0 -0
  121. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/statusline/test_store.py +0 -0
  122. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_animator_shuffle.py +0 -0
  123. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_app_mcp_preload.py +0 -0
  124. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_app_preflight_gate.py +0 -0
  125. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_app_print_mode.py +0 -0
  126. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_app_shutdown.py +0 -0
  127. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_app_startup_latency.py +0 -0
  128. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_app_token_cost_config.py +0 -0
  129. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_app_usage_line.py +0 -0
  130. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_btw_slash_command.py +0 -0
  131. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_cli_project_root.py +0 -0
  132. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_compact_command_semantics.py +0 -0
  133. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_completion_context_activation.py +0 -0
  134. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_context_command.py +0 -0
  135. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_custom_slash_commands.py +0 -0
  136. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_discover_tab.py +0 -0
  137. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_errors_tab.py +0 -0
  138. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_event_renderer.py +0 -0
  139. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_event_renderer_boundary.py +0 -0
  140. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_event_renderer_e2e.py +0 -0
  141. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_event_renderer_log_boundary.py +0 -0
  142. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_event_renderer_log_queue.py +0 -0
  143. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_event_renderer_streaming.py +0 -0
  144. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_event_renderer_tool_fold.py +0 -0
  145. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_file_ref_hint.py +0 -0
  146. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_format_error.py +0 -0
  147. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_goal_resume_tui.py +0 -0
  148. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_goal_resume_view.py +0 -0
  149. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_goal_slash_command.py +0 -0
  150. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_handle_error.py +0 -0
  151. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_history_printer.py +0 -0
  152. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_history_printer_log.py +0 -0
  153. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_history_printer_subtitle_position.py +0 -0
  154. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_history_printer_tool_fold.py +0 -0
  155. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_history_sync.py +0 -0
  156. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_history_sync_tool_fold.py +0 -0
  157. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_input_behavior.py +0 -0
  158. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_input_history.py +0 -0
  159. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_installed_tab.py +0 -0
  160. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_interrupt_exit_semantics.py +0 -0
  161. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_layout_coordinator.py +0 -0
  162. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_logging_adapter.py +0 -0
  163. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_logo.py +0 -0
  164. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_main_args.py +0 -0
  165. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_markdown_render.py +0 -0
  166. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_marketplaces_tab.py +0 -0
  167. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_mcp_cli.py +0 -0
  168. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_mcp_slash_command.py +0 -0
  169. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_mention_completer.py +0 -0
  170. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_model_switch_command.py +0 -0
  171. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_path_context_hint.py +0 -0
  172. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_plugin_operation_state.py +0 -0
  173. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_plugin_slash_commands.py +0 -0
  174. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_plugin_tui_components.py +0 -0
  175. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_preflight.py +0 -0
  176. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_preflight_copilot.py +0 -0
  177. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_question_key_bindings.py +0 -0
  178. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_question_view.py +0 -0
  179. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_resume_picker.py +0 -0
  180. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_resume_preview.py +0 -0
  181. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_resume_selector.py +0 -0
  182. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_rewind_command_semantics.py +0 -0
  183. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_rpc_stdio_bridge.py +0 -0
  184. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_selection_menu.py +0 -0
  185. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_session_query_token_summary.py +0 -0
  186. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_shutdown_noise_guard.py +0 -0
  187. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_shutdown_noise_integration.py +0 -0
  188. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_skills_slash_command.py +0 -0
  189. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_slash_argument_hint.py +0 -0
  190. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_slash_clear.py +0 -0
  191. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_slash_completer.py +0 -0
  192. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_slash_registry.py +0 -0
  193. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_startup_import_budget.py +0 -0
  194. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_startup_profile.py +0 -0
  195. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_status_bar.py +0 -0
  196. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_status_bar_transient.py +0 -0
  197. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_task_panel_format.py +0 -0
  198. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_task_panel_key_bindings.py +0 -0
  199. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_task_panel_rendering.py +0 -0
  200. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_task_poll.py +0 -0
  201. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tips.py +0 -0
  202. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tool_fold.py +0 -0
  203. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tool_fold_panel.py +0 -0
  204. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tool_result_formatters.py +0 -0
  205. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tool_result_store.py +0 -0
  206. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tool_result_viewer.py +0 -0
  207. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tool_result_viewer_key_bindings.py +0 -0
  208. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tool_view.py +0 -0
  209. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_transcript_viewer.py +0 -0
  210. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_transcript_viewer_tool_fold.py +0 -0
  211. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_builtin_slash_chain.py +0 -0
  212. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_elapsed_status.py +0 -0
  213. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_esc_queue.py +0 -0
  214. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_llm_retry_panel.py +0 -0
  215. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_local_interactive_barrier.py +0 -0
  216. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_mcp_init_gate.py +0 -0
  217. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_paste_newline_guard.py +0 -0
  218. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_paste_placeholder.py +0 -0
  219. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_queue_preview.py +0 -0
  220. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_queue_sdk_source.py +0 -0
  221. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_scheduled_fire_log.py +0 -0
  222. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_send_undo_window.py +0 -0
  223. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_split_invariance.py +0 -0
  224. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_startup_latency.py +0 -0
  225. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_team_messages.py +0 -0
  226. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_thinking_display.py +0 -0
  227. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
  228. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_update_check.py +0 -0
  229. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_usage_command.py +0 -0
  230. {comate_cli-0.8.3 → comate_cli-0.8.4}/tests/test_wrap_user_text.py +0 -0
  231. {comate_cli-0.8.3 → comate_cli-0.8.4}/tools/logo_lab.py +0 -0
  232. {comate_cli-0.8.3 → comate_cli-0.8.4}/tools/nico_preview.py +0 -0
  233. {comate_cli-0.8.3 → comate_cli-0.8.4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comate-cli
3
- Version: 0.8.3
3
+ Version: 0.8.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
@@ -35,8 +35,10 @@ from comate_agent_sdk.agent.events import (
35
35
  ThinkingDeltaEvent,
36
36
  TaskUpdatedEvent,
37
37
  ToolCallEvent,
38
+ ToolCallArgsProgressEvent,
38
39
  ToolCallStartEvent,
39
40
  ToolResultEvent,
41
+ StreamDisplayResetEvent,
40
42
  UsageDeltaEvent,
41
43
  UserQuestionEvent,
42
44
  )
@@ -81,6 +83,9 @@ _DEFAULT_TOOL_PANEL_MAX_LINES = 4
81
83
  _DEFAULT_TASK_PANEL_MAX_LINES = 6
82
84
  _RECENT_TEAM_EVENT_CACHE_SIZE = 128
83
85
  _SYSTEM_MESSAGE_DEDUPE_WINDOW_SECONDS = 0.5
86
+ TOOL_ARGS_PROGRESS_VISIBLE_TOKENS = 1000
87
+ TOOL_ARGS_PROGRESS_VISIBLE_AFTER_SECONDS = 1.5
88
+ TOOL_ARGS_PROGRESS_VISIBLE_MIN_TOKENS_AFTER_DELAY = 256
84
89
 
85
90
 
86
91
  def _truncate(content: str, max_len: int = 120) -> str:
@@ -129,6 +134,13 @@ def _format_tokens(token_count: int) -> str:
129
134
  return f"{compact}k tok"
130
135
 
131
136
 
137
+ def _format_tool_args_tokens(token_count: int) -> str:
138
+ tokens = max(int(token_count), 0)
139
+ if tokens < 1_000:
140
+ return f"{tokens} tokens"
141
+ return f"{tokens / 1_000:.1f}k tokens"
142
+
143
+
132
144
  def _split_fragments_by_newline(
133
145
  fragments: list[tuple[str, str]],
134
146
  ) -> list[list[tuple[str, str]]]:
@@ -245,6 +257,16 @@ class _RunningTool:
245
257
  subagent_model_name: str = ""
246
258
 
247
259
 
260
+ @dataclass
261
+ class _ToolArgsProgressState:
262
+ tool_call_id: str
263
+ tool_name: str
264
+ started_at_monotonic: float
265
+ last_updated_at_monotonic: float
266
+ estimated_tokens: int = 0
267
+ visible: bool = False
268
+
269
+
248
270
  @dataclass(frozen=True)
249
271
  class ActiveLLMRetrySnapshot:
250
272
  retry_id: str
@@ -309,6 +331,7 @@ class EventRenderer:
309
331
  self._fallback_tool_result_sequence = 0
310
332
  self._assistant_buffer = ""
311
333
  self._pending_tool_starts: set[str] = set()
334
+ self._tool_args_progress: dict[str, _ToolArgsProgressState] = {}
312
335
  self._loading_state: LoadingState = LoadingState.idle()
313
336
  self._current_tasks: list[dict[str, Any]] = []
314
337
  self._current_task_title: str | None = None
@@ -668,6 +691,55 @@ class EventRenderer:
668
691
  self._text_delta_started = False
669
692
  self._active_text_message_id = None
670
693
 
694
+ def _clear_tool_args_progress(self, tool_call_id: str | None = None) -> None:
695
+ if tool_call_id is None:
696
+ self._tool_args_progress.clear()
697
+ return
698
+ normalized_id = str(tool_call_id or "").strip()
699
+ if normalized_id:
700
+ self._tool_args_progress.pop(normalized_id, None)
701
+
702
+ def _start_tool_args_progress(self, tool_call_id: str, tool_name: str) -> None:
703
+ normalized_id = str(tool_call_id or "").strip()
704
+ if not normalized_id:
705
+ return
706
+ now = time.monotonic()
707
+ state = self._tool_args_progress.get(normalized_id)
708
+ if state is None:
709
+ self._tool_args_progress[normalized_id] = _ToolArgsProgressState(
710
+ tool_call_id=normalized_id,
711
+ tool_name=str(tool_name or "").strip(),
712
+ started_at_monotonic=now,
713
+ last_updated_at_monotonic=now,
714
+ )
715
+ return
716
+ if tool_name:
717
+ state.tool_name = str(tool_name).strip()
718
+
719
+ def _handle_tool_args_progress(self, event: ToolCallArgsProgressEvent) -> None:
720
+ normalized_id = str(event.tool_call_id or "").strip()
721
+ if not normalized_id:
722
+ return
723
+ state = self._tool_args_progress.get(normalized_id)
724
+ if state is None:
725
+ return
726
+ now = time.monotonic()
727
+ state.last_updated_at_monotonic = now
728
+ if event.tool:
729
+ state.tool_name = str(event.tool).strip()
730
+ state.estimated_tokens = max(int(event.estimated_tokens), 0)
731
+ if state.visible:
732
+ return
733
+ elapsed = now - state.started_at_monotonic
734
+ state.visible = (
735
+ state.estimated_tokens >= TOOL_ARGS_PROGRESS_VISIBLE_TOKENS
736
+ or (
737
+ elapsed >= TOOL_ARGS_PROGRESS_VISIBLE_AFTER_SECONDS
738
+ and state.estimated_tokens
739
+ >= TOOL_ARGS_PROGRESS_VISIBLE_MIN_TOKENS_AFTER_DELAY
740
+ )
741
+ )
742
+
671
743
  def _should_drop_duplicate_system_message(
672
744
  self,
673
745
  *,
@@ -699,6 +771,7 @@ class EventRenderer:
699
771
  self._active_llm_retry = None
700
772
  self._flush_assistant_segment()
701
773
  self._pending_tool_starts.clear()
774
+ self._clear_tool_args_progress()
702
775
  self._turn_received_text_delta = False
703
776
  self._turn_received_thinking_delta = False
704
777
  self._text_delta_started = False
@@ -760,6 +833,7 @@ class EventRenderer:
760
833
  self._flush_assistant_segment()
761
834
  self.flush_pending_logs()
762
835
  self._pending_tool_starts.clear()
836
+ self._clear_tool_args_progress()
763
837
  self._rebuild_loading_line()
764
838
 
765
839
  def tick_progress(self) -> None:
@@ -782,6 +856,7 @@ class EventRenderer:
782
856
  self._flush_assistant_segment()
783
857
  self._commit_active_tool_fold()
784
858
  self._pending_tool_starts.clear()
859
+ self._clear_tool_args_progress()
785
860
  # spec §6.4:用户中断 → 5 个新字段全部丢弃不入队
786
861
  self._drop_all_pending()
787
862
  self._rebuild_loading_line()
@@ -802,6 +877,7 @@ class EventRenderer:
802
877
  self._active_tool_fold.clear()
803
878
  self._assistant_buffer = ""
804
879
  self._pending_tool_starts.clear()
880
+ self._clear_tool_args_progress()
805
881
  self._turn_received_text_delta = False
806
882
  self._turn_received_thinking_delta = False
807
883
  self._text_delta_started = False
@@ -869,6 +945,15 @@ class EventRenderer:
869
945
  拼接到 spinner phrase。无容器时返回 ""。"""
870
946
  return self._loading_aux_text
871
947
 
948
+ def tool_args_progress_text(self) -> str:
949
+ visible_states = [
950
+ state for state in self._tool_args_progress.values() if state.visible
951
+ ]
952
+ if not visible_states:
953
+ return ""
954
+ total_tokens = sum(state.estimated_tokens for state in visible_states)
955
+ return f"↓ {_format_tool_args_tokens(total_tokens)}"
956
+
872
957
  def active_llm_retry_snapshot(self) -> ActiveLLMRetrySnapshot | None:
873
958
  return self._active_llm_retry
874
959
 
@@ -1467,7 +1552,14 @@ class EventRenderer:
1467
1552
  def handle_event(self, event: Any) -> tuple[bool, list[dict[str, Any]] | None]:
1468
1553
  if not isinstance(
1469
1554
  event,
1470
- (TextEvent, TextDeltaEvent, ThinkingDeltaEvent, ToolCallStartEvent),
1555
+ (
1556
+ TextEvent,
1557
+ TextDeltaEvent,
1558
+ ThinkingDeltaEvent,
1559
+ ToolCallStartEvent,
1560
+ ToolCallArgsProgressEvent,
1561
+ StreamDisplayResetEvent,
1562
+ ),
1471
1563
  ):
1472
1564
  self._flush_assistant_segment()
1473
1565
 
@@ -1498,6 +1590,12 @@ class EventRenderer:
1498
1590
  normalized_id = str(tool_call_id or "").strip()
1499
1591
  if normalized_id and normalized_id not in self._pending_tool_starts:
1500
1592
  self._pending_tool_starts.add(normalized_id)
1593
+ self._start_tool_args_progress(normalized_id, tool_name)
1594
+ case ToolCallArgsProgressEvent():
1595
+ self._handle_tool_args_progress(event)
1596
+ case StreamDisplayResetEvent(reason="stream_fallback"):
1597
+ self._pending_tool_starts.clear()
1598
+ self._clear_tool_args_progress()
1501
1599
  case ThinkingEvent(content=thinking):
1502
1600
  if self._turn_received_thinking_delta:
1503
1601
  pass
@@ -1566,6 +1664,7 @@ class EventRenderer:
1566
1664
  severity="warning",
1567
1665
  )
1568
1666
  case ToolCallEvent(tool=tool_name, args=arguments, tool_call_id=tool_call_id):
1667
+ self._clear_tool_args_progress(tool_call_id)
1569
1668
  args_dict = arguments if isinstance(arguments, dict) else {"_raw": str(arguments)}
1570
1669
  # Store args for ToolResult phase lookup.
1571
1670
  self._tool_call_args[tool_call_id] = args_dict
@@ -1584,6 +1683,7 @@ class EventRenderer:
1584
1683
  return (False, None)
1585
1684
  self._append_tool_call(tool_name, args_dict, tool_call_id)
1586
1685
  case ToolResultEvent(tool=tool_name, result=result, tool_call_id=tool_call_id, is_error=is_error, metadata=metadata, output=output):
1686
+ self._clear_tool_args_progress(tool_call_id)
1587
1687
  stored_args = self._tool_call_args.pop(tool_call_id, {})
1588
1688
  if self._active_tool_fold.has_call(tool_call_id):
1589
1689
  display_name = resolve_display_tool_name(tool_name, stored_args)
@@ -1764,11 +1864,13 @@ class EventRenderer:
1764
1864
  # Cancellation/error paths may emit StepCompleteEvent without ToolResultEvent.
1765
1865
  # Ensure tool panel state is cleaned up by step id.
1766
1866
  self._running_tools.pop(step_id, None)
1867
+ self._clear_tool_args_progress(step_id)
1767
1868
  case StopEvent(reason=reason):
1768
1869
  self._active_llm_retry = None
1769
1870
  self._flush_assistant_segment()
1770
1871
  self._commit_active_tool_fold()
1771
1872
  self._pending_tool_starts.clear()
1873
+ self._clear_tool_args_progress()
1772
1874
  # Safety net: if any running tool rows remain, stop event means this turn is ending.
1773
1875
  self._running_tools.clear()
1774
1876
  self._hidden_thinking_badge_text = ""
@@ -823,35 +823,55 @@ class RenderPanelsMixin:
823
823
  return ""
824
824
  return str(badge_fn() or "").strip()
825
825
 
826
+ def _tool_args_progress_text(self) -> str:
827
+ progress_fn = getattr(self._renderer, "tool_args_progress_text", None)
828
+ if not callable(progress_fn):
829
+ return ""
830
+ return str(progress_fn() or "").strip()
831
+
826
832
  @staticmethod
827
833
  def _fragments_width(frags: list[tuple[str, str]]) -> int:
828
834
  return sum(get_cwidth(char) for _, text in frags for char in text)
829
835
 
830
- def _running_status_suffix_fragments(self) -> list[tuple[str, str]]:
836
+ def _running_status_suffix_fragments(
837
+ self,
838
+ *,
839
+ max_width: int | None = None,
840
+ ) -> list[tuple[str, str]]:
831
841
  """Build the dim parenthetical status suffix for active work."""
832
842
  status_style = "fg:#6B7280"
833
843
  badge_style = "fg:#FBBF24 bold"
834
844
 
835
845
  run_start: float | None = getattr(self, "_run_start_time", None)
846
+ args_progress = self._tool_args_progress_text()
836
847
  badge = self._hidden_thinking_badge_text()
837
- if run_start is None and not badge:
848
+ if run_start is None and not args_progress and not badge:
838
849
  return []
839
850
 
840
851
  parts: list[tuple[str, str]] = [(status_style, " (")]
841
- has_elapsed = run_start is not None
842
- if has_elapsed:
852
+ has_parts = False
853
+ if run_start is not None:
843
854
  elapsed = time.monotonic() - run_start
844
- parts.append(
845
- (
846
- status_style,
847
- f"{self._format_run_elapsed(elapsed)} {BULLET} ctrl+c to interrupt",
848
- )
849
- )
855
+ parts.append((status_style, self._format_run_elapsed(elapsed)))
856
+ has_parts = True
857
+ if args_progress:
858
+ if has_parts:
859
+ parts.append((status_style, f" {BULLET} "))
860
+ parts.append((status_style, args_progress))
861
+ has_parts = True
862
+ if run_start is not None:
863
+ if has_parts:
864
+ parts.append((status_style, f" {BULLET} "))
865
+ parts.append((status_style, "ctrl+c to interrupt"))
866
+ has_parts = True
850
867
  if badge:
851
- if has_elapsed:
868
+ if has_parts:
852
869
  parts.append((status_style, f" {BULLET} "))
853
870
  parts.append((badge_style, badge))
854
871
  parts.append((status_style, ")"))
872
+ if max_width is not None and self._fragments_width(parts) > max_width:
873
+ clipped = fit_single_line("".join(text for _, text in parts), max_width)
874
+ return [(status_style, clipped)]
855
875
  return parts
856
876
 
857
877
  def _visible_loading_tip(self) -> str:
@@ -897,7 +917,9 @@ class RenderPanelsMixin:
897
917
  ) -> list[tuple[str, str]]:
898
918
  """用低噪音 spinner + 流光走字渲染 loading 文案."""
899
919
  width = self._terminal_width()
900
- status_suffix = self._running_status_suffix_fragments()
920
+ status_suffix = self._running_status_suffix_fragments(
921
+ max_width=max(width - 10, 8)
922
+ )
901
923
 
902
924
  # glyph(1) + space(1) = 2,再减去运行状态后缀宽度
903
925
  phrase_budget = width - 2 - self._fragments_width(status_suffix)
@@ -921,7 +943,10 @@ class RenderPanelsMixin:
921
943
  frags: list[tuple[str, str]],
922
944
  ) -> list[tuple[str, str]]:
923
945
  """Append the running status suffix before any Rich trailing newline."""
924
- status_suffix = self._running_status_suffix_fragments()
946
+ width = self._terminal_width()
947
+ status_suffix = self._running_status_suffix_fragments(
948
+ max_width=max(width - 8, 8)
949
+ )
925
950
  if not status_suffix:
926
951
  return frags
927
952
  # Rich __rich_console__ 末尾会输出一个 "\n" segment,需要先剥掉再追加,
@@ -930,7 +955,17 @@ class RenderPanelsMixin:
930
955
  trimmed = list(frags)
931
956
  while trimmed and trimmed[-1][1] == "\n":
932
957
  tail.insert(0, trimmed.pop())
933
- return trimmed + status_suffix + tail
958
+ base_width = self._fragments_width(trimmed)
959
+ suffix_width = self._fragments_width(status_suffix)
960
+ if base_width + suffix_width <= width:
961
+ return trimmed + status_suffix + tail
962
+
963
+ base_budget = max(width - suffix_width, 8)
964
+ clipped_base = fit_single_line(
965
+ "".join(text for _, text in trimmed),
966
+ base_budget,
967
+ )
968
+ return [("class:loading", clipped_base)] + status_suffix + tail
934
969
 
935
970
  def _loading_height(self) -> int:
936
971
  if self._should_hide_loading_panel():
@@ -217,8 +217,10 @@ stdio bridge 明确不承担历史重放职责。需要历史请走 `comate resu
217
217
  | `TextEvent` | 文本块最终落地(入历史) | `content`, `structured_output`, `force_display` |
218
218
  | `ThinkingDeltaEvent` / `ThinkingEvent` | 思维链增量 / 落地 | `content`/`delta` |
219
219
  | `ToolCallStartEvent` | 工具调用开始(transient) | `tool_call_id`, `tool` |
220
+ | `ToolCallArgsProgressEvent` | 工具参数仍在流式生成中的 UI-only 进度(transient) | `tool_call_id`, `tool`, `estimated_tokens` |
220
221
  | `ToolCallEvent` | 工具调用入参确定 | `tool`, `args`, `tool_call_id`, `input` |
221
222
  | `ToolResultEvent` | 工具返回 | `tool`, `result`, `is_error`, `output` |
223
+ | `StreamDisplayResetEvent` | 内部 transient display reset;stream fallback 前要求客户端清理 partial display state;facade 不公开 | `reason` |
222
224
  | `StepStartEvent` / `StepCompleteEvent` | 单步边界 | — |
223
225
  | `StopEvent` | 流终止信号 | `reason` |
224
226
  | `UsageDeltaEvent` | token 用量 | — |
@@ -228,7 +230,10 @@ stdio bridge 明确不承担历史重放职责。需要历史请走 `comate resu
228
230
  **消费建议**:
229
231
  - 只关心最终答案:忽略所有 `*DeltaEvent`,只收集 `TextEvent`,以 `StopEvent` 为终止。
230
232
  - 想做打字机效果:按 `message_id` 累加 `TextDeltaEvent`,`TextEvent` 到达后用 `force_display` 决定是否覆盖。
231
- - 工具调用可视化:用 `ToolCallStartEvent` → `ToolCallEvent` → `ToolResultEvent` 的三段式。
233
+ - 工具调用可视化:用 `ToolCallStartEvent` → 可选多个 `ToolCallArgsProgressEvent` → `ToolCallEvent` → `ToolResultEvent`。
234
+ - stream fallback:收到 `StreamDisplayResetEvent(reason="stream_fallback")` 时清理任何 pending tool args progress / pending tool start 展示;不要把它显示为 assistant 内容。
235
+
236
+ `ToolCallArgsProgressEvent` 与 `StreamDisplayResetEvent` 都不是 assistant message content,也不应作为 transcript 内容持久化。`ToolCallArgsProgressEvent.estimated_tokens` 只用于 UI estimate,不进入 provider usage、context token budget 或 compaction 阈值。`ToolCallArgsProgressEvent` 在 facade 中属于 `include_partial_messages=True` 时才公开的 partial event;`StreamDisplayResetEvent` 是 CLI/RPC internal transient display control,facade adapter 会忽略。
232
237
 
233
238
  事件被序列化时,dataclass 字段直接展开到 `params`,并补一个 `event_type` 字段。无法被 JSON 序列化的对象会 fallback 为 `str(value)`。
234
239
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "comate-cli"
7
- version = "0.8.3"
7
+ version = "0.8.4"
8
8
  description = "Comate terminal CLI built on comate-agent-sdk"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11,<3.14"
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import gc
4
5
  import unittest
5
6
 
6
7
  from comate_agent_sdk.agent.recap import RecapResult
8
+ from comate_agent_sdk.llm.exceptions import ModelRateLimitError
7
9
  from comate_cli.terminal_agent.recap.controller import IdleRecapController, IdleRecapOptions
8
10
 
9
11
 
@@ -298,6 +300,59 @@ class TestIdleRecapController(unittest.IsolatedAsyncioTestCase):
298
300
 
299
301
  self.assertEqual(appended, [])
300
302
 
303
+ async def test_cancelled_generation_provider_error_is_observed(self) -> None:
304
+ appended: list[str] = []
305
+ started = asyncio.Event()
306
+ provider_error_raised = asyncio.Event()
307
+ loop = asyncio.get_running_loop()
308
+ previous_handler = loop.get_exception_handler()
309
+ unretrieved_contexts: list[dict[str, object]] = []
310
+
311
+ async def generate(parent_runtime):
312
+ del parent_runtime
313
+ started.set()
314
+ try:
315
+ await asyncio.sleep(60)
316
+ except asyncio.CancelledError:
317
+ provider_error_raised.set()
318
+ raise ModelRateLimitError("rate limited", model="fake:model")
319
+
320
+ def _collect_unretrieved(loop, context): # type: ignore[no-untyped-def]
321
+ del loop
322
+ if context.get("message") == "Task exception was never retrieved":
323
+ unretrieved_contexts.append(dict(context))
324
+ return
325
+ if previous_handler is not None:
326
+ previous_handler(asyncio.get_running_loop(), context)
327
+
328
+ controller = IdleRecapController(
329
+ options=IdleRecapOptions(enabled=True, delay_seconds=0),
330
+ generate=generate,
331
+ append=appended.append,
332
+ is_idle=lambda: True,
333
+ get_input_text=lambda: "",
334
+ )
335
+
336
+ loop.set_exception_handler(_collect_unretrieved)
337
+ try:
338
+ controller.arm_after_stop(
339
+ stop_seq=1,
340
+ parent_runtime=object(),
341
+ stop_reason="completed",
342
+ )
343
+ await started.wait()
344
+ controller.cancel_for_busy()
345
+ await controller.wait_for_current()
346
+ await asyncio.wait_for(provider_error_raised.wait(), timeout=3.0)
347
+ await asyncio.sleep(0)
348
+ gc.collect()
349
+ await asyncio.sleep(0)
350
+ finally:
351
+ loop.set_exception_handler(previous_handler)
352
+
353
+ self.assertEqual(appended, [])
354
+ self.assertEqual(unretrieved_contexts, [])
355
+
301
356
  async def test_generate_exception_does_not_poison_next_stop(self) -> None:
302
357
  appended: list[str] = []
303
358
  call_count = 0
@@ -7,6 +7,7 @@ from unittest.mock import patch
7
7
 
8
8
  from prompt_toolkit.completion import Completion
9
9
  from prompt_toolkit.document import Document
10
+ from prompt_toolkit.utils import get_cwidth
10
11
  from rich.text import Text
11
12
 
12
13
  from comate_cli.terminal_agent.event_renderer import ActiveLLMRetrySnapshot
@@ -68,6 +69,7 @@ class _FakeRenderer:
68
69
  self.running_tools = False
69
70
  self.auto_compacting = False
70
71
  self.hidden_thinking_badge = ""
72
+ self.tool_args_progress = ""
71
73
  self.todo_lines: list[str] = []
72
74
  self.tool_entries: list[tuple[int, str]] = [(0, "Agent(task) · 1s · +1 tool uses")]
73
75
  self.retry_snapshot: ActiveLLMRetrySnapshot | None = None
@@ -93,6 +95,9 @@ class _FakeRenderer:
93
95
  def hidden_thinking_badge_text(self) -> str:
94
96
  return self.hidden_thinking_badge
95
97
 
98
+ def tool_args_progress_text(self) -> str:
99
+ return self.tool_args_progress
100
+
96
101
  def active_llm_retry_snapshot(self) -> ActiveLLMRetrySnapshot | None:
97
102
  return self.retry_snapshot
98
103
 
@@ -436,6 +441,63 @@ class TestCompletionStatusPanel(unittest.TestCase):
436
441
  )
437
442
  )
438
443
 
444
+ def test_loading_text_appends_tool_args_progress_before_interrupt_hint(self) -> None:
445
+ tui = _FakeTUI(None, width=120)
446
+ tui._busy = True
447
+ tui._run_start_time = 780.0
448
+ tui._renderer.tool_args_progress = "↓ 1.2k tokens"
449
+ tui._renderer.hidden_thinking_badge = "ultra thinking"
450
+
451
+ with patch(
452
+ "comate_cli.terminal_agent.tui_parts.render_panels.time.monotonic",
453
+ return_value=1000.0,
454
+ ):
455
+ fragments = tui._loading_text()
456
+ text = _join_fragments(fragments)
457
+
458
+ self.assertIn(
459
+ "Thinking (3m 40s • ↓ 1.2k tokens • ctrl+c to interrupt • ultra thinking)",
460
+ text,
461
+ )
462
+
463
+ def test_loading_text_keeps_status_suffix_on_one_line_when_width_is_tight(self) -> None:
464
+ tui = _FakeTUI(None, width=40)
465
+ tui._busy = True
466
+ tui._run_start_time = 780.0
467
+ tui._renderer.tool_args_progress = "↓ 12.5k tokens"
468
+
469
+ with patch(
470
+ "comate_cli.terminal_agent.tui_parts.render_panels.time.monotonic",
471
+ return_value=1000.0,
472
+ ):
473
+ fragments = tui._loading_text()
474
+ text = _join_fragments(fragments)
475
+
476
+ self.assertNotIn("\n", text)
477
+ self.assertLessEqual(get_cwidth(text), 40)
478
+ self.assertIn("↓ 12.5k tokens", text)
479
+
480
+ def test_animator_loading_text_keeps_status_suffix_inside_tight_width(self) -> None:
481
+ tui = _FakeTUI(None, width=40)
482
+ tui._busy = True
483
+ tui._run_start_time = 780.0
484
+ tui._renderer.tool_args_progress = "↓ 12.5k tokens"
485
+ tui._animator = SimpleNamespace(
486
+ is_active=True,
487
+ renderable=lambda: Text("Generating a very long phrase"),
488
+ )
489
+
490
+ with patch(
491
+ "comate_cli.terminal_agent.tui_parts.render_panels.time.monotonic",
492
+ return_value=1000.0,
493
+ ):
494
+ fragments = tui._loading_text()
495
+ text = _join_fragments(fragments)
496
+
497
+ self.assertNotIn("\n", text)
498
+ self.assertLessEqual(get_cwidth(text), 40)
499
+ self.assertIn("↓ 12.5k tokens", text)
500
+
439
501
  def test_loading_text_appends_hidden_thinking_badge_to_active_animator(self) -> None:
440
502
  tui = _FakeTUI(None, width=80)
441
503
  tui._busy = True