comate-cli 0.7.0a7__tar.gz → 0.7.0a9__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 (178) hide show
  1. comate_cli-0.7.0a9/AGENTS.md +115 -0
  2. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/CHANGELOG.md +4 -0
  3. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/PKG-INFO +1 -1
  4. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/animations.py +29 -5
  5. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/event_renderer.py +102 -15
  6. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/history_printer.py +86 -17
  7. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/logging_adapter.py +26 -0
  8. comate_cli-0.7.0a9/comate_cli/terminal_agent/markdown_render.py +138 -0
  9. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/models.py +2 -0
  10. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/slash_commands.py +5 -0
  11. comate_cli-0.7.0a9/comate_cli/terminal_agent/tool_fold.py +206 -0
  12. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tool_result_formatters.py +65 -33
  13. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/transcript_viewer.py +60 -27
  14. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tui.py +23 -14
  15. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tui_parts/commands.py +76 -40
  16. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tui_parts/history_sync.py +45 -3
  17. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tui_parts/input_behavior.py +13 -4
  18. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tui_parts/key_bindings.py +2 -2
  19. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tui_parts/render_panels.py +93 -10
  20. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/pyproject.toml +1 -1
  21. comate_cli-0.7.0a9/report.md +382 -0
  22. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_completion_status_panel.py +43 -0
  23. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_event_renderer.py +102 -28
  24. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_event_renderer_streaming.py +32 -1
  25. comate_cli-0.7.0a9/tests/test_event_renderer_tool_fold.py +146 -0
  26. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_history_printer.py +57 -1
  27. comate_cli-0.7.0a9/tests/test_history_printer_subtitle_position.py +78 -0
  28. comate_cli-0.7.0a9/tests/test_history_printer_tool_fold.py +78 -0
  29. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_history_sync.py +29 -5
  30. comate_cli-0.7.0a9/tests/test_history_sync_tool_fold.py +98 -0
  31. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_logging_adapter.py +43 -0
  32. comate_cli-0.7.0a9/tests/test_markdown_render.py +102 -0
  33. comate_cli-0.7.0a9/tests/test_plugin_slash_commands.py +107 -0
  34. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_skills_slash_command.py +3 -27
  35. comate_cli-0.7.0a9/tests/test_slash_clear.py +350 -0
  36. comate_cli-0.7.0a9/tests/test_tool_fold.py +89 -0
  37. comate_cli-0.7.0a9/tests/test_tool_fold_panel.py +159 -0
  38. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tool_result_formatters.py +163 -12
  39. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_transcript_viewer.py +64 -6
  40. comate_cli-0.7.0a9/tests/test_transcript_viewer_tool_fold.py +47 -0
  41. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tui_paste_placeholder.py +46 -1
  42. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tui_queue_sdk_source.py +35 -11
  43. comate_cli-0.7.0a9/tests/test_tui_thinking_display.py +71 -0
  44. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/uv.lock +2 -2
  45. comate_cli-0.7.0a7/comate_cli/terminal_agent/markdown_render.py +0 -26
  46. comate_cli-0.7.0a7/tests/test_markdown_render.py +0 -53
  47. comate_cli-0.7.0a7/tests/test_plugin_slash_commands.py +0 -17
  48. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/.gitignore +0 -0
  49. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/README.md +0 -0
  50. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/bash-exit-code-green-dot-bug.md +0 -0
  51. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/__init__.py +0 -0
  52. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/__main__.py +0 -0
  53. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/main.py +0 -0
  54. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/mcp_cli.py +0 -0
  55. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/__init__.py +0 -0
  56. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/app.py +0 -0
  57. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/assistant_render.py +0 -0
  58. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/codenames.py +0 -0
  59. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  60. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/env_utils.py +0 -0
  61. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/error_display.py +0 -0
  62. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/figures.py +0 -0
  63. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  64. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/input_geometry.py +0 -0
  65. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  66. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/logo.py +0 -0
  67. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/mention_completer.py +0 -0
  68. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/message_style.py +0 -0
  69. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/path_context_hint.py +0 -0
  70. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
  71. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
  72. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
  73. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
  74. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
  75. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
  76. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
  77. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
  78. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
  79. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
  80. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
  81. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
  82. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
  83. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/preflight.py +0 -0
  84. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/question_view.py +0 -0
  85. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/resume_picker.py +0 -0
  86. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/resume_preview.py +0 -0
  87. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/resume_selector.py +0 -0
  88. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  89. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  90. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/selection_menu.py +0 -0
  91. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/startup.py +0 -0
  92. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/status_bar.py +0 -0
  93. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/text_effects.py +0 -0
  94. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tips.py +0 -0
  95. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tool_result_store.py +0 -0
  96. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
  97. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tool_view.py +0 -0
  98. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  99. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
  100. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
  101. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  102. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  103. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/docs/hooks.md +0 -0
  104. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
  105. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
  106. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
  107. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/conftest.py +0 -0
  108. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/fixtures/fake_mcp_misbehaving_stdout.py +0 -0
  109. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_animator_shuffle.py +0 -0
  110. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_app_mcp_preload.py +0 -0
  111. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_app_preflight_gate.py +0 -0
  112. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_app_print_mode.py +0 -0
  113. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_app_shutdown.py +0 -0
  114. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_app_usage_line.py +0 -0
  115. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_btw_slash_command.py +0 -0
  116. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_cli_project_root.py +0 -0
  117. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_compact_command_semantics.py +0 -0
  118. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_completion_context_activation.py +0 -0
  119. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_context_command.py +0 -0
  120. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_custom_slash_commands.py +0 -0
  121. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_discover_tab.py +0 -0
  122. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_errors_tab.py +0 -0
  123. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_event_renderer_boundary.py +0 -0
  124. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_event_renderer_e2e.py +0 -0
  125. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_event_renderer_log_boundary.py +0 -0
  126. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_event_renderer_log_queue.py +0 -0
  127. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_format_error.py +0 -0
  128. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_handle_error.py +0 -0
  129. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_history_printer_log.py +0 -0
  130. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_input_behavior.py +0 -0
  131. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_input_history.py +0 -0
  132. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_installed_tab.py +0 -0
  133. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_interrupt_exit_semantics.py +0 -0
  134. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_layout_coordinator.py +0 -0
  135. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_logo.py +0 -0
  136. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_main_args.py +0 -0
  137. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_marketplaces_tab.py +0 -0
  138. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_mcp_cli.py +0 -0
  139. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_mcp_slash_command.py +0 -0
  140. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_mention_completer.py +0 -0
  141. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_path_context_hint.py +0 -0
  142. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_plugin_tui_components.py +0 -0
  143. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_preflight.py +0 -0
  144. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_preflight_copilot.py +0 -0
  145. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_question_key_bindings.py +0 -0
  146. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_question_view.py +0 -0
  147. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_resume_picker.py +0 -0
  148. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_resume_preview.py +0 -0
  149. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_resume_selector.py +0 -0
  150. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_rewind_command_semantics.py +0 -0
  151. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_rpc_protocol.py +0 -0
  152. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_rpc_stdio_bridge.py +0 -0
  153. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_selection_menu.py +0 -0
  154. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_session_query_token_summary.py +0 -0
  155. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_shutdown_noise_guard.py +0 -0
  156. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_shutdown_noise_integration.py +0 -0
  157. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_slash_argument_hint.py +0 -0
  158. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_slash_completer.py +0 -0
  159. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_slash_registry.py +0 -0
  160. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_status_bar.py +0 -0
  161. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_status_bar_transient.py +0 -0
  162. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_task_panel_format.py +0 -0
  163. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_task_panel_key_bindings.py +0 -0
  164. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_task_panel_rendering.py +0 -0
  165. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_task_poll.py +0 -0
  166. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tool_result_store.py +0 -0
  167. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tool_result_viewer.py +0 -0
  168. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tool_result_viewer_key_bindings.py +0 -0
  169. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tool_view.py +0 -0
  170. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tui_elapsed_status.py +0 -0
  171. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tui_esc_queue.py +0 -0
  172. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tui_mcp_init_gate.py +0 -0
  173. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tui_paste_newline_guard.py +0 -0
  174. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tui_queue_preview.py +0 -0
  175. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tui_split_invariance.py +0 -0
  176. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tui_team_messages.py +0 -0
  177. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
  178. {comate_cli-0.7.0a7 → comate_cli-0.7.0a9}/tests/test_update_check.py +0 -0
