comate-cli 0.7.0a5__tar.gz → 0.7.0a6__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 (165) hide show
  1. comate_cli-0.7.0a6/CHANGELOG.md +46 -0
  2. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/PKG-INFO +26 -1
  3. comate_cli-0.7.0a6/README.md +41 -0
  4. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/main.py +96 -2
  5. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/app.py +7 -5
  6. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui.py +6 -0
  7. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/input_behavior.py +41 -4
  8. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/key_bindings.py +3 -0
  9. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/pyproject.toml +1 -1
  10. comate_cli-0.7.0a6/tests/fixtures/fake_mcp_misbehaving_stdout.py +75 -0
  11. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_app_shutdown.py +66 -1
  12. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_compact_command_semantics.py +4 -4
  13. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_preflight.py +3 -2
  14. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_rewind_command_semantics.py +4 -1
  15. comate_cli-0.7.0a6/tests/test_shutdown_noise_guard.py +185 -0
  16. comate_cli-0.7.0a6/tests/test_shutdown_noise_integration.py +116 -0
  17. comate_cli-0.7.0a6/tests/test_tui_paste_newline_guard.py +198 -0
  18. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/uv.lock +2 -2
  19. comate_cli-0.7.0a5/CHANGELOG.md +0 -22
  20. comate_cli-0.7.0a5/README.md +0 -16
  21. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/.gitignore +0 -0
  22. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/bash-exit-code-green-dot-bug.md +0 -0
  23. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/__init__.py +0 -0
  24. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/__main__.py +0 -0
  25. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/mcp_cli.py +0 -0
  26. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/__init__.py +0 -0
  27. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/animations.py +0 -0
  28. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/assistant_render.py +0 -0
  29. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/codenames.py +0 -0
  30. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  31. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/env_utils.py +0 -0
  32. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/error_display.py +0 -0
  33. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/event_renderer.py +0 -0
  34. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/figures.py +0 -0
  35. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  36. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/history_printer.py +0 -0
  37. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/input_geometry.py +0 -0
  38. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  39. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/logging_adapter.py +0 -0
  40. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/logo.py +0 -0
  41. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/markdown_render.py +0 -0
  42. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/mention_completer.py +0 -0
  43. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/message_style.py +0 -0
  44. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/models.py +0 -0
  45. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/path_context_hint.py +0 -0
  46. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
  47. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
  48. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
  49. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
  50. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
  51. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
  52. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
  53. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
  54. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
  55. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
  56. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
  57. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
  58. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
  59. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/preflight.py +0 -0
  60. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/question_view.py +0 -0
  61. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/resume_picker.py +0 -0
  62. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/resume_preview.py +0 -0
  63. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/resume_selector.py +0 -0
  64. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  65. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  66. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/selection_menu.py +0 -0
  67. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/slash_commands.py +0 -0
  68. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/startup.py +0 -0
  69. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/status_bar.py +0 -0
  70. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/text_effects.py +0 -0
  71. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tips.py +0 -0
  72. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tool_result_formatters.py +0 -0
  73. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tool_result_store.py +0 -0
  74. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
  75. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tool_view.py +0 -0
  76. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
  77. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  78. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
  79. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
  80. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
  81. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
  82. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
  83. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  84. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  85. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/docs/hooks.md +0 -0
  86. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
  87. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
  88. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
  89. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/conftest.py +0 -0
  90. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_animator_shuffle.py +0 -0
  91. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_app_mcp_preload.py +0 -0
  92. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_app_preflight_gate.py +0 -0
  93. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_app_print_mode.py +0 -0
  94. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_app_usage_line.py +0 -0
  95. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_btw_slash_command.py +0 -0
  96. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_cli_project_root.py +0 -0
  97. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_completion_context_activation.py +0 -0
  98. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_completion_status_panel.py +0 -0
  99. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_context_command.py +0 -0
  100. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_custom_slash_commands.py +0 -0
  101. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_discover_tab.py +0 -0
  102. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_errors_tab.py +0 -0
  103. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_event_renderer.py +0 -0
  104. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_event_renderer_boundary.py +0 -0
  105. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_event_renderer_e2e.py +0 -0
  106. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_event_renderer_log_boundary.py +0 -0
  107. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_event_renderer_log_queue.py +0 -0
  108. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_event_renderer_streaming.py +0 -0
  109. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_format_error.py +0 -0
  110. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_handle_error.py +0 -0
  111. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_history_printer.py +0 -0
  112. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_history_printer_log.py +0 -0
  113. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_history_sync.py +0 -0
  114. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_input_behavior.py +0 -0
  115. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_input_history.py +0 -0
  116. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_installed_tab.py +0 -0
  117. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_interrupt_exit_semantics.py +0 -0
  118. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_layout_coordinator.py +0 -0
  119. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_logging_adapter.py +0 -0
  120. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_logo.py +0 -0
  121. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_main_args.py +0 -0
  122. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_markdown_render.py +0 -0
  123. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_marketplaces_tab.py +0 -0
  124. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_mcp_cli.py +0 -0
  125. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_mcp_slash_command.py +0 -0
  126. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_mention_completer.py +0 -0
  127. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_path_context_hint.py +0 -0
  128. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_plugin_slash_commands.py +0 -0
  129. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_plugin_tui_components.py +0 -0
  130. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_preflight_copilot.py +0 -0
  131. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_question_key_bindings.py +0 -0
  132. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_question_view.py +0 -0
  133. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_resume_picker.py +0 -0
  134. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_resume_preview.py +0 -0
  135. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_resume_selector.py +0 -0
  136. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_rpc_protocol.py +0 -0
  137. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_rpc_stdio_bridge.py +0 -0
  138. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_selection_menu.py +0 -0
  139. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_session_query_token_summary.py +0 -0
  140. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_skills_slash_command.py +0 -0
  141. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_slash_argument_hint.py +0 -0
  142. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_slash_completer.py +0 -0
  143. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_slash_registry.py +0 -0
  144. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_status_bar.py +0 -0
  145. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_status_bar_transient.py +0 -0
  146. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_task_panel_format.py +0 -0
  147. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_task_panel_key_bindings.py +0 -0
  148. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_task_panel_rendering.py +0 -0
  149. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_task_poll.py +0 -0
  150. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tool_result_formatters.py +0 -0
  151. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tool_result_store.py +0 -0
  152. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tool_result_viewer.py +0 -0
  153. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tool_result_viewer_key_bindings.py +0 -0
  154. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tool_view.py +0 -0
  155. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_transcript_viewer.py +0 -0
  156. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_elapsed_status.py +0 -0
  157. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_esc_queue.py +0 -0
  158. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_mcp_init_gate.py +0 -0
  159. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_paste_placeholder.py +0 -0
  160. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_queue_preview.py +0 -0
  161. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_queue_sdk_source.py +0 -0
  162. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_split_invariance.py +0 -0
  163. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_team_messages.py +0 -0
  164. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
  165. {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_update_check.py +0 -0
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ### Fixed
6
+
7
+ - Windows 上 Ctrl+C 退出时 `ERROR:mcp.client.stdio:Failed to parse JSONRPC message from server`
8
+ 和 traceback 漏到终端的问题。修复包含两部分:`app.py` finally 顺序调整,
9
+ 以及 SDK stdio MCP 关闭路径切换到主动信号升级链。
10
+ - Windows 上 Ctrl+C 退出时 `Exception ignored in: <BaseSubprocessTransport.__del__>`
11
+ 和 `ValueError: I/O operation on closed pipe` 漏到终端的问题。现在由
12
+ `_ShutdownNoiseGuard` catch-all 接管 `sys.unraisablehook` 并写入 agent log。
13
+
14
+ ### Added
15
+
16
+ - `_ShutdownNoiseGuard` 扩展为四合一接管器:SIGINT、`sys.unraisablehook`、
17
+ `sys.excepthook`、`warnings.showwarning`。捕获到的噪音统一路由到独立的
18
+ `comate.noise` logger(`propagate=False`),写入 `~/.comate/logs/agent.log`。
19
+ - 新增 `COMATE_DEBUG_NOISE=1` 环境变量逃生口,开发排障时可关闭噪音接管器,
20
+ 查看旧版本的原始 stderr 输出。
21
+
22
+ ### Changed
23
+
24
+ - TUI 分支退出时先执行 `_graceful_shutdown(...)`,再关闭 `logging_session`,
25
+ 让现有 logging 屏蔽机制覆盖 MCP teardown 全程。
26
+
27
+ ## 0.7.0a1 - 2026-05-19 (alpha)
28
+
29
+ > 这是一个 **alpha 预发布版本**,用于内部 / 早期测试。
30
+ > `pip install comate-cli` 默认不会安装到本版本,需要显式 `pip install comate-cli==0.7.0a1` 或 `pip install --pre comate-cli`。
31
+
32
+ ### Breaking Changes
33
+
34
+ - 依赖 `comate-agent-sdk>=0.8.0a1`,与旧 SDK 不兼容。
35
+ - 旧版本生成的 resume jsonl / context 快照不保证可以正常回放,**建议从空 session 重新开始**。
36
+
37
+ ### 累积变更
38
+
39
+ 自 0.6.x 起累积大量 TUI 与会话能力改进,涵盖:
40
+
41
+ - Resume picker 重做:搜索、跨 cwd 切换、预览模式、视觉抛光
42
+ - Auto compact 期间显式提示 `Compacting context`
43
+ - Team / subagent 事件透出与渲染统一
44
+ - 与 SDK 0.8.0a2 配套的 tool envelope / token 账本展示
45
+
46
+ 详细变更请参考 git log。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comate-cli
3
- Version: 0.7.0a5
3
+ Version: 0.7.0a6
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
@@ -41,3 +41,28 @@ Or install globally:
41
41
  uv tool install comate-cli
42
42
  comate
43
43
  ```
44
+
45
+ ## 排错 / Troubleshooting
46
+
47
+ ### 看不到错误信息但行为异常
48
+
49
+ comate 在生产模式下会把若干无害但吵闹的 shutdown 噪音写入
50
+ `~/.comate/logs/agent.log`,而不是显示在终端里。这包括 Windows asyncio
51
+ shutdown 的 `ResourceWarning`、MCP 服务器违规向 stdout 写日志导致的 JSONRPC
52
+ 解析失败,以及解释器退出阶段的 unraisable 异常。
53
+
54
+ 如果你怀疑隐藏了真实错误,可以按下面顺序排查:
55
+
56
+ ```bash
57
+ # 1. 看 noise 通道的最近 50 条
58
+ grep -E "\[unraisable\]|\[excepthook\]|\[warning\]" ~/.comate/logs/agent.log | tail -50
59
+
60
+ # 2. 看完整 agent.log 末尾
61
+ tail -200 ~/.comate/logs/agent.log
62
+
63
+ # 3. 临时关掉噪音接管器,看原始输出(开发场景)
64
+ COMATE_DEBUG_NOISE=1 comate
65
+ ```
66
+
67
+ `COMATE_DEBUG_NOISE=1` 设置后,`_ShutdownNoiseGuard` 会跳过 install,
68
+ 错误会像旧版本一样直接打印到 stderr。
@@ -0,0 +1,41 @@
1
+ # comate-cli
2
+
3
+ Comate terminal CLI package.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ uvx comate-cli
9
+ ```
10
+
11
+ Or install globally:
12
+
13
+ ```bash
14
+ uv tool install comate-cli
15
+ comate
16
+ ```
17
+
18
+ ## 排错 / Troubleshooting
19
+
20
+ ### 看不到错误信息但行为异常
21
+
22
+ comate 在生产模式下会把若干无害但吵闹的 shutdown 噪音写入
23
+ `~/.comate/logs/agent.log`,而不是显示在终端里。这包括 Windows asyncio
24
+ shutdown 的 `ResourceWarning`、MCP 服务器违规向 stdout 写日志导致的 JSONRPC
25
+ 解析失败,以及解释器退出阶段的 unraisable 异常。
26
+
27
+ 如果你怀疑隐藏了真实错误,可以按下面顺序排查:
28
+
29
+ ```bash
30
+ # 1. 看 noise 通道的最近 50 条
31
+ grep -E "\[unraisable\]|\[excepthook\]|\[warning\]" ~/.comate/logs/agent.log | tail -50
32
+
33
+ # 2. 看完整 agent.log 末尾
34
+ tail -200 ~/.comate/logs/agent.log
35
+
36
+ # 3. 临时关掉噪音接管器,看原始输出(开发场景)
37
+ COMATE_DEBUG_NOISE=1 comate
38
+ ```
39
+
40
+ `COMATE_DEBUG_NOISE=1` 设置后,`_ShutdownNoiseGuard` 会跳过 install,
41
+ 错误会像旧版本一样直接打印到 stderr。
@@ -3,9 +3,11 @@ from __future__ import annotations
3
3
  import atexit
4
4
  import asyncio
5
5
  import logging
6
+ import os
6
7
  import signal
7
8
  import subprocess
8
9
  import sys
10
+ import warnings
9
11
  try:
10
12
  import termios
11
13
  except ImportError:
@@ -68,10 +70,21 @@ class _ShutdownNoiseGuard:
68
70
 
69
71
  def __init__(self) -> None:
70
72
  self._shutdown_armed = False
73
+ self._installed = False
71
74
  self._orig_unraisablehook = sys.unraisablehook
75
+ self._orig_excepthook = sys.excepthook
76
+ self._orig_showwarning = warnings.showwarning
77
+ self._noise_logger = self._build_noise_logger()
72
78
 
73
79
  def install(self) -> None:
80
+ if self._installed:
81
+ return
82
+ if os.environ.get("COMATE_DEBUG_NOISE", "").strip().lower() in ("1", "true", "yes"):
83
+ return
74
84
  sys.unraisablehook = self._unraisablehook
85
+ sys.excepthook = self._excepthook
86
+ warnings.showwarning = self._showwarning
87
+ self._installed = True
75
88
 
76
89
  def begin_shutdown(self) -> None:
77
90
  if self._shutdown_armed:
@@ -88,13 +101,94 @@ class _ShutdownNoiseGuard:
88
101
  except Exception as exc:
89
102
  logger.debug(f"Failed to switch SIGINT to SIG_IGN during shutdown: {exc}")
90
103
 
104
+ def _route(
105
+ self,
106
+ channel: str,
107
+ *,
108
+ exc: BaseException | None = None,
109
+ msg: str = "",
110
+ **extra: object,
111
+ ) -> None:
112
+ try:
113
+ if exc is not None:
114
+ self._noise_logger.warning(
115
+ "[%s] %s: %s",
116
+ channel,
117
+ type(exc).__name__,
118
+ exc,
119
+ exc_info=(type(exc), exc, exc.__traceback__),
120
+ extra={"noise_channel": channel, **extra},
121
+ )
122
+ else:
123
+ self._noise_logger.warning(
124
+ "[%s] %s",
125
+ channel,
126
+ msg,
127
+ extra={"noise_channel": channel, **extra},
128
+ )
129
+ except Exception:
130
+ pass
131
+
91
132
  def _unraisablehook(self, unraisable: object) -> None:
92
133
  exc_value = getattr(unraisable, "exc_value", None)
93
134
  if self._shutdown_armed and isinstance(exc_value, KeyboardInterrupt):
94
- logger.debug("Suppressed unraisable KeyboardInterrupt during shutdown")
95
135
  return
136
+ obj = getattr(unraisable, "object", None)
137
+ self._route("unraisable", exc=exc_value, object_repr=repr(obj))
138
+
139
+ def _excepthook(
140
+ self,
141
+ exc_type: type[BaseException],
142
+ exc_value: BaseException,
143
+ exc_tb: object,
144
+ ) -> None:
145
+ self._route("excepthook", exc=exc_value)
146
+
147
+ def _showwarning(
148
+ self,
149
+ message: Warning | str,
150
+ category: type[Warning],
151
+ filename: str,
152
+ lineno: int,
153
+ file: object | None = None,
154
+ line: str | None = None,
155
+ ) -> None:
156
+ self._route(
157
+ "warning",
158
+ msg=str(message),
159
+ category=category.__name__,
160
+ warning_filename=str(filename),
161
+ warning_lineno=lineno,
162
+ )
163
+
164
+ def _build_noise_logger(self) -> logging.Logger:
165
+ log = logging.getLogger("comate.noise")
166
+ log.setLevel(logging.WARNING)
167
+ log.propagate = False
168
+ if log.handlers:
169
+ return log
170
+
171
+ try:
172
+ from concurrent_log_handler import ConcurrentRotatingFileHandler
173
+
174
+ log_dir = os.path.join(os.path.expanduser("~"), ".comate", "logs")
175
+ os.makedirs(log_dir, exist_ok=True)
176
+ handler = ConcurrentRotatingFileHandler(
177
+ os.path.join(log_dir, "agent.log"),
178
+ maxBytes=10 * 1024 * 1024,
179
+ backupCount=3,
180
+ encoding="utf-8",
181
+ )
182
+ handler.setFormatter(
183
+ logging.Formatter(
184
+ "%(asctime)s %(levelname)s %(name)s [%(noise_channel)s]: %(message)s"
185
+ )
186
+ )
187
+ log.addHandler(handler)
188
+ except Exception:
189
+ log.addHandler(logging.NullHandler())
96
190
 
97
- self._orig_unraisablehook(unraisable)
191
+ return log
98
192
 
99
193
 
100
194
  def _usage_text() -> str:
@@ -415,11 +415,13 @@ async def run(
415
415
  exc_info=True,
416
416
  )
417
417
  finally:
418
- logging_session.close()
419
- if active_session is session:
420
- await _graceful_shutdown(active_session)
421
- else:
422
- await _graceful_shutdown(session, active_session)
418
+ try:
419
+ if active_session is session:
420
+ await _graceful_shutdown(active_session)
421
+ else:
422
+ await _graceful_shutdown(session, active_session)
423
+ finally:
424
+ logging_session.close()
423
425
 
424
426
  if usage_line:
425
427
  console.print(f"[dim]{usage_line}[/]")
@@ -274,6 +274,12 @@ class TerminalAgentTUI(
274
274
  "AGENT_SDK_TUI_PASTE_PLACEHOLDER_THRESHOLD_CHARS",
275
275
  500,
276
276
  )
277
+ paste_guard_window_ms = read_env_int(
278
+ "AGENT_SDK_TUI_PASTE_GUARD_WINDOW_MS",
279
+ 120,
280
+ )
281
+ self._paste_guard_window_seconds = paste_guard_window_ms / 1000.0
282
+ self._paste_guard_active_until = 0.0
277
283
  self._paste_placeholder_text: str | None = None
278
284
  self._active_paste_token: str | None = None
279
285
  self._paste_payload_by_token: dict[str, str] = {}
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import time
3
4
  from bisect import bisect_right
4
5
  from typing import Any
5
6
 
@@ -212,6 +213,35 @@ class InputBehaviorMixin:
212
213
  self._active_paste_token = None
213
214
  self._paste_placeholder_text = None
214
215
  self._paste_payload_by_token.clear()
216
+ self._paste_guard_active_until = 0.0
217
+
218
+ def _paste_guard_window(self) -> float:
219
+ return max(0.05, float(getattr(self, "_paste_guard_window_seconds", 0.12)))
220
+
221
+ def _mark_paste_like_input(self, _text: str = "") -> None:
222
+ self._paste_guard_active_until = time.monotonic() + self._paste_guard_window()
223
+
224
+ def _extend_paste_guard(self) -> None:
225
+ self._mark_paste_like_input("")
226
+
227
+ def _is_paste_guard_active(self) -> bool:
228
+ return time.monotonic() < float(
229
+ getattr(self, "_paste_guard_active_until", 0.0)
230
+ )
231
+
232
+ def _should_treat_enter_as_pasted_newline(self, buffer: Any) -> bool:
233
+ if not self._is_paste_guard_active():
234
+ return False
235
+ if getattr(buffer, "complete_state", None) is not None:
236
+ return False
237
+ return True
238
+
239
+ def _handle_normal_enter_key(self, buffer: Any) -> bool:
240
+ if not self._should_treat_enter_as_pasted_newline(buffer):
241
+ return False
242
+ buffer.insert_text("\n")
243
+ self._extend_paste_guard()
244
+ return True
215
245
 
216
246
  @staticmethod
217
247
  def _find_inserted_segment(
@@ -258,14 +288,23 @@ class InputBehaviorMixin:
258
288
  def _handle_large_paste(self, buffer: Any) -> bool:
259
289
  if self._suppress_input_change_hook:
260
290
  return False
261
- if self._busy:
262
- return False
263
291
 
264
292
  text = str(buffer.text)
265
293
  previous_text = self._last_input_text
266
294
  self._last_input_len = len(text)
267
295
  self._last_input_text = text
268
296
 
297
+ threshold = max(1, int(self._paste_threshold_chars))
298
+ segment = self._find_inserted_segment(previous_text, text)
299
+ inserted_text = segment[2] if segment is not None else ""
300
+ if inserted_text and ("\n" in inserted_text or "\r" in inserted_text):
301
+ self._mark_paste_like_input(inserted_text)
302
+ if len(inserted_text) >= threshold:
303
+ self._mark_paste_like_input(inserted_text)
304
+
305
+ if self._busy:
306
+ return False
307
+
269
308
  placeholder = self._paste_placeholder_text
270
309
  if self._active_paste_token is not None and placeholder and placeholder in text:
271
310
  return False
@@ -276,8 +315,6 @@ class InputBehaviorMixin:
276
315
  ):
277
316
  self._clear_paste_state()
278
317
 
279
- threshold = max(1, int(self._paste_threshold_chars))
280
- segment = self._find_inserted_segment(previous_text, text)
281
318
  if segment is None:
282
319
  return False
283
320
  start, end, inserted_text = segment
@@ -411,6 +411,9 @@ class KeyBindingsMixin:
411
411
  @bindings.add("enter", filter=normal_mode, eager=True)
412
412
  def _enter(event) -> None:
413
413
  buffer = event.current_buffer
414
+ if self._handle_normal_enter_key(buffer):
415
+ return
416
+
414
417
  cs = buffer.complete_state
415
418
 
416
419
  # 菜单打开时:先接受补全
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "comate-cli"
7
- version = "0.7.0a5"
7
+ version = "0.7.0a6"
8
8
  description = "Comate terminal CLI built on comate-agent-sdk"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,75 @@
1
+ """故意违反 MCP stdio 规范的假服务器。"""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import signal
6
+ import sys
7
+ import threading
8
+ import time
9
+
10
+
11
+ _running = True
12
+
13
+
14
+ def _read_loop() -> None:
15
+ """从 stdin 读 JSONRPC 请求,对 initialize 回一个最小响应。"""
16
+ for line in sys.stdin:
17
+ line = line.strip()
18
+ if not line:
19
+ continue
20
+ try:
21
+ req = json.loads(line)
22
+ except Exception:
23
+ continue
24
+ if req.get("method") == "initialize":
25
+ resp = {
26
+ "jsonrpc": "2.0",
27
+ "id": req.get("id"),
28
+ "result": {
29
+ "protocolVersion": "2024-11-05",
30
+ "capabilities": {},
31
+ "serverInfo": {
32
+ "name": "fake-misbehaving",
33
+ "version": "0.0.1",
34
+ },
35
+ },
36
+ }
37
+ sys.stdout.write(json.dumps(resp) + "\n")
38
+ sys.stdout.flush()
39
+
40
+
41
+ def _noise_loop() -> None:
42
+ """每 50ms 向 stdout 写一行彩色日志。"""
43
+ counter = 0
44
+ while _running:
45
+ sys.stdout.write(
46
+ f"\x1b[90m2026-05-19T02:29:{counter:02d}Z "
47
+ "[INFO] internal status tick\x1b[0m\n"
48
+ )
49
+ sys.stdout.flush()
50
+ counter = (counter + 1) % 60
51
+ time.sleep(0.05)
52
+
53
+
54
+ def _on_signal(signum, frame) -> None:
55
+ global _running
56
+ _running = False
57
+ try:
58
+ sys.stdout.write(
59
+ "\x1b[90m2026-05-19T02:29:99Z "
60
+ "[INFO] shutting down gracefully\x1b[0m\n"
61
+ )
62
+ sys.stdout.flush()
63
+ except Exception:
64
+ pass
65
+ raise SystemExit(0)
66
+
67
+
68
+ if __name__ == "__main__":
69
+ signal.signal(signal.SIGINT, _on_signal)
70
+ if sys.platform == "win32":
71
+ signal.signal(signal.SIGTERM, _on_signal)
72
+ threading.Thread(target=_read_loop, daemon=True).start()
73
+ threading.Thread(target=_noise_loop, daemon=True).start()
74
+ while True:
75
+ time.sleep(0.5)
@@ -2,7 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import unittest
4
4
  from contextlib import contextmanager
5
- from unittest.mock import MagicMock, call, patch
5
+ from types import SimpleNamespace
6
+ from unittest.mock import AsyncMock, MagicMock, call, patch
6
7
 
7
8
  from comate_cli.terminal_agent import app as app_module
8
9
 
@@ -20,6 +21,70 @@ class _FakeSession:
20
21
 
21
22
 
22
23
  class TestAppShutdown(unittest.IsolatedAsyncioTestCase):
24
+ async def test_run_closes_logging_after_graceful_shutdown(self) -> None:
25
+ call_log: list[str] = []
26
+
27
+ fake_logging_session = MagicMock()
28
+ fake_logging_session.close.side_effect = lambda: call_log.append("logging_close")
29
+
30
+ fake_session = MagicMock()
31
+ fake_session.session_id = "session-1"
32
+ fake_session.runtime = SimpleNamespace(_mcp_manager=None)
33
+ fake_session.get_mode.return_value = "default"
34
+ fake_session.get_usage = AsyncMock(return_value=object())
35
+
36
+ class _FakeStatusBar:
37
+ def __init__(self, session) -> None:
38
+ self.session = session
39
+
40
+ def set_mode(self, mode) -> None:
41
+ pass
42
+
43
+ async def refresh(self) -> None:
44
+ pass
45
+
46
+ def show_transient(self, message: str, *, duration_s: float) -> None:
47
+ pass
48
+
49
+ class _FakeTUI:
50
+ initialized_session_id = None
51
+
52
+ def __init__(self, session, status_bar, renderer) -> None:
53
+ self.session = session
54
+
55
+ def add_resume_history(self, mode: str) -> None:
56
+ pass
57
+
58
+ async def run(self, *, mcp_init) -> None:
59
+ call_log.append("tui_run")
60
+
61
+ async def fake_preflight(*args, **kwargs):
62
+ return SimpleNamespace(should_abort_launch=False)
63
+
64
+ async def fake_graceful(*sessions) -> None:
65
+ call_log.append("graceful_shutdown")
66
+
67
+ with (
68
+ patch.object(app_module, "_resolve_cli_project_root", return_value=SimpleNamespace()),
69
+ patch.object(app_module, "run_preflight_if_needed", side_effect=fake_preflight),
70
+ patch.object(app_module, "_build_agent", return_value=object()),
71
+ patch.object(app_module, "print_logo"),
72
+ patch.object(app_module, "EventRenderer", return_value=MagicMock()),
73
+ patch(
74
+ "comate_cli.terminal_agent.logging_adapter.setup_tui_logging",
75
+ return_value=fake_logging_session,
76
+ ),
77
+ patch.object(app_module, "_resolve_session", return_value=(fake_session, "new")),
78
+ patch.object(app_module, "_check_update", return_value=None),
79
+ patch.object(app_module, "StatusBar", _FakeStatusBar),
80
+ patch.object(app_module, "TerminalAgentTUI", _FakeTUI),
81
+ patch.object(app_module, "_format_exit_usage_line", return_value=None),
82
+ patch.object(app_module, "_graceful_shutdown", side_effect=fake_graceful),
83
+ ):
84
+ await app_module.run()
85
+
86
+ self.assertEqual(call_log, ["tui_run", "graceful_shutdown", "logging_close"])
87
+
23
88
  async def test_graceful_shutdown_deduplicates_sessions_and_flushes(self) -> None:
24
89
  events: list[str] = []
25
90
 
@@ -149,7 +149,7 @@ class TestCompactCommandSemantics(unittest.TestCase):
149
149
  self.assertEqual(commands._status_bar.refresh_calls, 1)
150
150
  self.assertTrue(
151
151
  any(
152
- kind == "subtitle" and "/compact completed:" in msg
152
+ kind == "subtitle" and "compact completed:" in msg
153
153
  for kind, msg, _ in commands._renderer.events
154
154
  )
155
155
  )
@@ -167,7 +167,7 @@ class TestCompactCommandSemantics(unittest.TestCase):
167
167
  asyncio.run(commands._execute_command("/compact"))
168
168
 
169
169
  self.assertEqual(commands._renderer.events[-1][0], "subtitle")
170
- self.assertEqual(commands._renderer.events[-1][1], "/compact skipped: disabled")
170
+ self.assertEqual(commands._renderer.events[-1][1], "compact skipped: disabled")
171
171
  self.assertEqual(commands._renderer.events[-1][2], "warning")
172
172
 
173
173
  def test_compact_reports_no_changes_in_english(self) -> None:
@@ -184,7 +184,7 @@ class TestCompactCommandSemantics(unittest.TestCase):
184
184
  self.assertEqual(commands._renderer.events[-1][0], "subtitle")
185
185
  self.assertEqual(
186
186
  commands._renderer.events[-1][1],
187
- "/compact made no changes: The context was left unchanged.",
187
+ "compact made no changes: The context was left unchanged.",
188
188
  )
189
189
  self.assertEqual(commands._renderer.events[-1][2], "warning")
190
190
 
@@ -205,7 +205,7 @@ class TestCompactCommandSemantics(unittest.TestCase):
205
205
  ("user", "/compact", "info"),
206
206
  (
207
207
  "subtitle",
208
- "/compact made no changes: The context was left unchanged.",
208
+ "compact made no changes: The context was left unchanged.",
209
209
  "warning",
210
210
  ),
211
211
  ],
