comate-cli 0.5.8__tar.gz → 0.6.0__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 (139) hide show
  1. {comate_cli-0.5.8 → comate_cli-0.6.0}/PKG-INFO +1 -1
  2. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/history_printer.py +13 -2
  3. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/tui.py +1 -0
  4. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/tui_parts/history_sync.py +12 -0
  5. {comate_cli-0.5.8 → comate_cli-0.6.0}/pyproject.toml +1 -1
  6. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_history_printer.py +85 -4
  7. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_skills_slash_command.py +47 -0
  8. {comate_cli-0.5.8 → comate_cli-0.6.0}/uv.lock +2 -2
  9. {comate_cli-0.5.8 → comate_cli-0.6.0}/.gitignore +0 -0
  10. {comate_cli-0.5.8 → comate_cli-0.6.0}/README.md +0 -0
  11. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/__init__.py +0 -0
  12. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/__main__.py +0 -0
  13. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/main.py +0 -0
  14. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/mcp_cli.py +0 -0
  15. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/__init__.py +0 -0
  16. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/animations.py +0 -0
  17. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/app.py +0 -0
  18. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/assistant_render.py +0 -0
  19. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/codenames.py +0 -0
  20. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  21. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/env_utils.py +0 -0
  22. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/error_display.py +0 -0
  23. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/event_renderer.py +0 -0
  24. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/figures.py +0 -0
  25. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  26. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/input_geometry.py +0 -0
  27. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  28. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/logging_adapter.py +0 -0
  29. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/logo.py +0 -0
  30. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/markdown_render.py +0 -0
  31. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/mention_completer.py +0 -0
  32. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/message_style.py +0 -0
  33. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/models.py +0 -0
  34. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/path_context_hint.py +0 -0
  35. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
  36. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
  37. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
  38. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
  39. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
  40. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
  41. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
  42. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
  43. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
  44. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
  45. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
  46. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
  47. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
  48. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/preflight.py +0 -0
  49. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/question_view.py +0 -0
  50. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/resume_selector.py +0 -0
  51. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/rewind_store.py +0 -0
  52. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  53. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  54. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/selection_menu.py +0 -0
  55. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/session_view.py +0 -0
  56. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/slash_commands.py +0 -0
  57. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/startup.py +0 -0
  58. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/status_bar.py +0 -0
  59. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/text_effects.py +0 -0
  60. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/tips.py +0 -0
  61. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/tool_view.py +0 -0
  62. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  63. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
  64. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
  65. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
  66. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
  67. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
  68. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  69. {comate_cli-0.5.8 → comate_cli-0.6.0}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  70. {comate_cli-0.5.8 → comate_cli-0.6.0}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
  71. {comate_cli-0.5.8 → comate_cli-0.6.0}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
  72. {comate_cli-0.5.8 → comate_cli-0.6.0}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
  73. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/conftest.py +0 -0
  74. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_animator_shuffle.py +0 -0
  75. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_app_mcp_preload.py +0 -0
  76. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_app_preflight_gate.py +0 -0
  77. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_app_print_mode.py +0 -0
  78. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_app_shutdown.py +0 -0
  79. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_app_usage_line.py +0 -0
  80. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_cli_project_root.py +0 -0
  81. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_compact_command_semantics.py +0 -0
  82. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_completion_context_activation.py +0 -0
  83. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_completion_status_panel.py +0 -0
  84. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_context_command.py +0 -0
  85. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_custom_slash_commands.py +0 -0
  86. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_discover_tab.py +0 -0
  87. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_errors_tab.py +0 -0
  88. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_event_renderer.py +0 -0
  89. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_event_renderer_boundary.py +0 -0
  90. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_event_renderer_e2e.py +0 -0
  91. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_event_renderer_streaming.py +0 -0
  92. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_format_error.py +0 -0
  93. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_handle_error.py +0 -0
  94. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_history_sync.py +0 -0
  95. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_input_behavior.py +0 -0
  96. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_input_history.py +0 -0
  97. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_installed_tab.py +0 -0
  98. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_interrupt_exit_semantics.py +0 -0
  99. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_layout_coordinator.py +0 -0
  100. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_logging_adapter.py +0 -0
  101. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_logo.py +0 -0
  102. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_main_args.py +0 -0
  103. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_markdown_render.py +0 -0
  104. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_marketplaces_tab.py +0 -0
  105. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_mcp_cli.py +0 -0
  106. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_mcp_slash_command.py +0 -0
  107. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_mention_completer.py +0 -0
  108. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_path_context_hint.py +0 -0
  109. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_plugin_slash_commands.py +0 -0
  110. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_plugin_tui_components.py +0 -0
  111. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_preflight.py +0 -0
  112. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_preflight_copilot.py +0 -0
  113. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_question_key_bindings.py +0 -0
  114. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_question_view.py +0 -0
  115. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_resume_selector.py +0 -0
  116. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_rewind_command_semantics.py +0 -0
  117. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_rewind_store.py +0 -0
  118. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_rpc_protocol.py +0 -0
  119. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_rpc_stdio_bridge.py +0 -0
  120. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_selection_menu.py +0 -0
  121. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_slash_argument_hint.py +0 -0
  122. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_slash_completer.py +0 -0
  123. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_slash_registry.py +0 -0
  124. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_status_bar.py +0 -0
  125. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_status_bar_transient.py +0 -0
  126. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_task_panel_format.py +0 -0
  127. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_task_panel_key_bindings.py +0 -0
  128. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_task_panel_rendering.py +0 -0
  129. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_task_poll.py +0 -0
  130. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_tool_view.py +0 -0
  131. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_tui_elapsed_status.py +0 -0
  132. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_tui_esc_queue.py +0 -0
  133. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_tui_mcp_init_gate.py +0 -0
  134. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_tui_paste_placeholder.py +0 -0
  135. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_tui_queue_preview.py +0 -0
  136. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_tui_queue_sdk_source.py +0 -0
  137. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_tui_split_invariance.py +0 -0
  138. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_tui_team_messages.py +0 -0
  139. {comate_cli-0.5.8 → comate_cli-0.6.0}/tests/test_update_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comate-cli