@@ -0,0 +1,115 @@
1
+ ## global rule
2
+ 1. 涉及到 Prompt_toolkit 这个TUI框架, 你必须使用 context7 mcp里面的 query-docs和resolve-library-id 功能来查询相关文档, 以确保你对这个框架的理解是正确的. this is MUST
3
+ 2. 总是使用中文编写详细的git commit message
4
+
5
+ ## 12条黄金规则
6
+
7
+ 这些规则适用于本项目中的每一个任务,除非被明确覆盖。核心原则:非简单任务宁可谨慎,不要图快。简单任务可以灵活处理。
8
+
9
+ 1. 动手之前先动脑
10
+ 把你的假设说出来。不确定就问,别猜。遇到模糊需求时,列出多种可能的理解。如果有更简单的方案,要敢于提出来。搞不清楚的时候就停下来,把不清楚的点明确说出来。
11
+
12
+ 2. 简单至上
13
+ 写能解决问题的最少代码,不写投机性代码。不加需求以外的功能。一次性代码不搞抽象封装。自测标准:一个资深工程师看了会不会觉得过度设计?如果会,就简化。
14
+
15
+ 3. 精准修改,不多动
16
+ 只改必须改的地方。只清理自己弄乱的部分。不要顺手"优化"旁边的代码、注释或格式。没坏的东西不要重构。风格跟已有代码保持一致。
17
+
18
+ 4. 以目标驱动执行
19
+ 先定义成功标准,然后循环迭代直到验证通过。不要机械地按步骤走,而是盯着目标不断调整。有了清晰的成功标准,你才能自主地持续迭代。
20
+
21
+ 5. 模型只用于需要判断力的场景
22
+ 适合用 AI 做的事:分类、起草、总结、信息提取。不该用 AI 做的事:路由分发、重试逻辑、确定性转换。如果写代码就能搞定,就用代码搞定。
23
+
24
+ 6. 核实优先于推断(冰山法则)
25
+ 你在上下文中看到的信息只是冰山一角,不能代替真实验证。
26
+ 判断标准不是"我有没有说出猜测的话",而是"我这个结论背后有没有本轮刚验证过的事实支撑"——只要要陈述文件/函数/配置内容或修改代码,却没有刚读过、刚确认过,就是在猜,必须先验证再继续。
27
+
28
+ 7. 遇到冲突要选边,不要和稀泥
29
+ 两种模式互相矛盾时,选一个(优先选更新的或经过更多验证的),说明理由,把另一个标记为待清理。不要把两种冲突的模式混在一起用。
30
+
31
+ 8. 先读再写
32
+ 写新代码之前,先读清楚现有的导出接口、直接调用方、公共工具函数。"看起来互不相关"这种判断很危险。如果不理解代码为什么这样写,先问。
33
+
34
+ 9. 测试要验证意图,不只是行为
35
+ 测试必须体现某个行为**为什么**重要,而不仅仅是**做了什么**。如果业务逻辑变了,测试却不会挂,那这个测试就是有问题的。
36
+
37
+ 10. 每完成一个重要步骤就做检查点
38
+ 总结一下:做了什么、验证了什么、还剩什么。不要在一个自己都说不清楚的状态上继续推进。如果失去了方向感,停下来,重新陈述当前状态。
39
+
40
+ 11. 跟着项目的既有约定走,哪怕你不认同
41
+ 在项目内部,一致性 > 个人偏好。如果你真觉得某个约定有害,把问题提出来讨论,但不要悄悄另搞一套。
42
+
43
+ 12. 有问题就大声说
44
+ 如果有东西被悄悄跳过了,不能说"已完成"。如果有测试被跳过了,不能说"测试全过"。默认立场是把不确定性暴露出来,而不是藏起来。
45
+
46
+
47
+ ## python coding rule
48
+ 1. 代码必须使用 logging 模块进行日志记录,禁止使用 print 语句
49
+ 2. 运行 python必须使用 uv run python 脚本名.py 的方式运行
50
+ 3. 安装pip包必须使用 uv add 包名 的方式安装
51
+
52
+ ## Context 重建铁律
53
+ comate_agent_sdk 的上下文重建规则是高优先级架构约束。涉及 static header / session_state / conversation 三层分离、header_snapshot 的 first-snapshot-wins 语义、resume / fork / compact / clear_history 各自的不变量,以及最低测试护栏清单。
54
+ 任何改动涉及 chat_session.py、context/ir.py、context/compaction.py、agent/history.py、agent/init.py、agent/setup.py 时 MUST 先阅读完整规则。
55
+ → ./rules/context-rebuild.md
56
+
57
+ ## Token 账本语义铁律
58
+
59
+ comate_agent_sdk 的 token 相关概念分属不同账本,后续改动 MUST 先阅读完整规则,禁止为了修某个阈值或展示问题把不同账本混算。
60
+ → ./rules/token-accounting.md
61
+
62
+ ## TUI 编程铁律
63
+ 不做 History TUI,历史只写 scrollback。layout 只允许底部交互带。颜色策略以 shell 背景为准。包含 Team 事件显示铁律和 Pre-flight TUI 铁律。
64
+ 涉及 prompt_toolkit layout、team 事件渲染、独立 Application 弹窗时 MUST 先阅读完整规则。
65
+ → ./rules/tui.md
66
+
67
+ ## 代码放置规则
68
+
69
+ 当需要为某个模块添加新功能时,先判断:这个功能是该模块的 **核心职责** 还是 **业务扩展**?
70
+
71
+ - **核心职责**:直接操作该模块自身管理的数据结构 → 可以加方法
72
+ - **业务扩展**:借用该模块的 API 完成上层业务目标 → 在调用方实现,禁止侵入
73
+
74
+ 判断标准:如果删掉这个方法,该模块的核心抽象(数据结构 + 主流程)是否仍然完整?
75
+ 如果仍然完整,说明这个方法不属于这里。
76
+
77
+ 实践要求:
78
+ 1. 新增方法前,说明它属于核心职责还是业务扩展
79
+ 2. 如果是业务扩展,必须放在调用方或独立 service 中,通过公共 API 操作
80
+ 3. 单个模块超过 500 行时,主动审视是否有职责泄漏
81
+
82
+ ## Prompt 工程规则
83
+
84
+ 涉及 SDK 内置 prompt / 提示词时,后续 agent MUST 默认遵循以下约定,禁止重新回到“局部硬编码 + 局部替换”的分散实现。
85
+
86
+ - SDK 自带 prompt 资源 MUST 放在 `comate_agent_sdk/prompts/` 下,按领域分目录,例如 `agent/`、`subagent/`。
87
+ - 新增 prompt 时,优先判断它是 **SDK 静态资源** 还是 **调用方临时拼接文本**:
88
+ - SDK 静态资源:放入 `comate_agent_sdk/prompts/`,通过统一 loader 读取。
89
+ - 调用方临时拼接文本:保留在调用方,禁止为了“统一”硬塞进不相关模块。
90
+ - MUST 使用两阶段接口:
91
+ - `load_prompt()` 只负责读取包内 markdown 资源。
92
+ - `render_prompt()` 只负责变量渲染。
93
+ - 新 prompt 的变量语法 MUST 统一使用 `{{VAR_NAME}}`,不要新增 `{var}`、`%s`、手写 `replace()` 等新风格。
94
+
95
+ ## System Tool 描述工程规则
96
+
97
+ 涉及 SDK 内置 system tool 的 schema description / system prompt usage_rules / 默认上限值时,
98
+ 后续 agent MUST 默认遵循以下约定。
99
+
100
+ → ./rules/system-tool-prompt-conventions.md
101
+
102
+ ## Tool Envelope 与 Typed Schema 铁律
103
+
104
+ comate_agent_sdk 内置 system tool 的"信封 (`ok/err` 6-key envelope) + typed OutputSchema + `ToolMessage.raw_envelope` 三轨分离"是 typed `ToolResultEvent.output`、SDK 投影、resume 持久化、reminder 引擎共同依赖的事实契约。envelope 一旦被静默改形 / 截断 / 污染框架字段,会让 typed 事件、投影、reminder、jsonl resume **同时静默失效**。
105
+
106
+ 涉及以下任一项 MUST 先阅读完整规则,禁止仅"局部 patch":
107
+
108
+ - `comate_agent_sdk/system_tools/tool_result.py`、`tool_schemas/`(含 `_registry.py` / `_projection.py` / `_base.py` / 各 `<tool>.py`)、`system_tools/tools|task|team/` 下 system tool 的返回结构。
109
+ - `agent/tool_exec.py` 中 `_build_tool_result_message()` / `_extract_tool_envelope()`、`llm/messages.py` 中 `ToolMessage` 的 `content` / `raw_envelope` / `execution_meta` 字段、`mcp/manager.py` 中 `McpToolResult`。
110
+ - `agent/runner_engine/execution/tool_execution.py` 的 `_resolve_tool_output()`、`runner_engine/projection/tool_result_projection.py`、`agent/events.py` 中 `ToolResultEvent.output`、`context/reminder_engine.py`、`subagent/team_session.py`、`chat_session.py` resume 路径。
111
+ - size guard / truncation / artifact spill 与 envelope 关系的任何 plan。
112
+
113
+ 核心铁律一句话:**system tool 必须 `ok()/err()` envelope;envelope 形状不可破坏性变更;envelope 是 typed schema 唯一原料;envelope 只装业务字段,框架元数据走 `execution_meta`;envelope 严禁 slim / 入 prompt / 被 monkey-patch 注册;MCP 用 `McpToolResult.raw_result`、不混 `raw_envelope`;用户 `@tool` 永远是 `CustomToolOutput`。**
114
+
115
+ → ./rules/tool-envelope.md
@@ -13,6 +13,10 @@
13
13
 
