comate-cli 0.5.4__tar.gz → 0.5.5__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.4 → comate_cli-0.5.5}/PKG-INFO +1 -1
  2. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/event_renderer.py +28 -6
  3. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/history_printer.py +8 -1
  4. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/rpc_stdio.py +1 -1
  5. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tool_view.py +2 -3
  6. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui.py +50 -82
  7. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/commands.py +1 -1
  8. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/input_behavior.py +23 -28
  9. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/key_bindings.py +63 -12
  10. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/render_panels.py +49 -10
  11. {comate_cli-0.5.4 → comate_cli-0.5.5}/pyproject.toml +1 -1
  12. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_completion_status_panel.py +3 -9
  13. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_event_renderer.py +22 -9
  14. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_event_renderer_boundary.py +13 -1
  15. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_event_renderer_streaming.py +94 -2
  16. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_history_printer.py +46 -0
  17. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_history_sync.py +2 -4
  18. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_interrupt_exit_semantics.py +16 -2
  19. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_question_key_bindings.py +15 -2
  20. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_rpc_stdio_bridge.py +2 -1
  21. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_task_panel_key_bindings.py +16 -1
  22. comate_cli-0.5.5/tests/test_tui_esc_queue.py +218 -0
  23. comate_cli-0.5.5/tests/test_tui_mcp_init_gate.py +80 -0
  24. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_tui_paste_placeholder.py +0 -170
  25. comate_cli-0.5.5/tests/test_tui_queue_preview.py +151 -0
  26. comate_cli-0.5.5/tests/test_tui_queue_sdk_source.py +273 -0
  27. {comate_cli-0.5.4 → comate_cli-0.5.5}/uv.lock +1438 -1453
  28. comate_cli-0.5.4/tests/test_tui_mcp_init_gate.py +0 -107
  29. {comate_cli-0.5.4 → comate_cli-0.5.5}/.gitignore +0 -0
  30. {comate_cli-0.5.4 → comate_cli-0.5.5}/README.md +0 -0
  31. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/__init__.py +0 -0
  32. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/__main__.py +0 -0
  33. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/main.py +0 -0
  34. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/mcp_cli.py +0 -0
  35. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/__init__.py +0 -0
  36. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/animations.py +0 -0
  37. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/app.py +0 -0
  38. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/assistant_render.py +0 -0
  39. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/codenames.py +0 -0
  40. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  41. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/env_utils.py +0 -0
  42. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/error_display.py +0 -0
  43. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/figures.py +0 -0
  44. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  45. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/input_geometry.py +0 -0
  46. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  47. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/logging_adapter.py +0 -0
  48. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/logo.py +0 -0
  49. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/markdown_render.py +0 -0
  50. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/mention_completer.py +0 -0
  51. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/message_style.py +0 -0
  52. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/models.py +0 -0
  53. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/path_context_hint.py +0 -0
  54. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
  55. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
  56. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
  57. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
  58. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
  59. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
  60. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
  61. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
  62. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
  63. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
  64. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
  65. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
  66. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
  67. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/preflight.py +0 -0
  68. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/question_view.py +0 -0
  69. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/resume_selector.py +0 -0
  70. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/rewind_store.py +0 -0
  71. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  72. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/selection_menu.py +0 -0
  73. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/session_view.py +0 -0
  74. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/slash_commands.py +0 -0
  75. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/startup.py +0 -0
  76. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/status_bar.py +0 -0
  77. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/text_effects.py +0 -0
  78. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tips.py +0 -0
  79. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  80. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
  81. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
  82. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  83. {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  84. {comate_cli-0.5.4 → comate_cli-0.5.5}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
  85. {comate_cli-0.5.4 → comate_cli-0.5.5}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
  86. {comate_cli-0.5.4 → comate_cli-0.5.5}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
  87. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/conftest.py +0 -0
  88. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_animator_shuffle.py +0 -0
  89. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_app_mcp_preload.py +0 -0
  90. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_app_preflight_gate.py +0 -0
  91. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_app_print_mode.py +0 -0
  92. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_app_shutdown.py +0 -0
  93. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_app_usage_line.py +0 -0
  94. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_cli_project_root.py +0 -0
  95. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_compact_command_semantics.py +0 -0
  96. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_completion_context_activation.py +0 -0
  97. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_context_command.py +0 -0
  98. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_custom_slash_commands.py +0 -0
  99. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_discover_tab.py +0 -0
  100. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_errors_tab.py +0 -0
  101. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_event_renderer_e2e.py +0 -0
  102. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_format_error.py +0 -0
  103. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_handle_error.py +0 -0
  104. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_input_behavior.py +0 -0
  105. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_input_history.py +0 -0
  106. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_installed_tab.py +0 -0
  107. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_layout_coordinator.py +0 -0
  108. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_logging_adapter.py +0 -0
  109. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_logo.py +0 -0
  110. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_main_args.py +0 -0
  111. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_markdown_render.py +0 -0
  112. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_marketplaces_tab.py +0 -0
  113. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_mcp_cli.py +0 -0
  114. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_mcp_slash_command.py +0 -0
  115. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_mention_completer.py +0 -0
  116. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_path_context_hint.py +0 -0
  117. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_plugin_slash_commands.py +0 -0
  118. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_plugin_tui_components.py +0 -0
  119. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_preflight.py +0 -0
  120. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_preflight_copilot.py +0 -0
  121. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_question_view.py +0 -0
  122. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_resume_selector.py +0 -0
  123. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_rewind_command_semantics.py +0 -0
  124. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_rewind_store.py +0 -0
  125. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_rpc_protocol.py +0 -0
  126. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_selection_menu.py +0 -0
  127. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_skills_slash_command.py +0 -0
  128. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_slash_argument_hint.py +0 -0
  129. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_slash_completer.py +0 -0
  130. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_slash_registry.py +0 -0
  131. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_status_bar.py +0 -0
  132. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_status_bar_transient.py +0 -0
  133. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_task_panel_format.py +0 -0
  134. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_task_panel_rendering.py +0 -0
  135. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_task_poll.py +0 -0
  136. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_tool_view.py +0 -0
  137. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_tui_elapsed_status.py +0 -0
  138. {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_tui_split_invariance.py +0 -0
  139. {comate_cli-0.5.4 → comate_cli-0.5.5}/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.4
3
+ Version: 0.5.5
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
@@ -279,12 +279,13 @@ class EventRenderer:
279
279
  self._show_thinking_cb: Callable[[], bool] = lambda: True
280
280
  self._turn_received_text_delta: bool = False
281
281
  self._turn_received_thinking_delta: bool = False
282
+ self._text_delta_started: bool = False
282
283
  # ━━━━━ spec §4.1 新管道状态字段(Phase 1.2 引入,Phase 4-6 接入) ━━━━━
283
284
  # Pipeline A: text line-commit
284
285
  self._text_pending: str = ""
285
286
  self._held_pipe_line: str | None = None
286
287
  self._container: ContainerState | None = None
287
- # Pipeline B: thinking batch
288
+ # Pipeline B: thinking line-commit + tail
288
289
  self._thinking_batch: str = ""
289
290
  # Pipeline C: loading 行 aux
290
291
  self._loading_aux_text: str = ""
@@ -367,7 +368,7 @@ class EventRenderer:
367
368
  self._loading_aux_text = ""
368
369
 
369
370
  def _flush_thinking_batch(self) -> None:
370
- """spec §6.5:thinking batch 整段 → HistoryEntry(受 _show_thinking_cb 过滤)。
371
+ """spec §6.5:thinking 残段 → HistoryEntry(受 _show_thinking_cb 过滤)。
371
372
  即使 hidden 也清空 batch 避免持续累积。"""
372
373
  text = self._thinking_batch
373
374
  self._thinking_batch = ""
@@ -379,6 +380,22 @@ class EventRenderer:
379
380
  HistoryEntry(entry_type="thinking", text=text)
380
381
  )
381
382
 
383
+ def _consume_thinking_delta(self, delta: str) -> None:
384
+ """spec §6.1:thinking delta 主入口。
385
+
386
+ 与 text delta 对称:按完整行即时 commit 到 scrollback,未换行尾巴留在
387
+ _thinking_batch,等待 text/tool/turn 边界通过 _flush_thinking_batch 落盘。
388
+ """
389
+ if not delta:
390
+ return
391
+ self._thinking_batch += delta
392
+ while "\n" in self._thinking_batch:
393
+ line, self._thinking_batch = self._thinking_batch.split("\n", 1)
394
+ if self._show_thinking_cb():
395
+ self._append_history_entry(
396
+ HistoryEntry(entry_type="thinking", text=line + "\n")
397
+ )
398
+
382
399
  # ── 行级路由(spec §6.2,Task 4.1-4.4 渐进扩展)──
383
400
 
384
401
  def _handle_complete_line(self, line: str) -> None:
@@ -457,7 +474,7 @@ class EventRenderer:
457
474
 
458
475
  把 delta 累到 _text_pending,按 \\n 切完整行送 _handle_complete_line。
459
476
 
460
- 关键顺序保证:text 进入前先 flush 待 thinking batch —— 保证
477
+ 关键顺序保证:text 进入前先 flush 待 thinking 残段 —— 保证
461
478
  scrollback 看到 "thinking → text" 而非 "text → thinking" 错序。
462
479
  """
463
480
  if not delta:
@@ -467,6 +484,9 @@ class EventRenderer:
467
484
  self._text_pending += delta
468
485
  while "\n" in self._text_pending:
469
486
  line, self._text_pending = self._text_pending.split("\n", 1)
487
+ if line == "" and not self._text_delta_started:
488
+ continue
489
+ self._text_delta_started = True
470
490
  self._handle_complete_line(line)
471
491
 
472
492
  # ── turn 边界(spec §6.4)──
@@ -509,6 +529,7 @@ class EventRenderer:
509
529
  self._container = None
510
530
  self._thinking_batch = ""
511
531
  self._loading_aux_text = ""
532
+ self._text_delta_started = False
512
533
 
513
534
  def _should_drop_duplicate_system_message(
514
535
  self,
@@ -541,6 +562,7 @@ class EventRenderer:
541
562
  self._pending_tool_starts.clear()
542
563
  self._turn_received_text_delta = False
543
564
  self._turn_received_thinking_delta = False
565
+ self._text_delta_started = False
544
566
  # spec §6.4:清空新 5 字段(每 turn 起点保证状态干净)
545
567
  self._text_pending = ""
546
568
  self._held_pipe_line = None
@@ -630,6 +652,7 @@ class EventRenderer:
630
652
  self._pending_tool_starts.clear()
631
653
  self._turn_received_text_delta = False
632
654
  self._turn_received_thinking_delta = False
655
+ self._text_delta_started = False
633
656
  # spec §6.4:清空 5 个新字段(与 start_turn 同语义)
634
657
  self._text_pending = ""
635
658
  self._held_pipe_line = None
@@ -1444,10 +1467,9 @@ class EventRenderer:
1444
1467
  self._turn_received_text_delta = True
1445
1468
  case ThinkingDeltaEvent(delta=delta, message_id=_):
1446
1469
  if delta:
1447
- # spec §6.1:thinking delta → batch 累加,段结束(StopEvent /
1448
- # text delta 来时)才一次性 commit
1449
- self._thinking_batch += delta
1470
+ # spec §6.1:thinking delta → \n 切完整行入队,残段留 batch
1450
1471
  self._turn_received_thinking_delta = True
1472
+ self._consume_thinking_delta(delta)
1451
1473
  case ToolCallStartEvent(tool_call_id=tool_call_id, tool=tool_name):
1452
1474
  # tool 调用前先把 _text_pending / _thinking_batch 落到 scrollback,
1453
1475
  # 保证说明文字出现在 tool UI 之前(无 \n 结尾的模型也适用)。
@@ -103,6 +103,7 @@ def render_history_group(
103
103
  in_run_continuation = is_assistant and prev_was_assistant
104
104
  next_entry = entries[idx + 1] if idx + 1 < len(entries) else None
105
105
  next_is_assistant = next_entry is not None and next_entry.entry_type == "assistant"
106
+ next_is_thinking = next_entry is not None and next_entry.entry_type == "thinking"
106
107
 
107
108
  # 跳过位于两个块标记 entry 之间的空 assistant entry(loose list 视觉对齐)
108
109
  if is_assistant and _is_blank_assistant(entry):
@@ -127,7 +128,13 @@ def render_history_group(
127
128
  line_text.append(" ", style="dim")
128
129
  line_text.append(line, style="dim")
129
130
  renderables.append(line_text)
130
- renderables.append(Text(""))
131
+ is_batch_tail = next_entry is None
132
+ suppress_trailing_gap = (
133
+ is_batch_tail
134
+ and assume_run_continues_at_tail
135
+ )
136
+ if not next_is_thinking and not suppress_trailing_gap:
137
+ renderables.append(Text(""))
131
138
  prev_was_assistant = False
132
139
  continue
133
140
 
@@ -228,7 +228,7 @@ class StdioRPCBridge:
228
228
  self._active_prompt_result = result_future
229
229
 
230
230
  try:
231
- await self._session.send(user_input)
231
+ _ = await self._session.send(user_input)
232
232
  except Exception as exc:
233
233
  self._clear_active_prompt(result_future)
234
234
  await self._send(
@@ -93,11 +93,10 @@ def should_show_tool_in_scrollback(
93
93
  return is_result and is_error
94
94
 
95
95
  if lowered == "taskcreate":
96
- return is_result
96
+ return is_result and is_error
97
97
 
98
98
  if lowered == "taskupdate":
99
- status = str(args.get("status", "")).strip().lower()
100
- return is_result and (is_error or status == "completed")
99
+ return is_result and is_error
101
100
 
102
101
  # Read-only task tools: only show errors.
103
102
  if lowered in _SILENT_TASK_TOOLS:
@@ -9,7 +9,6 @@ import asyncio
9
9
  import logging
10
10
  import random
11
11
  import time
12
- from collections import deque
13
12
  from collections.abc import Callable
14
13
  from contextlib import suppress
15
14
  from pathlib import Path
@@ -34,10 +33,14 @@ from comate_agent_sdk.agent import ChatSession
34
33
  from comate_agent_sdk.agent.events import (
35
34
  ExternalTurnScheduledEvent,
36
35
  PlanApprovalRequiredEvent,
36
+ QueueDirtyEvent,
37
+ QueueDirtyReason,
38
+ QueuedMessageInjectedEvent,
37
39
  SessionInitEvent,
38
40
  StopEvent,
39
41
  TeamMessageEvent,
40
42
  )
43
+ from comate_agent_sdk.agent.queue_types import MessageOrigin
41
44
 
42
45
  from comate_cli.terminal_agent.animations import (
43
46
  DEFAULT_STATUS_PHRASES,
@@ -196,22 +199,21 @@ class TerminalAgentTUI(
196
199
  "AGENT_SDK_TUI_TODO_PANEL_EXPANDED_MAX_LINES",
197
200
  12,
198
201
  )
199
- self._message_queue_max_size = read_env_int(
200
- "AGENT_SDK_TUI_MESSAGE_QUEUE_MAX_SIZE",
201
- 3,
202
- )
203
202
  self._queued_preview_max_chars = read_env_int(
204
203
  "AGENT_SDK_TUI_QUEUED_PREVIEW_MAX_CHARS",
205
204
  24,
206
205
  )
206
+ self._queued_preview_max_rows = read_env_int(
207
+ "AGENT_SDK_TUI_QUEUED_PREVIEW_MAX_ROWS",
208
+ 3,
209
+ )
207
210
 
208
211
  self._busy = False
209
- self._initializing = False # MCP 加载中
210
212
  self._is_compacting = False
211
213
  self._compact_task: asyncio.Task[Any] | None = None
212
214
  self._compact_cancel_requested = False
213
215
  self._pending_exit_after_compact_cancel = False
214
- self._queued_messages: deque[str] = deque() # busy 时入队的消息(FIFO)
216
+ self._queued_display_by_message_id: dict[str, str] = {}
215
217
  self._waiting_for_input = False
216
218
  self._pending_questions: list[dict[str, Any]] | None = None
217
219
  self._pending_plan_approval: dict[str, str] | None = None
@@ -288,10 +290,6 @@ class TerminalAgentTUI(
288
290
  self._ctrl_c_press_count: int = 0
289
291
  ctrl_c_window_ms = read_env_int("AGENT_SDK_TUI_CTRL_C_EXIT_WINDOW_MS", 700)
290
292
  self._ctrl_c_exit_window_seconds = ctrl_c_window_ms / 1000.0
291
- self._mcp_init_gate_timeout_s = read_env_float(
292
- "AGENT_SDK_TUI_MCP_INIT_GATE_TIMEOUT_S",
293
- 8.0,
294
- )
295
293
  self._mcp_init_cancel_timeout_s = read_env_float(
296
294
  "AGENT_SDK_TUI_MCP_INIT_CANCEL_TIMEOUT_S",
297
295
  1.0,
@@ -559,8 +557,8 @@ class TerminalAgentTUI(
559
557
  "status.transient.warning": "bg:default italic fg:ansiyellow",
560
558
  "input.placeholder": "bg:default #9CA3AF",
561
559
  "auto-suggestion": "bg:default #94a3b8",
562
- "queue": "bg:#1d222a #d8dee9",
563
- "queue.item": "bg:#1d222a #cbd5e1",
560
+ "queue": "bg:default #d8dee9",
561
+ "queue.item": "bg:default #cbd5e1",
564
562
  "git-diff.added": "#4ade80",
565
563
  "git-diff.removed": "#f87171",
566
564
  "question.tabs": "bg:default #c7d2fe",
@@ -638,9 +636,7 @@ class TerminalAgentTUI(
638
636
  )
639
637
 
640
638
  def _should_show_queue_panel(self) -> bool:
641
- return self._ui_mode == UIMode.NORMAL and len(self._queued_messages) >= 1
642
-
643
- _QUEUED_SLASH_PREFIX = "__COMATE_QUEUED_SLASH__::"
639
+ return self._ui_mode == UIMode.NORMAL and bool(self._queued_human_messages())
644
640
 
645
641
  def _build_slash_registry(self) -> None:
646
642
  handlers: dict[str, Callable[[str], Any]] = {
@@ -729,26 +725,6 @@ class TerminalAgentTUI(
729
725
  return None
730
726
  return entry.spec
731
727
 
732
- def _encode_queued_slash(self, raw_command: str) -> str:
733
- return f"{self._QUEUED_SLASH_PREFIX}{raw_command}"
734
-
735
- def _is_encoded_queued_slash(self, value: str) -> bool:
736
- return value.startswith(self._QUEUED_SLASH_PREFIX)
737
-
738
- def _decode_queued_slash(self, value: str) -> str:
739
- if not self._is_encoded_queued_slash(value):
740
- return value
741
- return value[len(self._QUEUED_SLASH_PREFIX) :]
742
-
743
- def _queued_preview_text(self, value: str) -> str:
744
- return self._decode_queued_slash(value)
745
-
746
- def _dispatch_queued_item(self, queued: str) -> None:
747
- if self._is_encoded_queued_slash(queued):
748
- self._schedule_background(self._execute_command(self._decode_queued_slash(queued)))
749
- return
750
- self._schedule_background(self._submit_user_message(queued))
751
-
752
728
  def _create_input_history(self) -> FileHistory | InMemoryHistory:
753
729
  """Create persistent FileHistory based on cwd, with fallback to InMemoryHistory."""
754
730
  try:
@@ -771,15 +747,16 @@ class TerminalAgentTUI(
771
747
  def _input_placeholder_hint(self) -> str | None:
772
748
  if self._ui_mode != UIMode.NORMAL:
773
749
  return None
774
- if not self._queued_messages:
750
+ queued_messages = self._queued_human_messages()
751
+ if not queued_messages:
775
752
  return None
776
753
  input_text = str(getattr(getattr(self, "_input_area", None), "text", ""))
777
754
  if input_text.strip():
778
755
  return None
779
- if len(self._queued_messages) >= 2:
756
+ if len(queued_messages) >= 2:
780
757
  return self._queued_input_hint
781
758
 
782
- preview = " ".join(self._queued_preview_text(self._queued_messages[0]).split())
759
+ preview = self._queued_message_preview(queued_messages[0])
783
760
  max_chars = max(8, int(self._queued_preview_max_chars))
784
761
  if len(preview) > max_chars:
785
762
  preview = f"{preview[: max_chars - 3]}..."
@@ -931,6 +908,28 @@ class TerminalAgentTUI(
931
908
  await self._animation_controller.on_event(event)
932
909
  if isinstance(event, SessionInitEvent):
933
910
  self._initialized_session_id = str(event.session_id)
911
+ if isinstance(event, QueueDirtyEvent):
912
+ if event.reason in {
913
+ QueueDirtyReason.CANCELLED,
914
+ QueueDirtyReason.CLEARED,
915
+ }:
916
+ for message_id in event.message_ids:
917
+ self._queued_display_by_message_id.pop(
918
+ str(message_id),
919
+ None,
920
+ )
921
+ self._refresh_layers()
922
+ self._invalidate()
923
+ if isinstance(event, QueuedMessageInjectedEvent):
924
+ for message_id, origin in zip(event.message_ids, event.origins):
925
+ if origin is not MessageOrigin.HUMAN:
926
+ continue
927
+ display_text = self._queued_display_by_message_id.pop(
928
+ str(message_id),
929
+ None,
930
+ )
931
+ if display_text:
932
+ self._renderer.seed_user_message(display_text)
934
933
  if isinstance(event, ExternalTurnScheduledEvent) and not self._busy:
935
934
  self._session.run_controller.clear()
936
935
  self._interrupt_requested_at = None
@@ -1013,14 +1012,6 @@ class TerminalAgentTUI(
1013
1012
 
1014
1013
  self._refresh_layers()
1015
1014
 
1016
- if (
1017
- self._queued_messages
1018
- and not waiting_for_input
1019
- and plan_approval is None
1020
- ):
1021
- queued = self._queued_messages.popleft()
1022
- self._dispatch_queued_item(queued)
1023
-
1024
1015
  waiting_for_input = False
1025
1016
  questions = None
1026
1017
  plan_approval = None
@@ -1285,17 +1276,16 @@ class TerminalAgentTUI(
1285
1276
  if self._renderer.has_running_tools():
1286
1277
  # Keep repainting for breathing animation.
1287
1278
  self._render_dirty = True
1288
- elif self._busy or self._initializing:
1279
+ elif self._busy:
1289
1280
  self._render_dirty = True
1290
1281
 
1291
1282
  if self._render_dirty:
1292
1283
  self._invalidate()
1293
1284
  self._render_dirty = False
1294
1285
 
1295
- # 动态帧率:busy/动画/initializing 时降为 6fps(缓解 scrollback 污染),idle 时 4fps
1286
+ # 动态帧率:busy/动画时降为 6fps(缓解 scrollback 污染),idle 时 4fps
1296
1287
  fast = (
1297
1288
  self._busy
1298
- or self._initializing
1299
1289
  or self._animator.is_active
1300
1290
  or self._renderer.has_running_tools()
1301
1291
  )
@@ -1408,45 +1398,23 @@ class TerminalAgentTUI(
1408
1398
  logger.exception("Plugin init failed at startup; continuing without plugins")
1409
1399
 
1410
1400
  if mcp_init is not None:
1411
- self._initializing = True
1412
-
1413
1401
  async def _do_init() -> None:
1414
- init_task = asyncio.create_task(mcp_init(), name="terminal-mcp-init-worker")
1415
1402
  try:
1416
- await asyncio.wait_for(
1417
- asyncio.shield(init_task),
1418
- timeout=self._mcp_init_gate_timeout_s,
1419
- )
1420
- except asyncio.TimeoutError:
1421
- logger.debug(
1422
- "MCP init timed out after "
1423
- f"{self._mcp_init_gate_timeout_s:.1f}s; continuing in background",
1424
- )
1425
- self._status_bar.show_transient(
1426
- f"MCP: init timed out, continuing in background{ELLIPSIS}",
1427
- duration_s=5.0,
1428
- )
1429
- # Don't cancel init_task — let it complete in background.
1430
- # asyncio.shield above already protects it from wait_for's
1431
- # auto-cancellation. When it finishes, runtime._mcp_manager
1432
- # will be set and tools become available for the next turn.
1403
+ await mcp_init()
1433
1404
  except asyncio.CancelledError:
1434
- await self._cancel_task_with_timeout(
1435
- init_task,
1436
- timeout_s=self._mcp_init_cancel_timeout_s,
1437
- task_name="terminal-mcp-init-worker",
1438
- )
1439
- return
1405
+ raise
1440
1406
  except Exception as exc:
1441
- logger.debug(f"MCP init failed in TUI bootstrap: {exc}", exc_info=True)
1407
+ logger.debug(
1408
+ f"MCP init failed in TUI bootstrap: {exc}",
1409
+ exc_info=True,
1410
+ )
1442
1411
  finally:
1443
- self._initializing = False
1444
- if self._queued_messages and not self._busy:
1445
- queued = self._queued_messages.popleft()
1446
- self._dispatch_queued_item(queued)
1447
1412
  self._refresh_layers()
1448
1413
 
1449
- self._mcp_init_task = asyncio.create_task(_do_init(), name="terminal-mcp-init")
1414
+ self._mcp_init_task = asyncio.create_task(
1415
+ _do_init(),
1416
+ name="terminal-mcp-init",
1417
+ )
1450
1418
 
1451
1419
  self._ui_tick_task = asyncio.create_task(
1452
1420
  self._ui_tick(),
@@ -81,7 +81,7 @@ class CommandsMixin:
81
81
  self._refresh_layers()
82
82
  return
83
83
 
84
- is_busy = self._busy or self._initializing
84
+ is_busy = self._busy
85
85
  if is_busy and not entry.allow_when_busy:
86
86
  self._renderer.append_system_message(
87
87
  f"当前任务运行中,暂不可执行 /{entry.spec.name}。",
@@ -5,6 +5,8 @@ from typing import Any
5
5
 
6
6
  from prompt_toolkit.document import Document
7
7
 
8
+ from comate_agent_sdk.agent.chat_session import ChatSessionQueueFullError
9
+
8
10
  from comate_cli.terminal_agent.input_geometry import (
9
11
  compute_visual_line_ranges,
10
12
  index_for_visual_col,
@@ -333,8 +335,9 @@ class InputBehaviorMixin:
333
335
  self._input_area.buffer.cancel_completion()
334
336
  self._clear_input_area(save_to_history=True)
335
337
 
336
- # busy initializing 时:非斜杠命令 入队,斜杠命令 → 交由命令分发决定
337
- is_busy = self._busy or self._initializing
338
+ # busy 时:TUI 不再维护本地消息 deque,统一交给 SDK 队列。
339
+ # MCP preload 只在后台准备工具,不阻塞首条用户消息的回显与提交。
340
+ is_busy = self._busy
338
341
  if is_busy:
339
342
  if raw_text.startswith("/"):
340
343
  normalized_slash = display_text.strip()
@@ -343,39 +346,31 @@ class InputBehaviorMixin:
343
346
  entry = registry.resolve(parsed.name) if (registry is not None and parsed is not None) else None
344
347
 
345
348
  if entry is not None and str(getattr(entry, "source", "")) == "custom":
346
- queue_size = len(self._queued_messages)
347
- if queue_size >= int(self._message_queue_max_size):
348
- self._renderer.append_system_message(
349
- f"消息队列已满({queue_size}/{self._message_queue_max_size}),请等待当前任务完成。",
350
- severity="error",
351
- )
352
- self._refresh_layers()
353
- return
354
-
355
- encode_fn = getattr(self, "_encode_queued_slash", None)
356
- queued_value = (
357
- str(encode_fn(normalized_slash))
358
- if callable(encode_fn)
359
- else normalized_slash
349
+ self._renderer.append_system_message(
350
+ "当前任务运行中,custom slash command 不能排队执行,请等待当前任务结束后再运行。",
351
+ severity="error",
360
352
  )
361
- self._queued_messages.append(queued_value)
362
- self._invalidate()
353
+ self._refresh_layers()
363
354
  return
364
355
 
365
356
  self._schedule_background(self._execute_command(normalized_slash))
366
357
  return
367
358
 
368
359
  if not raw_text.startswith("/"):
369
- queue_size = len(self._queued_messages)
370
- if queue_size >= int(self._message_queue_max_size):
371
- self._renderer.append_system_message(
372
- f"消息队列已满({queue_size}/{self._message_queue_max_size}),请等待当前任务完成。",
373
- severity="error",
374
- )
375
- self._refresh_layers()
376
- return
377
- self._queued_messages.append(submit_text)
378
- self._invalidate()
360
+ async def _send_busy_message(content: str, display: str) -> None:
361
+ try:
362
+ message_id = await self._session.send(content)
363
+ self._queued_display_by_message_id[str(message_id)] = display
364
+ except ChatSessionQueueFullError:
365
+ self._renderer.append_system_message(
366
+ "消息队列已满,请等待当前任务完成。",
367
+ severity="error",
368
+ )
369
+ self._refresh_layers()
370
+
371
+ self._schedule_background(
372
+ _send_busy_message(submit_text, display_text)
373
+ )
379
374
  return
380
375
 
381
376
  if raw_text.startswith("/"):
@@ -6,6 +6,8 @@ from prompt_toolkit.filters import Condition, has_completions, has_focus
6
6
  from prompt_toolkit.key_binding import KeyBindings
7
7
  from prompt_toolkit.keys import Keys
8
8
 
9
+ from comate_agent_sdk.agent.queue_types import MessageOrigin
10
+
9
11
  from comate_cli.terminal_agent.slash_commands import parse_slash_command_call
10
12
  from comate_cli.terminal_agent.tui_parts.ui_mode import UIMode
11
13
 
@@ -334,18 +336,6 @@ class KeyBindingsMixin:
334
336
  @bindings.add("up", filter=normal_mode & ~diff_panel_open & ~todo_panel_expanded)
335
337
  def _active_prev_up(event) -> None:
336
338
  buffer = event.current_buffer
337
- # busy + 有 queue + input 为空 → 取回最近一条 queue,加载回 input
338
- is_busy = self._busy or self._initializing
339
- if is_busy and self._queued_messages and not buffer.text.strip():
340
- queued = self._queued_messages.pop()
341
- decode_fn = getattr(self, "_queued_preview_text", None)
342
- if callable(decode_fn):
343
- buffer.text = str(decode_fn(queued))
344
- else:
345
- buffer.text = str(queued)
346
- buffer.cursor_position = len(buffer.text)
347
- self._invalidate()
348
- return
349
339
  if self._move_completion_selection(buffer, backward=True):
350
340
  return
351
341
  if self._move_cursor_visual(buffer, backward=True):
@@ -375,6 +365,67 @@ class KeyBindingsMixin:
375
365
  self._esc_last_pressed_at = now
376
366
  return
377
367
 
368
+ is_busy = False
369
+ is_busy_fn = getattr(self._session, "is_busy", None)
370
+ if callable(is_busy_fn):
371
+ is_busy = bool(is_busy_fn())
372
+ else:
373
+ is_busy = bool(self._busy)
374
+ if is_busy:
375
+ self._session.run_controller.interrupt(reason="user_esc")
376
+ self._esc_press_count = 0
377
+ self._esc_last_pressed_at = now
378
+ return
379
+
380
+ peek_queue = getattr(self._session, "peek_queue", None)
381
+ queued_messages = tuple(peek_queue()) if callable(peek_queue) else ()
382
+ human_messages = [
383
+ message
384
+ for message in queued_messages
385
+ if getattr(message, "origin", None) == MessageOrigin.HUMAN
386
+ ]
387
+ if human_messages:
388
+ def _editable_text(message) -> str: # type: ignore[no-untyped-def]
389
+ content = getattr(message, "content", "")
390
+ if isinstance(content, str):
391
+ return content
392
+ if isinstance(content, list):
393
+ pieces: list[str] = []
394
+ for part in content:
395
+ if isinstance(part, dict):
396
+ part_type = str(part.get("type", ""))
397
+ if part_type == "text":
398
+ pieces.append(str(part.get("text", "")))
399
+ elif "image" in part_type:
400
+ pieces.append("[image]")
401
+ else:
402
+ pieces.append(str(part))
403
+ return "\n".join(piece for piece in pieces if piece)
404
+ return str(content)
405
+
406
+ text = "\n\n".join(
407
+ piece
408
+ for piece in (_editable_text(message) for message in human_messages)
409
+ if piece
410
+ )
411
+ buffer.text = text
412
+ buffer.cursor_position = len(text)
413
+ message_ids = tuple(
414
+ str(getattr(message, "message_id"))
415
+ for message in human_messages
416
+ )
417
+ cancel_many = getattr(self._session, "cancel_many", None)
418
+ if callable(cancel_many):
419
+ cancel_many(message_ids)
420
+ queued_display = getattr(self, "_queued_display_by_message_id", None)
421
+ if isinstance(queued_display, dict):
422
+ for message_id in message_ids:
423
+ queued_display.pop(message_id, None)
424
+ self._esc_press_count = 0
425
+ self._esc_last_pressed_at = now
426
+ self._invalidate()
427
+ return
428
+
378
429
  if now - self._esc_last_pressed_at > self._esc_clear_window_seconds:
379
430
  self._esc_press_count = 0
380
431
  self._esc_press_count += 1