@@ -196,11 +196,11 @@ class TestPreflightMerge(unittest.TestCase):
196
196
  self.assertIn("zai_global", preset_map)
197
197
  self.assertEqual(
198
198
  preset_map["zai_cn"].base_url,
199
- "https://open.bigmodel.cn/api/anthropic",
199
+ "https://open.bigmodel.cn/api/coding/paas/v4",
200
200
  )
201
201
  self.assertEqual(
202
202
  preset_map["zai_global"].base_url,
203
- "https://api.z.ai/api/anthropic",
203
+ "https://api.z.ai/api/coding/paas/v4",
204
204
  )
205
205
 
206
206
  def test_provider_presets_are_brand_only(self) -> None:
@@ -216,6 +216,7 @@ class TestPreflightMerge(unittest.TestCase):
216
216
  "OpenAI and compatible",
217
217
  "Anthropic and compatible",
218
218
  "GitHub Copilot Plan",
219
+ "Xiaomi MiMo Token Plan",
219
220
  ],
220
221
  )
221
222
 
@@ -144,7 +144,10 @@ class TestRewindCommandSemantics(unittest.TestCase):
144
144
 
145
145
  asyncio.run(commands._slash_rewind(""))
146
146
 
147
- self.assertIn("暂时没有可以回到的文字消息", commands._renderer.messages[-1][0])
147
+ self.assertIn(
148
+ "No rewind-able text messages are available",
149
+ commands._renderer.messages[-1][0],
150
+ )
148
151
  self.assertNotIn("active replay", commands._renderer.messages[-1][0])
149
152
  self.assertNotIn("meta", commands._renderer.messages[-1][0])
150
153