3
- Version: 0.5.8
3
+ Version: 0.6.0
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
@@ -87,6 +87,7 @@ def render_history_group(
87
87
  terminal_width: int,
88
88
  render_markdown_to_plain: Callable[..., str],
89
89
  prev_was_assistant: bool = False,
90
+ prev_was_thinking: bool = False,
90
91
  assume_run_continues_at_tail: bool = False,
91
92
  ) -> Group | None:
92
93
  if not entries:
@@ -98,6 +99,7 @@ def render_history_group(
98
99
  # printer 这一层被识别为"连续 assistant entry 的 run"——只在 run 起始处加 `●`,
99
100
  # 后续行用 2 空格缩进保持视觉对齐。`prev_was_assistant` 把 run 状态跨 drain 传过来。
100
101
  renderables: list[Any] = []
102
+ needs_leading_gap_after_thinking = prev_was_thinking
101
103
  for idx, entry in enumerate(entries):
102
104
  is_assistant = entry.entry_type == "assistant"
103
105
  in_run_continuation = is_assistant and prev_was_assistant
@@ -116,10 +118,15 @@ def render_history_group(
116
118
  ):
117
119
  continue
118
120
 
119
- # Thinking entries: 灰色显示,无前缀
121
+ # Thinking entries: 灰色显示,无前缀。thinking 流式文本里的空白行
122
+ # 只属于模型输出内容,不应把同一个 thinking block 视觉上切散。
120
123
  if entry.entry_type == "thinking":
121
124
  content = str(entry.text)
122
- content_lines = content.splitlines() or [""]
125
+ content_lines = [line for line in content.splitlines() if line.strip()]
126
+ if not content_lines:
127
+ prev_was_assistant = False
128
+ continue
129
+ needs_leading_gap_after_thinking = False
123
130
  line_text = Text()
124
131
  line_text.append(" ", style="dim")
125
132
  line_text.append(content_lines[0], style="dim")
@@ -138,6 +145,10 @@ def render_history_group(
138
145
  prev_was_assistant = False
139
146
  continue
140
147
 
148
+ if needs_leading_gap_after_thinking:
149
+ renderables.append(Text(""))
150
+ needs_leading_gap_after_thinking = False
151
+
141
152
  # Elapsed entries: 灰色显示,无前缀
142
153
  if entry.entry_type == "elapsed":
143
154
  line_text = Text()
@@ -270,6 +270,7 @@ class TerminalAgentTUI(
270
270
  # 拆成 N 条 entry,跨 drain 时需要用这个 seed 让 history_printer 把 run 续上
271
271
  # (否则每次 drain 都重新加一个 `●`)。
272
272
  self._last_drained_was_assistant = False
273
+ self._last_drained_thinking_tail_needs_gap = False
273
274
  self._render_dirty = True
274
275
  self._diff_panel_visible = False
275
276
  self._diff_panel_scroll = 0
@@ -30,12 +30,14 @@ class HistorySyncMixin:
30
30
  # message 拆成 N 条 entry,需要 seed 让 history_printer 把 run 续上)。class-level
31
31
  # default 让所有 host(含测试 dummy)无需显式初始化也能用。
32
32
  _last_drained_was_assistant: bool = False
33
+ _last_drained_thinking_tail_needs_gap: bool = False
33
34
 
34
35
  async def _replay_scrollback_after_rewind(self) -> None:
35
36
  """清屏并重建 scrollback:Logo + 当前会话历史。"""
36
37
  self._renderer.reset_history_view()
37
38
  self._printed_history_index = 0
38
39
  self._last_drained_was_assistant = False
40
+ self._last_drained_thinking_tail_needs_gap = False
39
41
 
40
42
  def _clear_and_print_logo() -> None:
41
43
  out = sys.__stdout__ or sys.stdout
@@ -289,11 +291,16 @@ class HistorySyncMixin:
289
291
  terminal_width=self._terminal_width(),
290
292
  render_markdown_to_plain=render_markdown_to_plain,
291
293
  prev_was_assistant=self._last_drained_was_assistant,
294
+ prev_was_thinking=self._last_drained_thinking_tail_needs_gap,
292
295
  assume_run_continues_at_tail=getattr(self, "_busy", False),
293
296
  )
294
297
  if group is None:
295
298
  return
299
+ busy = getattr(self, "_busy", False)
296
300
  self._last_drained_was_assistant = pending[-1].entry_type == "assistant"
301
+ self._last_drained_thinking_tail_needs_gap = (
302
+ pending[-1].entry_type == "thinking" and busy
303
+ )
297
304
 
298
305
  def _print() -> None:
299
306
  width = self._terminal_width()
@@ -336,11 +343,16 @@ class HistorySyncMixin:
336
343
  terminal_width=self._terminal_width(),
337
344
  render_markdown_to_plain=render_markdown_to_plain,
338
345
  prev_was_assistant=self._last_drained_was_assistant,
346
+ prev_was_thinking=self._last_drained_thinking_tail_needs_gap,
339
347
  assume_run_continues_at_tail=getattr(self, "_busy", False),
340
348
  )
341
349
  if group is None:
342
350
  return
351
+ busy = getattr(self, "_busy", False)
343
352
  self._last_drained_was_assistant = pending[-1].entry_type == "assistant"
353
+ self._last_drained_thinking_tail_needs_gap = (
354
+ pending[-1].entry_type == "thinking" and busy
355
+ )
344
356
  print_history_group_sync(console, group)
345
357
 
346
358
  def _pending_history_entries(self) -> list[HistoryEntry]:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "comate-cli"
7
- version = "0.5.8"
7
+ version = "0.6.0"
8
8
  description = "Comate terminal CLI built on comate-agent-sdk"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -299,11 +299,12 @@ class TestAssumeRunContinuesAtTail(unittest.TestCase):
299
299
  assume_run_continues_at_tail=True 让 history_printer 把 batch 末尾的 assistant
300
300
  entry 当作 "run 仍在续" 处理,不加 trailing 空行。"""
301
301
 
302
- def _render_lines(
302
+ def _render_str(
303
303
  self,
304
304
  entries: list[HistoryEntry],
305
305
  *,
306
306
  prev_was_assistant: bool = False,
307
+ prev_was_thinking: bool = False,
307
308
  assume_run_continues_at_tail: bool = False,
308
309
  ) -> list[str]:
309
310
  console = Console(file=None, force_terminal=True, width=120)
@@ -311,13 +312,32 @@ class TestAssumeRunContinuesAtTail(unittest.TestCase):
311
312
  console, entries, terminal_width=120,
312
313
  render_markdown_to_plain=_identity_md,
313
314
  prev_was_assistant=prev_was_assistant,
315
+ prev_was_thinking=prev_was_thinking,
314
316
  assume_run_continues_at_tail=assume_run_continues_at_tail,
315
317
  )
316
318
  if group is None:
317
- return []
319
+ return ""
318
320
  with console.capture() as capture:
319
321
  console.print(group, soft_wrap=True)
320
- return _strip_ansi(capture.get()).split("\n")
322
+ return _strip_ansi(capture.get())
323
+
324
+ def _render_lines(
325
+ self,
326
+ entries: list[HistoryEntry],
327
+ *,
328
+ prev_was_assistant: bool = False,
329
+ prev_was_thinking: bool = False,
330
+ assume_run_continues_at_tail: bool = False,
331
+ ) -> list[str]:
332
+ output = self._render_str(
333
+ entries,
334
+ prev_was_assistant=prev_was_assistant,
335
+ prev_was_thinking=prev_was_thinking,
336
+ assume_run_continues_at_tail=assume_run_continues_at_tail,
337
+ )
338
+ if output == "":
339
+ return []
340
+ return output.split("\n")
321
341
 
322
342
  def test_default_off_preserves_trailing_blank_line(self) -> None:
323
343
  """默认 False:保持原行为,末尾 assistant entry 后加 trailing 空行。"""
@@ -394,12 +414,73 @@ class TestAssumeRunContinuesAtTail(unittest.TestCase):
394
414
  first_idx = lines.index(" line one")
395
415
  self.assertEqual(lines[first_idx + 1], " line two", msg=lines)
396
416
 
417
+ def test_thinking_internal_blank_lines_are_collapsed(self) -> None:
418
+ entries = [
419
+ HistoryEntry(
420
+ entry_type="thinking",
421
+ text="line one\n\nline two\n \nline three\n",
422
+ ),
423
+ ]
424
+ lines = self._render_lines(entries)
425
+ first_idx = lines.index(" line one")
426
+ self.assertEqual(
427
+ lines[first_idx : first_idx + 3],
428
+ [" line one", " line two", " line three"],
429
+ msg=lines,
430
+ )
431
+
432
+ def test_thinking_keeps_boundary_blank_lines(self) -> None:
433
+ entries = [
434
+ HistoryEntry(entry_type="user", text="hello"),
435
+ HistoryEntry(entry_type="thinking", text="line one\n\nline two\n"),
436
+ HistoryEntry(entry_type="assistant", text="hi\n"),
437
+ ]
438
+ lines = self._render_lines(entries)
439
+ user_idx = lines.index("> hello")
440
+ thinking_idx = lines.index(" line one")
441
+ assistant_idx = lines.index("⏺ hi")
442
+
443
+ self.assertEqual(lines[user_idx + 1], "", msg=lines)
444
+ self.assertEqual(thinking_idx, user_idx + 2, msg=lines)
445
+ self.assertEqual(lines[thinking_idx + 1], " line two", msg=lines)
446
+ self.assertEqual(lines[thinking_idx + 2], "", msg=lines)
447
+ self.assertEqual(assistant_idx, thinking_idx + 3, msg=lines)
448
+
397
449
  def test_assume_tail_suppresses_trailing_blank_for_thinking_tail(self) -> None:
398
450
  entry = HistoryEntry(entry_type="thinking", text="streamed thought\n")
399
451
  lines = self._render_lines([entry], assume_run_continues_at_tail=True)
400
452
  non_empty = [ln for ln in lines if ln.strip() != ""]
401
453
  self.assertEqual(non_empty, [" streamed thought"], msg=lines)
402
454
 
455
+ def test_cross_batch_thinking_to_assistant_keeps_boundary_blank_line(self) -> None:
456
+ first = self._render_str(
457
+ [HistoryEntry(entry_type="thinking", text="thought\n")],
458
+ assume_run_continues_at_tail=True,
459
+ )
460
+ second = self._render_str(
461
+ [HistoryEntry(entry_type="assistant", text="answer\n")],
462
+ prev_was_thinking=True,
463
+ )
464
+ merged = (first + second).split("\n")
465
+ thinking_idx = merged.index(" thought")
466
+ assistant_idx = merged.index("⏺ answer")
467
+
468
+ self.assertEqual(merged[thinking_idx + 1], "", msg=merged)
469
+ self.assertEqual(assistant_idx, thinking_idx + 2, msg=merged)
470
+
471
+ def test_cross_batch_thinking_to_thinking_does_not_insert_blank_line(self) -> None:
472
+ first = self._render_str(
473
+ [HistoryEntry(entry_type="thinking", text="line one\n")],
474
+ assume_run_continues_at_tail=True,
475
+ )
476
+ second = self._render_str(
477
+ [HistoryEntry(entry_type="thinking", text="line two\n")],
478
+ prev_was_thinking=True,
479
+ )
480
+ merged = (first + second).split("\n")
481
+ first_idx = merged.index(" line one")
482
+ self.assertEqual(merged[first_idx + 1], " line two", msg=merged)
483
+
403
484
  def test_split_thinking_batch_visual_matches_single_batch(self) -> None:
404
485
  entries = [
405
486
  HistoryEntry(entry_type="thinking", text="line one\n"),
@@ -410,7 +491,7 @@ class TestAssumeRunContinuesAtTail(unittest.TestCase):
410
491
  first = self._render_lines(
411
492
  entries[:2], assume_run_continues_at_tail=True,
412
493
  )
413
- second = self._render_lines(entries[2:])
494
+ second = self._render_lines(entries[2:], prev_was_thinking=True)
414
495
  merged = first + second
415
496
 
416
497
  single_content = [ln for ln in single_lines if ln.strip() != ""]
@@ -5,6 +5,7 @@ from types import SimpleNamespace
5
5
 
6
6
  from comate_agent_sdk.context import ContextIR, TokenCounter
7
7
  from comate_agent_sdk.context.items import ItemType
8
+ from comate_agent_sdk.skill.bundled import get_builtin_skills
8
9
  from comate_agent_sdk.skill.models import SkillDefinition
9
10
  from comate_cli.terminal_agent.slash_commands import SlashCommandSpec
10
11
  from comate_cli.terminal_agent.tui import TerminalAgentTUI
@@ -381,6 +382,52 @@ class TestSkillSlashRegistration(unittest.TestCase):
381
382
  self.assertEqual(app._skill_slash_command_names, {"demo"})
382
383
  self.assertIn("demo", {spec.name for spec in completer.updated_specs})
383
384
 
385
+ def test_sync_registers_builtin_skillify_with_argument_hint(self) -> None:
386
+ registry = SlashCommandRegistry()
387
+ completer = _FakeSlashCompleter()
388
+ app = TerminalAgentTUI.__new__(TerminalAgentTUI)
389
+ app._session = _build_session(list(get_builtin_skills()))
390
+ app._slash_registry = registry
391
+ app._skill_slash_command_names = set()
392
+ app._slash_completer = completer
393
+
394
+ app._sync_skill_slash_commands()
395
+
396
+ skillify = registry.resolve("skillify")
397
+ self.assertIsNotNone(skillify)
398
+ assert skillify is not None
399
+ self.assertEqual(skillify.source, "skill")
400
+ self.assertEqual(skillify.spec.execution_kind, "hybrid")
401
+ self.assertIsNotNone(skillify.spec.argument_hint)
402
+ self.assertIn("description", skillify.spec.argument_hint or "")
403
+ self.assertEqual(app._skill_slash_command_names, {"skillify"})
404
+ self.assertIn("skillify", {spec.name for spec in completer.updated_specs})
405
+
406
+ def test_custom_slash_skillify_takes_precedence_over_builtin_skill(self) -> None:
407
+ registry = SlashCommandRegistry()
408
+ registry.register(
409
+ spec=SlashCommandSpec(
410
+ name="skillify",
411
+ description="custom skillify",
412
+ execution_kind="local",
413
+ ),
414
+ handler=_noop_handler,
415
+ )
416
+ completer = _FakeSlashCompleter()
417
+ app = TerminalAgentTUI.__new__(TerminalAgentTUI)
418
+ app._session = _build_session(list(get_builtin_skills()))
419
+ app._slash_registry = registry
420
+ app._skill_slash_command_names = set()
421
+ app._slash_completer = completer
422
+
423
+ app._sync_skill_slash_commands()
424
+
425
+ skillify = registry.resolve("skillify")
426
+ self.assertIsNotNone(skillify)
427
+ assert skillify is not None
428
+ self.assertEqual(skillify.source, "builtin")
429
+ self.assertEqual(app._skill_slash_command_names, set())
430
+
384
431
 
385
432
  if __name__ == "__main__":
386
433
  unittest.main()
@@ -389,7 +389,7 @@ wheels = [
389
389
 
390
390
  [[package]]
391
391
  name = "comate-agent-sdk"
392
- version = "0.6.7"
392
+ version = "0.6.9"
393
393
  source = { editable = "../../" }
394
394
  dependencies = [
395
395
  { name = "aiohttp" },
@@ -461,7 +461,7 @@ dev = [
461
461
 
462
462
  [[package]]
463
463
  name = "comate-cli"
464
- version = "0.5.6"
464
+ version = "0.5.8"
465
465
  source = { editable = "." }
466
466
  dependencies = [
467
467
  { name = "charset-normalizer" },
File without changes
File without changes
File without changes