14
14
  ### Added
15
15
 
16
+ - 新增 `/clear` slash command:清空当前 leader session 的 conversation,
17
+ 保留 session_id、team membership、inbox 与 TaskStore。成功后会清屏、
18
+ 重打 logo、重置 TUI transcript view,并刷新 status bar;busy 或
19
+ compacting 状态会拒绝执行。
16
20
  - `_ShutdownNoiseGuard` 扩展为四合一接管器:SIGINT、`sys.unraisablehook`、
17
21
  `sys.excepthook`、`warnings.showwarning`。捕获到的噪音统一路由到独立的
18
22
  `comate.noise` logger(`propagate=False`),写入 `~/.comate/logs/agent.log`。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comate-cli
3
- Version: 0.7.0a7
3
+ Version: 0.7.0a9
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
@@ -9,7 +9,7 @@ from enum import Enum
9
9
  from rich.console import RenderableType
10
10
  from rich.text import Text
11
11
 
12
- from comate_agent_sdk.agent.events import StopEvent, TextEvent, ToolCallEvent, ToolResultEvent, UserQuestionEvent
12
+ from comate_agent_sdk.agent.events import StopEvent, ToolCallEvent, ToolResultEvent, UserQuestionEvent
13
13
 
14
14
  from comate_cli.terminal_agent.figures import (
15
15
  BREATH_DOT_GLYPHS as _FIGURES_BREATH_DOT_GLYPHS,
@@ -238,6 +238,27 @@ BREATH_DOT_COLORS: tuple[str, ...] = (
238
238
  )
239
239
  BREATH_DOT_GLYPHS: tuple[str, ...] = _FIGURES_BREATH_DOT_GLYPHS
240
240
 
241
+ LOADING_SPINNER_FRAMES: tuple[str, ...] = (
242
+ "⠋",
243
+ "⠙",
244
+ "⠹",
245
+ "⠸",
246
+ "⠼",
247
+ "⠴",
248
+ "⠦",
249
+ "⠧",
250
+ "⠇",
251
+ "⠏",
252
+ )
253
+
254
+ HIDDEN_THINKING_BADGES: tuple[str, ...] = (
255
+ "ultra thinking",
256
+ "deep reasoning",
257
+ "thinking deeper",
258
+ "extended thinking",
259
+ "working through it",
260
+ )
261
+
241
262
 
242
263
  def breathing_dot_color(frame: int) -> str:
243
264
  """Return the breathing dot color for a given animation frame."""
@@ -252,6 +273,11 @@ def breathing_dot_glyph(now_monotonic: float | None = None) -> str:
252
273
  return BREATH_DOT_GLYPHS[phase]
253
274
 
254
275
 
276
+ def loading_spinner_glyph(frame: int) -> str:
277
+ """Return the compact spinner glyph for the main loading line."""
278
+ return LOADING_SPINNER_FRAMES[frame % len(LOADING_SPINNER_FRAMES)]
279
+
280
+
255
281
  def _lerp_rgb(
256
282
  start_rgb: tuple[int, int, int],
257
283
  end_rgb: tuple[int, int, int],
@@ -385,11 +411,9 @@ class SubmissionAnimator:
385
411
  return Text("")
386
412
 
387
413
  phrase = self._status_hint if self._status_hint else (self._shuffled[self._phrase_idx] + ELLIPSIS)
388
- dot_color = breathing_dot_color(self._frame)
389
- now_monotonic = time.monotonic()
390
414
  dot = Text(
391
- f"{breathing_dot_glyph(now_monotonic)} ",
392
- style=f"bold {dot_color}",
415
+ f"{loading_spinner_glyph(self._frame)} ",
416
+ style="bold #9CA3AF",
393
417
  )
394
418
  sweep = _cyan_sweep_text(phrase, frame=self._frame)
395
419
  return Text.assemble(dot, sweep)
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ import random
4
5
  import re
5
6
  import threading
6
7
  import time
@@ -37,6 +38,7 @@ from comate_agent_sdk.agent.events import (
37
38
  from rich.console import RenderableType
38
39
  from rich.text import Text
39
40
 
41
+ from comate_cli.terminal_agent.animations import HIDDEN_THINKING_BADGES
40
42
  from comate_cli.terminal_agent.figures import (
41
43
  BOTTOM_LEFT_CROP,
42
44
  BULLET_OPERATOR,
@@ -59,6 +61,11 @@ from comate_cli.terminal_agent.tool_result_store import (
59
61
  ToolResultRegistry,
60
62
  )
61
63
  from comate_cli.terminal_agent.tool_view import summarize_tool_args, resolve_display_tool_name, should_show_tool_in_scrollback
64
+ from comate_cli.terminal_agent.tool_fold import (
65
+ ActiveToolFold,
66
+ ToolFoldSnapshot,
67
+ is_foldable_tool,
68
+ )
62
69
  from comate_cli.terminal_agent.env_utils import read_env_int
63
70
  from comate_cli.terminal_agent.custom_slash_commands import FILE_REF_PATTERN
64
71
  logger = logging.getLogger(__name__)
@@ -258,6 +265,7 @@ class EventRenderer:
258
265
  self._history: list[HistoryEntry] = []
259
266
  self._running_tools: dict[str, _RunningTool] = {}
260
267
  self._tool_call_args: dict[str, dict[str, Any]] = {}
268
+ self._active_tool_fold = ActiveToolFold()
261
269
  self._tool_results = tool_results
262
270
  self._fallback_tool_result_sequence = 0
263
271
  self._assistant_buffer = ""
@@ -286,6 +294,7 @@ class EventRenderer:
286
294
  self._show_thinking_cb: Callable[[], bool] = lambda: True
287
295
  self._turn_received_text_delta: bool = False
288
296
  self._turn_received_thinking_delta: bool = False
297
+ self._hidden_thinking_badge_text: str = ""
289
298
  self._text_delta_started: bool = False
290
299
  self._active_text_message_id: str | None = None
291
300
  # ━━━━━ spec §4.1 新管道状态字段(Phase 1.2 引入,Phase 4-6 接入) ━━━━━
@@ -358,6 +367,21 @@ class EventRenderer:
358
367
  if entry.entry_type == "tool_result":
359
368
  self.flush_pending_logs()
360
369
 
370
+ def _commit_active_tool_fold(self) -> None:
371
+ snapshot = self._active_tool_fold.snapshot(active=False)
372
+ if snapshot is None:
373
+ return
374
+ self._append_history_entry(
375
+ HistoryEntry(
376
+ entry_type="tool_fold",
377
+ text=snapshot.summary,
378
+ severity="error" if snapshot.any_error else "info",
379
+ subtitle=snapshot.error_summary or snapshot.latest_hint,
380
+ tool_call_ids=snapshot.tool_call_ids,
381
+ )
382
+ )
383
+ self._active_tool_fold.clear()
384
+
361
385
  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
362
386
  # spec §6 流式新管道方法集(Phase 1-6 新增;Phase 7 接入 handle_event)
363
387
  # 容器检测原语(spec §6.3)
@@ -452,6 +476,10 @@ class EventRenderer:
452
476
  """
453
477
  if not delta:
454
478
  return
479
+ if self._show_thinking_cb():
480
+ self._hidden_thinking_badge_text = ""
481
+ elif not self._hidden_thinking_badge_text:
482
+ self._hidden_thinking_badge_text = random.choice(HIDDEN_THINKING_BADGES)
455
483
  self._thinking_batch += delta
456
484
  while "\n" in self._thinking_batch:
457
485
  line, self._thinking_batch = self._thinking_batch.split("\n", 1)
@@ -543,6 +571,7 @@ class EventRenderer:
543
571
  """
544
572
  if not delta:
545
573
  return
574
+ self._hidden_thinking_badge_text = ""
546
575
  if self._thinking_batch:
547
576
  self._flush_thinking_batch()
548
577
  self._text_pending += delta
@@ -583,6 +612,7 @@ class EventRenderer:
583
612
  self._flush_thinking_batch()
584
613
  # ⑤ aux
585
614
  self._loading_aux_text = ""
615
+ self._hidden_thinking_badge_text = ""
586
616
 
587
617
  def _drop_all_pending(self) -> None:
588
618
  """spec §6.4:interrupt_turn 调用。
@@ -593,6 +623,7 @@ class EventRenderer:
593
623
  self._container = None
594
624
  self._thinking_batch = ""
595
625
  self._loading_aux_text = ""
626
+ self._hidden_thinking_badge_text = ""
596
627
  self._text_delta_started = False
597
628
  self._active_text_message_id = None
598
629
 
@@ -636,6 +667,7 @@ class EventRenderer:
636
667
  self._container = None
637
668
  self._thinking_batch = ""
638
669
  self._loading_aux_text = ""
670
+ self._hidden_thinking_badge_text = ""
639
671
  self._rebuild_loading_line()
640
672
 
641
673
  def seed_user_message(
@@ -649,6 +681,7 @@ class EventRenderer:
649
681
  if not normalized:
650
682
  return
651
683
  self._flush_assistant_segment()
684
+ self._commit_active_tool_fold()
652
685
  if display_header and display_subtitle:
653
686
  # AskUserQuestion 答复入 scrollback:用 tool_result 样式渲染头行(●),
654
687
  # 答案明细作为 ⎿ subtitle 紧贴其下;行间不插入用户 prefix。
@@ -698,6 +731,7 @@ class EventRenderer:
698
731
  # spec §6.4:turn 结束 force flush 所有 pending(held / container /
699
732
  # text_pending / thinking)保证 scrollback 看到完整内容
700
733
  self._force_flush_all()
734
+ self._commit_active_tool_fold()
701
735
  self._flush_assistant_segment()
702
736
  self.flush_pending_logs()
703
737
  self._pending_tool_starts.clear()
@@ -720,6 +754,7 @@ class EventRenderer:
720
754
  )
721
755
  self._running_tools.clear()
722
756
  self._flush_assistant_segment()
757
+ self._commit_active_tool_fold()
723
758
  self._pending_tool_starts.clear()
724
759
  # spec §6.4:用户中断 → 5 个新字段全部丢弃不入队
725
760
  self._drop_all_pending()
@@ -728,12 +763,16 @@ class EventRenderer:
728
763
  def history_entries(self) -> list[HistoryEntry]:
729
764
  return list(self._history)
730
765
 
766
+ def active_tool_fold_snapshot(self) -> ToolFoldSnapshot | None:
767
+ return self._active_tool_fold.snapshot(active=True)
768
+
731
769
  def reset_history_view(self) -> None:
732
770
  """重置 history 视图状态(用于会话切换后的重新加载)。"""
733
771
  self._history = []
734
772
  self._recent_team_event_keys.clear()
735
773
  self._running_tools.clear()
736
774
  self._tool_call_args.clear()
775
+ self._active_tool_fold.clear()
737
776
  self._assistant_buffer = ""
738
777
  self._pending_tool_starts.clear()
739
778
  self._turn_received_text_delta = False
@@ -747,6 +786,7 @@ class EventRenderer:
747
786
  self._thinking_batch = ""
748
787
  self._loading_aux_text = ""
749
788
  self._loading_state = LoadingState.idle()
789
+ self._hidden_thinking_badge_text = ""
750
790
  self._current_tasks = []
751
791
  self._current_task_title = None
752
792
  self._task_started_at_monotonic = None
@@ -802,6 +842,10 @@ class EventRenderer:
802
842
  拼接到 spinner phrase。无容器时返回 ""。"""
803
843
  return self._loading_aux_text
804
844
 
845
+ def hidden_thinking_badge_text(self) -> str:
846
+ """Return the loading-line badge shown while hidden thinking streams."""
847
+ return self._hidden_thinking_badge_text
848
+
805
849
  def append_subtitle(
806
850
  self,
807
851
  text: str,
@@ -1182,11 +1226,6 @@ class EventRenderer:
1182
1226
  if state.is_task:
1183
1227
  subagent_name = state.subagent_name or "Agent"
1184
1228
  description = state.subagent_description or state.title
1185
- if description and description != subagent_name:
1186
- task_title = f"{subagent_name}({description})"
1187
- else:
1188
- task_title = subagent_name
1189
-
1190
1229
  model_name = ""
1191
1230
  if metadata and isinstance(metadata, dict):
1192
1231
  model_name = metadata.get("model_name", "")
@@ -1343,6 +1382,7 @@ class EventRenderer:
1343
1382
  pass
1344
1383
  case TextDeltaEvent(delta=delta, message_id=message_id):
1345
1384
  if delta:
1385
+ self._commit_active_tool_fold()
1346
1386
  normalized_message_id = str(message_id or "").strip()
1347
1387
  if normalized_message_id and normalized_message_id != self._active_text_message_id:
1348
1388
  self._active_text_message_id = normalized_message_id
@@ -1382,40 +1422,50 @@ class EventRenderer:
1382
1422
  attempted=attempted,
1383
1423
  compacted=compacted,
1384
1424
  reason=reason,
1385
- tokens_before=tokens_before,
1425
+ tokens_before=_,
1386
1426
  tokens_after=tokens_after,
1387
1427
  ):
1388
1428
  self.clear_auto_compacting()
1389
1429
  pct = int(tokens / threshold * 100) if threshold > 0 else 0
1430
+ # precheck 的 tokens 是 estimate_precheck() 推算值;
1431
+ # check 的 tokens 是上一次 API 返回的真实 context_usage。
1432
+ trigger_label = "est." if trigger == "precheck" else "usage"
1390
1433
  if compacted:
1434
+ reduction_pct = (
1435
+ max(0, int(((tokens - tokens_after) / tokens) * 100))
1436
+ if tokens > 0
1437
+ else 0
1438
+ )
1391
1439
  self.append_system_message(
1392
1440
  (
1393
- f"Context compaction completed ({trigger}): "
1394
- f"{tokens_before:,} {INJECTED_ARROW} {tokens_after:,} tokens "
1395
- f"(check {tokens:,}/{threshold:,}, {pct}%)"
1441
+ f"Auto-compact done ({trigger}): "
1442
+ f"{trigger_label} {tokens:,} / threshold {threshold:,} ({pct}%)\n"
1443
+ f" {INJECTED_ARROW} context reduced to ~{tokens_after:,} tokens "
1444
+ f"(-{reduction_pct}%)"
1396
1445
  ),
1397
1446
  severity="info",
1398
1447
  )
1399
1448
  elif attempted:
1400
1449
  is_recoverable_deferred = str(reason).startswith("partial_compact_no_op:")
1401
1450
  label = (
1402
- "Context compaction deferred"
1451
+ "Auto-compact deferred"
1403
1452
  if is_recoverable_deferred
1404
- else "Context compaction attempt failed"
1453
+ else "Auto-compact attempt failed"
1405
1454
  )
1406
1455
  self.append_system_message(
1407
1456
  (
1408
1457
  f"{label} ({trigger}): "
1409
- f"kept {tokens_after:,} tokens "
1410
- f"(check {tokens:,}/{threshold:,}, {pct}%, reason={reason})"
1458
+ f"{trigger_label} {tokens:,} / threshold {threshold:,} ({pct}%), "
1459
+ f"reason={reason}"
1411
1460
  ),
1412
1461
  severity="warning",
1413
1462
  )
1414
1463
  else:
1415
1464
  self.append_system_message(
1416
1465
  (
1417
- f"Context compaction skipped ({trigger}): "
1418
- f"{tokens:,}/{threshold:,} tokens ({pct}%), reason={reason}"
1466
+ f"Auto-compact skipped ({trigger}): "
1467
+ f"{trigger_label} {tokens:,} / threshold {threshold:,} ({pct}%), "
1468
+ f"reason={reason}"
1419
1469
  ),
1420
1470
  severity="warning",
1421
1471
  )
@@ -1423,12 +1473,46 @@ class EventRenderer:
1423
1473
  args_dict = arguments if isinstance(arguments, dict) else {"_raw": str(arguments)}
1424
1474
  # Store args for ToolResult phase lookup.
1425
1475
  self._tool_call_args[tool_call_id] = args_dict
1476
+ if is_foldable_tool(tool_name):
1477
+ self._active_tool_fold.add_call(
1478
+ tool_call_id=tool_call_id,
1479
+ tool_name=tool_name,
1480
+ args=args_dict,
1481
+ started_at_monotonic=time.monotonic(),
1482
+ )
1483
+ self._rebuild_loading_line()
1484
+ return (False, None)
1485
+ self._commit_active_tool_fold()
1426
1486
  if not should_show_tool_in_scrollback(tool_name, args_dict):
1427
1487
  self._rebuild_loading_line()
1428
1488
  return (False, None)
1429
1489
  self._append_tool_call(tool_name, args_dict, tool_call_id)
1430
1490
  case ToolResultEvent(tool=tool_name, result=result, tool_call_id=tool_call_id, is_error=is_error, metadata=metadata, output=output):
1431
1491
  stored_args = self._tool_call_args.pop(tool_call_id, {})
1492
+ if self._active_tool_fold.has_call(tool_call_id):
1493
+ display_name = resolve_display_tool_name(tool_name, stored_args)
1494
+ rec = self._make_tool_result_record(
1495
+ tool_call_id=tool_call_id,
1496
+ tool_name=tool_name,
1497
+ display_name=display_name,
1498
+ started_at_monotonic=time.monotonic(),
1499
+ args=stored_args,
1500
+ is_error=is_error,
1501
+ result=result,
1502
+ metadata=metadata,
1503
+ output=output,
1504
+ )
1505
+ error_summary = None
1506
+ if is_error:
1507
+ error_summary = str(result).strip().splitlines()[0] if str(result).strip() else None
1508
+ self._active_tool_fold.mark_result(
1509
+ tool_call_id,
1510
+ is_error=is_error,
1511
+ result_record_sequence=rec.sequence,
1512
+ error_summary=error_summary,
1513
+ )
1514
+ self._rebuild_loading_line()
1515
+ return (False, None)
1432
1516
  if not should_show_tool_in_scrollback(tool_name, stored_args, is_result=True, is_error=is_error):
1433
1517
  self._running_tools.pop(tool_call_id, None)
1434
1518
  self._rebuild_loading_line()
@@ -1532,6 +1616,7 @@ class EventRenderer:
1532
1616
  if self._turn_received_text_delta:
1533
1617
  pass
1534
1618
  elif text:
1619
+ self._commit_active_tool_fold()
1535
1620
  self._append_assistant_text(text)
1536
1621
  case StepCompleteEvent(step_id=step_id, status=_, duration_ms=_):
1537
1622
  # Cancellation/error paths may emit StepCompleteEvent without ToolResultEvent.
@@ -1539,9 +1624,11 @@ class EventRenderer:
1539
1624
  self._running_tools.pop(step_id, None)
1540
1625
  case StopEvent(reason=reason):
1541
1626
  self._flush_assistant_segment()
1627
+ self._commit_active_tool_fold()
1542
1628
  self._pending_tool_starts.clear()
1543
1629
  # Safety net: if any running tool rows remain, stop event means this turn is ending.
1544
1630
  self._running_tools.clear()
1631
+ self._hidden_thinking_badge_text = ""
1545
1632
  self._rebuild_loading_line()
1546
1633
  if reason == "waiting_for_input":
1547
1634
  return (True, None)
@@ -107,20 +107,40 @@ def _entry_content(
107
107
  entry: HistoryEntry,
108
108
  *,
109
109
  terminal_width: int,
110
- render_markdown_to_plain: Callable[..., str],
111
- ) -> str:
110
+ render_markdown_to_plain: Callable[..., str | Text],
111
+ ) -> str | Text:
112
112
  if entry.entry_type != "assistant":
113
113
  return str(entry.text)
114
114
  width = max(terminal_width - 6, 40)
115
115
  return render_markdown_to_plain(str(entry.text), width=width)
116
116
 
117
117
 
118
+ def _render_prefixed_rich_text(
119
+ content: Text,
120
+ *,
121
+ prefix: str,
122
+ in_run_continuation: bool,
123
+ ) -> Text:
124
+ content_lines = content.split("\n") or [Text("")]
125
+ line_text = Text()
126
+ if in_run_continuation:
127
+ line_text.append(" ", style="bold")
128
+ else:
129
+ line_text.append(f"{prefix} ", style="bold")
130
+ line_text.append_text(content_lines[0])
131
+ for line in content_lines[1:]:
132
+ line_text.append("\n")
133
+ line_text.append(" ")
134
+ line_text.append_text(line)
135
+ return line_text
136
+
137
+
118
138
  def render_history_group(
119
139
  console: Console,
120
140
  entries: list[HistoryEntry],
121
141
  *,
122
142
  terminal_width: int,
123
- render_markdown_to_plain: Callable[..., str],
143
+ render_markdown_to_plain: Callable[..., str | Text],
124
144
  prev_was_assistant: bool = False,
125
145
  prev_was_thinking: bool = False,
126
146
  assume_run_continues_at_tail: bool = False,
@@ -240,6 +260,33 @@ def render_history_group(
240
260
  subtitle_chain_active = True
241
261
  continue
242
262
 
263
+ if entry.entry_type == "tool_fold":
264
+ # 折叠组摘要:参考 thinking 的视觉处理(2 空格缩进 + dim 文案),
265
+ # 不带前缀圆点,行末追加 "(ctrl+o to expand)" 提示 Ctrl+O 展开。
266
+ is_error = entry.severity == "error"
267
+ content = str(entry.text).strip()
268
+ content_lines = [line for line in content.splitlines() if line.strip()] or [content]
269
+ suffix = " (ctrl+o to expand)" if entry.tool_call_ids else ""
270
+ text_style = "dim #FF6B6B" if is_error else "dim"
271
+
272
+ line_text = Text()
273
+ line_text.append(" ", style=text_style)
274
+ line_text.append(content_lines[0], style=text_style)
275
+ if suffix and len(content_lines) == 1:
276
+ line_text.append(suffix, style=text_style)
277
+ for idx_line, line in enumerate(content_lines[1:], start=1):
278
+ line_text.append("\n")
279
+ line_text.append(" ", style=text_style)
280
+ line_text.append(line, style=text_style)
281
+ if suffix and idx_line == len(content_lines) - 1:
282
+ line_text.append(suffix, style=text_style)
283
+
284
+ renderables.append(line_text)
285
+ renderables.append(Text(""))
286
+ prev_was_assistant = False
287
+ subtitle_chain_active = False
288
+ continue
289
+
243
290
  if entry.entry_type == "tool_result":
244
291
  prefix_char = BLACK_CIRCLE
245
292
  prefix_style = "bold red" if entry.severity == "error" else "bold green"
@@ -253,10 +300,20 @@ def render_history_group(
253
300
  first_line.append(f"{prefix_char} ", style=prefix_style)
254
301
  first_line.append_text(text_lines[0] if text_lines else Text())
255
302
  renderables.append(first_line)
303
+
304
+ has_body = len(text_lines) > 1
305
+ # For multi-line Rich Text bodies (Edit/Write inline diff),
306
+ # subtitle ("Added X, removed Y") goes between signature and
307
+ # body so the reader sees the summary before the diff. For
308
+ # single-line text (bash/grep/read previews), subtitle stays
309
+ # after the signature line as before.
310
+ if has_body and entry.subtitle:
311
+ renderables.append(_render_subtitle_line(entry.subtitle, error=is_error))
256
312
  for line in text_lines[1:]:
257
313
  renderables.append(line)
258
- if entry.subtitle:
314
+ if not has_body and entry.subtitle:
259
315
  renderables.append(_render_subtitle_line(entry.subtitle, error=is_error))
316
+
260
317
  renderables.append(Text(""))
261
318
  prev_was_assistant = False
262
319
  subtitle_chain_active = bool(entry.subtitle)
@@ -297,10 +354,10 @@ def render_history_group(
297
354
  style = "bold #FF6B6B" if entry.severity == "error" else "#E8B830"
298
355
  content_lines = content.splitlines() or [""]
299
356
  line_text = Text()
300
- line_text.append(content_lines[0], style=style)
357
+ line_text.append(f" {content_lines[0]}", style=style)
301
358
  for line in content_lines[1:]:
302
359
  line_text.append("\n")
303
- line_text.append(line, style=style)
360
+ line_text.append(f" {line}", style=style)
304
361
  renderables.append(line_text)
305
362
 
306
363
  subtitle_chain_active = False
@@ -324,16 +381,25 @@ def render_history_group(
324
381
  terminal_width=terminal_width,
325
382
  render_markdown_to_plain=render_markdown_to_plain,
326
383
  )
327
- content_lines = content.splitlines() or [""]
328
- if in_run_continuation:
329
- first_line = f" {content_lines[0]}"
384
+ if isinstance(content, Text):
385
+ renderables.append(
386
+ _render_prefixed_rich_text(
387
+ content,
388
+ prefix=_entry_prefix(entry),
389
+ in_run_continuation=in_run_continuation,
390
+ )
391
+ )
330
392
  else:
331
- prefix = _entry_prefix(entry)
332
- first_line = f"{prefix} {content_lines[0]}"
333
- lines = [first_line]
334
- for line in content_lines[1:]:
335
- lines.append(f" {line}")
336
- renderables.append("\n".join(lines))
393
+ content_lines = content.splitlines() or [""]
394
+ if in_run_continuation:
395
+ first_line = f" {content_lines[0]}"
396
+ else:
397
+ prefix = _entry_prefix(entry)
398
+ first_line = f"{prefix} {content_lines[0]}"
399
+ lines = [first_line]
400
+ for line in content_lines[1:]:
401
+ lines.append(f" {line}")
402
+ renderables.append("\n".join(lines))
337
403
 
338
404
  # 在 assistant run 内部不插入空行;只有 run 末尾才与下一段拉开。
339
405
  # assume_run_continues_at_tail=True 时(drain 在 streaming 中),把 batch
@@ -357,8 +423,11 @@ def render_history_group(
357
423
 
358
424
 
359
425
  async def print_history_group_async(console: Console, group: Group) -> None:
360
- console.print(group, soft_wrap=True)
426
+ # highlight=False:Rich 默认 ReprHighlighter 会把 "/cmd" 误识别为 path/filename
427
+ # 并染成 magenta/bright-magenta(即"粉红色 slash 命令名")。所有 entry 在
428
+ # render_history_group 内已自带 explicit Text 样式,不需要再做自动高亮。
429
+ console.print(group, soft_wrap=True, highlight=False)
361
430
 
362
431
 
363
432
  def print_history_group_sync(console: Console, group: Group) -> None:
364
- console.print(group, soft_wrap=True)
433
+ console.print(group, soft_wrap=True, highlight=False)