comate-cli 0.6.0__tar.gz → 0.6.1__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 (143) hide show
  1. {comate_cli-0.6.0 → comate_cli-0.6.1}/PKG-INFO +1 -1
  2. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/app.py +3 -2
  3. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/event_renderer.py +66 -7
  4. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/history_printer.py +101 -6
  5. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/logging_adapter.py +11 -34
  6. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/models.py +12 -1
  7. comate_cli-0.6.1/comate_cli/terminal_agent/tips.py +21 -0
  8. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui.py +16 -2
  9. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/commands.py +133 -94
  10. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/render_panels.py +41 -4
  11. {comate_cli-0.6.0 → comate_cli-0.6.1}/pyproject.toml +1 -1
  12. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_compact_command_semantics.py +119 -5
  13. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_completion_status_panel.py +40 -0
  14. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_context_command.py +43 -0
  15. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_event_renderer.py +24 -7
  16. comate_cli-0.6.1/tests/test_event_renderer_log_boundary.py +175 -0
  17. comate_cli-0.6.1/tests/test_event_renderer_log_queue.py +114 -0
  18. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_event_renderer_streaming.py +24 -0
  19. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_history_printer.py +2 -2
  20. comate_cli-0.6.1/tests/test_history_printer_log.py +199 -0
  21. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_logging_adapter.py +69 -17
  22. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_update_check.py +49 -0
  23. {comate_cli-0.6.0 → comate_cli-0.6.1}/uv.lock +1453 -1437
  24. comate_cli-0.6.0/comate_cli/terminal_agent/tips.py +0 -15
  25. {comate_cli-0.6.0 → comate_cli-0.6.1}/.gitignore +0 -0
  26. {comate_cli-0.6.0 → comate_cli-0.6.1}/README.md +0 -0
  27. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/__init__.py +0 -0
  28. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/__main__.py +0 -0
  29. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/main.py +0 -0
  30. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/mcp_cli.py +0 -0
  31. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/__init__.py +0 -0
  32. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/animations.py +0 -0
  33. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/assistant_render.py +0 -0
  34. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/codenames.py +0 -0
  35. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  36. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/env_utils.py +0 -0
  37. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/error_display.py +0 -0
  38. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/figures.py +0 -0
  39. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  40. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/input_geometry.py +0 -0
  41. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  42. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/logo.py +0 -0
  43. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/markdown_render.py +0 -0
  44. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/mention_completer.py +0 -0
  45. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/message_style.py +0 -0
  46. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/path_context_hint.py +0 -0
  47. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
  48. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
  49. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
  50. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
  51. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
  52. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
  53. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
  54. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
  55. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
  56. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
  57. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
  58. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
  59. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
  60. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/preflight.py +0 -0
  61. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/question_view.py +0 -0
  62. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/resume_selector.py +0 -0
  63. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/rewind_store.py +0 -0
  64. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  65. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  66. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/selection_menu.py +0 -0
  67. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/session_view.py +0 -0
  68. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/slash_commands.py +0 -0
  69. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/startup.py +0 -0
  70. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/status_bar.py +0 -0
  71. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/text_effects.py +0 -0
  72. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tool_view.py +0 -0
  73. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  74. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
  75. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
  76. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
  77. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
  78. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  79. {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  80. {comate_cli-0.6.0 → comate_cli-0.6.1}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
  81. {comate_cli-0.6.0 → comate_cli-0.6.1}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
  82. {comate_cli-0.6.0 → comate_cli-0.6.1}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
  83. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/conftest.py +0 -0
  84. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_animator_shuffle.py +0 -0
  85. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_app_mcp_preload.py +0 -0
  86. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_app_preflight_gate.py +0 -0
  87. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_app_print_mode.py +0 -0
  88. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_app_shutdown.py +0 -0
  89. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_app_usage_line.py +0 -0
  90. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_cli_project_root.py +0 -0
  91. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_completion_context_activation.py +0 -0
  92. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_custom_slash_commands.py +0 -0
  93. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_discover_tab.py +0 -0
  94. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_errors_tab.py +0 -0
  95. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_event_renderer_boundary.py +0 -0
  96. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_event_renderer_e2e.py +0 -0
  97. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_format_error.py +0 -0
  98. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_handle_error.py +0 -0
  99. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_history_sync.py +0 -0
  100. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_input_behavior.py +0 -0
  101. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_input_history.py +0 -0
  102. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_installed_tab.py +0 -0
  103. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_interrupt_exit_semantics.py +0 -0
  104. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_layout_coordinator.py +0 -0
  105. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_logo.py +0 -0
  106. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_main_args.py +0 -0
  107. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_markdown_render.py +0 -0
  108. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_marketplaces_tab.py +0 -0
  109. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_mcp_cli.py +0 -0
  110. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_mcp_slash_command.py +0 -0
  111. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_mention_completer.py +0 -0
  112. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_path_context_hint.py +0 -0
  113. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_plugin_slash_commands.py +0 -0
  114. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_plugin_tui_components.py +0 -0
  115. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_preflight.py +0 -0
  116. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_preflight_copilot.py +0 -0
  117. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_question_key_bindings.py +0 -0
  118. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_question_view.py +0 -0
  119. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_resume_selector.py +0 -0
  120. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_rewind_command_semantics.py +0 -0
  121. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_rewind_store.py +0 -0
  122. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_rpc_protocol.py +0 -0
  123. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_rpc_stdio_bridge.py +0 -0
  124. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_selection_menu.py +0 -0
  125. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_skills_slash_command.py +0 -0
  126. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_slash_argument_hint.py +0 -0
  127. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_slash_completer.py +0 -0
  128. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_slash_registry.py +0 -0
  129. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_status_bar.py +0 -0
  130. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_status_bar_transient.py +0 -0
  131. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_task_panel_format.py +0 -0
  132. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_task_panel_key_bindings.py +0 -0
  133. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_task_panel_rendering.py +0 -0
  134. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_task_poll.py +0 -0
  135. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tool_view.py +0 -0
  136. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_elapsed_status.py +0 -0
  137. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_esc_queue.py +0 -0
  138. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_mcp_init_gate.py +0 -0
  139. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_paste_placeholder.py +0 -0
  140. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_queue_preview.py +0 -0
  141. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_queue_sdk_source.py +0 -0
  142. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_split_invariance.py +0 -0
  143. {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_team_messages.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comate-cli
3
- Version: 0.6.0
3
+ Version: 0.6.1
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
@@ -22,7 +22,6 @@ from comate_agent_sdk.tools import tool
22
22
 
23
23
  from comate_cli.terminal_agent.event_renderer import EventRenderer
24
24
  from comate_cli.terminal_agent.logo import print_logo
25
- from comate_cli.terminal_agent.tips import TIPS
26
25
  from comate_cli.terminal_agent.preflight import run_preflight_if_needed
27
26
  from comate_cli.terminal_agent.resume_selector import select_resume_session_id
28
27
  from comate_cli.terminal_agent.rpc_stdio import StdioRPCBridge
@@ -357,7 +356,6 @@ async def run(
357
356
  return
358
357
 
359
358
  print_logo(console, project_root=project_root)
360
- console.print(f"[dim]💡 Tip: {random.choice(TIPS)}[/dim]")
361
359
 
362
360
  if resume_select and not resume_session_id:
363
361
  selected_session_id = await select_resume_session_id(console, cwd=project_root)
@@ -373,6 +371,9 @@ async def run(
373
371
  logging_session = setup_tui_logging(renderer, project_root=project_root)
374
372
 
375
373
  session, mode = _resolve_session(agent, resume_session_id, cwd=project_root)
374
+ # setup_tui_logging 在 _resolve_session 前安装,用于捕获 session 初始化期 warning/error。
375
+ # 这些日志没有交互 anchor,需在恢复历史或首次用户输入前落为 standalone log。
376
+ renderer.flush_pending_logs()
376
377
 
377
378
  # 版本检查:带超时,不阻塞启动
378
379
  try:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  import re
5
+ import threading
5
6
  import time
6
7
  from collections import deque
7
8
  from dataclasses import dataclass, field
@@ -280,6 +281,7 @@ class EventRenderer:
280
281
  self._turn_received_text_delta: bool = False
281
282
  self._turn_received_thinking_delta: bool = False
282
283
  self._text_delta_started: bool = False
284
+ self._active_text_message_id: str | None = None
283
285
  # ━━━━━ spec §4.1 新管道状态字段(Phase 1.2 引入,Phase 4-6 接入) ━━━━━
284
286
  # Pipeline A: text line-commit
285
287
  self._text_pending: str = ""
@@ -289,10 +291,14 @@ class EventRenderer:
289
291
  self._thinking_batch: str = ""
290
292
  # Pipeline C: loading 行 aux
291
293
  self._loading_aux_text: str = ""
294
+ self._pending_logs: list[tuple[Literal["warning", "error"], str]] = []
295
+ self._pending_logs_lock = threading.Lock()
292
296
 
293
297
  def _append_history_entry(self, entry: HistoryEntry) -> None:
294
298
  self._history.append(entry)
295
299
  self._last_history_append_at = time.monotonic()
300
+ if entry.entry_type == "tool_result":
301
+ self.flush_pending_logs()
296
302
 
297
303
  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
298
304
  # spec §6 流式新管道方法集(Phase 1-6 新增;Phase 7 接入 handle_event)
@@ -484,7 +490,7 @@ class EventRenderer:
484
490
  self._text_pending += delta
485
491
  while "\n" in self._text_pending:
486
492
  line, self._text_pending = self._text_pending.split("\n", 1)
487
- if line == "" and not self._text_delta_started:
493
+ if line.strip() == "" and not self._text_delta_started:
488
494
  continue
489
495
  self._text_delta_started = True
490
496
  self._handle_complete_line(line)
@@ -530,6 +536,7 @@ class EventRenderer:
530
536
  self._thinking_batch = ""
531
537
  self._loading_aux_text = ""
532
538
  self._text_delta_started = False
539
+ self._active_text_message_id = None
533
540
 
534
541
  def _should_drop_duplicate_system_message(
535
542
  self,
@@ -563,6 +570,7 @@ class EventRenderer:
563
570
  self._turn_received_text_delta = False
564
571
  self._turn_received_thinking_delta = False
565
572
  self._text_delta_started = False
573
+ self._active_text_message_id = None
566
574
  # spec §6.4:清空新 5 字段(每 turn 起点保证状态干净)
567
575
  self._text_pending = ""
568
576
  self._held_pipe_line = None
@@ -570,6 +578,7 @@ class EventRenderer:
570
578
  self._thinking_batch = ""
571
579
  self._loading_aux_text = ""
572
580
  self._rebuild_loading_line()
581
+ self.flush_pending_logs()
573
582
 
574
583
  def seed_user_message(self, content: str) -> None:
575
584
  normalized = content.strip()
@@ -578,9 +587,10 @@ class EventRenderer:
578
587
  self._flush_assistant_segment()
579
588
  self._append_history_entry(HistoryEntry(entry_type="user", text=normalized))
580
589
  self._maybe_append_file_ref_hint(normalized)
590
+ self.flush_pending_logs()
581
591
 
582
592
  def _maybe_append_file_ref_hint(self, text: str) -> None:
583
- """Append a dim ⎿ hint for the first valid @path reference."""
593
+ """Append dim ⎿ hints for valid @path references."""
584
594
  if self._project_root is None:
585
595
  return
586
596
  for match in FILE_REF_PATTERN.finditer(text):
@@ -591,7 +601,7 @@ class EventRenderer:
591
601
  self._append_history_entry(
592
602
  HistoryEntry(entry_type="file_ref", text=f"Listed directory {raw_path}")
593
603
  )
594
- return
604
+ continue
595
605
  if candidate.is_file():
596
606
  hint = f"Read {raw_path}"
597
607
  if candidate.stat().st_size <= _FILE_REF_MAX_COUNT_BYTES:
@@ -602,7 +612,7 @@ class EventRenderer:
602
612
  except OSError:
603
613
  pass
604
614
  self._append_history_entry(HistoryEntry(entry_type="file_ref", text=hint))
605
- return
615
+ continue
606
616
  except OSError:
607
617
  continue
608
618
 
@@ -614,6 +624,7 @@ class EventRenderer:
614
624
  # text_pending / thinking)保证 scrollback 看到完整内容
615
625
  self._force_flush_all()
616
626
  self._flush_assistant_segment()
627
+ self.flush_pending_logs()
617
628
  self._pending_tool_starts.clear()
618
629
  self._rebuild_loading_line()
619
630
 
@@ -653,6 +664,7 @@ class EventRenderer:
653
664
  self._turn_received_text_delta = False
654
665
  self._turn_received_thinking_delta = False
655
666
  self._text_delta_started = False
667
+ self._active_text_message_id = None
656
668
  # spec §6.4:清空 5 个新字段(与 start_turn 同语义)
657
669
  self._text_pending = ""
658
670
  self._held_pipe_line = None
@@ -664,6 +676,8 @@ class EventRenderer:
664
676
  self._current_task_title = None
665
677
  self._task_started_at_monotonic = None
666
678
  self._latest_diff_lines = None
679
+ with self._pending_logs_lock:
680
+ self._pending_logs.clear()
667
681
 
668
682
  @property
669
683
  def latest_diff_lines(self) -> list[str] | None:
@@ -718,12 +732,19 @@ class EventRenderer:
718
732
  拼接到 spinner phrase。无容器时返回 ""。"""
719
733
  return self._loading_aux_text
720
734
 
721
- def append_subtitle(self, text: str) -> None:
735
+ def append_subtitle(
736
+ self,
737
+ text: str,
738
+ *,
739
+ severity: Literal["info", "warning", "error"] = "info",
740
+ ) -> None:
722
741
  """Append a ⎿ subtitle entry, visually attached to the preceding entry."""
723
742
  normalized = text.strip()
724
743
  if not normalized:
725
744
  return
726
- self._append_history_entry(HistoryEntry(entry_type="file_ref", text=normalized))
745
+ self._append_history_entry(
746
+ HistoryEntry(entry_type="file_ref", text=normalized, severity=severity)
747
+ )
727
748
 
728
749
  def append_system_message(
729
750
  self,
@@ -744,6 +765,39 @@ class EventRenderer:
744
765
  HistoryEntry(entry_type="system", text=normalized, severity=severity)
745
766
  )
746
767
 
768
+ def enqueue_log(
769
+ self,
770
+ *,
771
+ severity: Literal["warning", "error"],
772
+ message: str,
773
+ ) -> None:
774
+ """线程安全入口:只入队,不调度 prompt_toolkit,不立即写 scrollback。"""
775
+ normalized = message.strip()
776
+ if not normalized:
777
+ return
778
+ with self._pending_logs_lock:
779
+ self._pending_logs.append((severity, normalized))
780
+
781
+ def flush_pending_logs(self) -> None:
782
+ """Drain pending log,按当前 history anchor 决定 attached 形态。"""
783
+ with self._pending_logs_lock:
784
+ if not self._pending_logs:
785
+ return
786
+ drained = list(self._pending_logs)
787
+ self._pending_logs.clear()
788
+
789
+ has_anchor = bool(self._history)
790
+ for severity, message in drained:
791
+ self._append_history_entry(
792
+ HistoryEntry(
793
+ entry_type="log",
794
+ text=message,
795
+ severity=severity,
796
+ attached=has_anchor,
797
+ )
798
+ )
799
+ has_anchor = True
800
+
747
801
  @staticmethod
748
802
  def _team_event_key(
749
803
  *,
@@ -943,6 +997,7 @@ class EventRenderer:
943
997
  HistoryEntry(entry_type="assistant", text=self._assistant_buffer)
944
998
  )
945
999
  self._assistant_buffer = ""
1000
+ self.flush_pending_logs()
946
1001
 
947
1002
  def _append_assistant_text(self, text: str) -> None:
948
1003
  self._assistant_buffer += text
@@ -1460,8 +1515,12 @@ class EventRenderer:
1460
1515
  match event:
1461
1516
  case SessionInitEvent(session_id=_):
1462
1517
  pass
1463
- case TextDeltaEvent(delta=delta, message_id=_):
1518
+ case TextDeltaEvent(delta=delta, message_id=message_id):
1464
1519
  if delta:
1520
+ normalized_message_id = str(message_id or "").strip()
1521
+ if normalized_message_id and normalized_message_id != self._active_text_message_id:
1522
+ self._active_text_message_id = normalized_message_id
1523
+ self._text_delta_started = False
1465
1524
  # spec §6.1:text delta → 累到 _text_pending → 按 \n 切完整行入队
1466
1525
  self._consume_text_delta(delta)
1467
1526
  self._turn_received_text_delta = True
@@ -19,14 +19,50 @@ from comate_cli.terminal_agent.message_style import ASSISTANT_PREFIX
19
19
  from comate_cli.terminal_agent.models import HistoryEntry
20
20
 
21
21
 
22
- def _render_subtitle_line(subtitle: str, *, error: bool = False) -> Text:
23
- """Render a ⎿ subtitle line for tool results."""
22
+ def _render_subtitle_line(
23
+ subtitle: str,
24
+ *,
25
+ error: bool = False,
26
+ warning: bool = False,
27
+ ) -> Text:
28
+ """Render a ⎿ subtitle line for tool results / attached log entries."""
29
+ content_lines = str(subtitle).splitlines() or [""]
30
+ text_style = _subtitle_text_style(error=error, warning=warning)
31
+
24
32
  line = Text()
25
33
  line.append(f" {BOTTOM_LEFT_CROP} ", style="#555555")
34
+ line.append(f" {content_lines[0]}", style=text_style)
35
+ for continuation in content_lines[1:]:
36
+ line.append("\n")
37
+ line.append(" ")
38
+ line.append(continuation, style=text_style)
39
+ return line
40
+
41
+
42
+ def _subtitle_text_style(*, error: bool = False, warning: bool = False) -> str:
26
43
  if error:
27
- line.append(f" {subtitle}", style="bold red")
28
- else:
29
- line.append(f" {subtitle}", style="dim")
44
+ return "bold #FF6B6B"
45
+ if warning:
46
+ return "#E8B830"
47
+ return "dim"
48
+
49
+
50
+ def _render_subtitle_continuation_line(
51
+ subtitle: str,
52
+ *,
53
+ error: bool = False,
54
+ warning: bool = False,
55
+ ) -> Text:
56
+ """Render a consecutive attached subtitle without repeating the ⎿ glyph."""
57
+ content_lines = str(subtitle).splitlines() or [""]
58
+ text_style = _subtitle_text_style(error=error, warning=warning)
59
+
60
+ line = Text()
61
+ for idx, content in enumerate(content_lines):
62
+ if idx > 0:
63
+ line.append("\n")
64
+ line.append(" ")
65
+ line.append(content, style=text_style)
30
66
  return line
31
67
 
32
68
 
@@ -100,6 +136,7 @@ def render_history_group(
100
136
  # 后续行用 2 空格缩进保持视觉对齐。`prev_was_assistant` 把 run 状态跨 drain 传过来。
101
137
  renderables: list[Any] = []
102
138
  needs_leading_gap_after_thinking = prev_was_thinking
139
+ subtitle_chain_active = False
103
140
  for idx, entry in enumerate(entries):
104
141
  is_assistant = entry.entry_type == "assistant"
105
142
  in_run_continuation = is_assistant and prev_was_assistant
@@ -116,6 +153,7 @@ def render_history_group(
116
153
  and next_entry is not None
117
154
  and _starts_with_block_marker(next_entry)
118
155
  ):
156
+ subtitle_chain_active = False
119
157
  continue
120
158
 
121
159
  # Thinking entries: 灰色显示,无前缀。thinking 流式文本里的空白行
@@ -125,6 +163,7 @@ def render_history_group(
125
163
  content_lines = [line for line in content.splitlines() if line.strip()]
126
164
  if not content_lines:
127
165
  prev_was_assistant = False
166
+ subtitle_chain_active = False
128
167
  continue
129
168
  needs_leading_gap_after_thinking = False
130
169
  line_text = Text()
@@ -143,6 +182,7 @@ def render_history_group(
143
182
  if not next_is_thinking and not suppress_trailing_gap:
144
183
  renderables.append(Text(""))
145
184
  prev_was_assistant = False
185
+ subtitle_chain_active = False
146
186
  continue
147
187
 
148
188
  if needs_leading_gap_after_thinking:
@@ -156,6 +196,7 @@ def render_history_group(
156
196
  renderables.append(line_text)
157
197
  renderables.append(Text(""))
158
198
  prev_was_assistant = False
199
+ subtitle_chain_active = False
159
200
  continue
160
201
 
161
202
  # System entries: 按 severity 区分视觉样式,缩进 2 空格与消息内容列对齐
@@ -176,15 +217,28 @@ def render_history_group(
176
217
  renderables.append(line_text)
177
218
  renderables.append(Text(""))
178
219
  prev_was_assistant = False
220
+ subtitle_chain_active = False
179
221
  continue
180
222
 
181
223
  # File reference hint: reuse subtitle style, attached to preceding user message
182
224
  if entry.entry_type == "file_ref":
183
225
  if renderables and isinstance(renderables[-1], Text) and not renderables[-1].plain:
184
226
  renderables.pop()
185
- renderables.append(_render_subtitle_line(str(entry.text)))
227
+ render_subtitle = (
228
+ _render_subtitle_continuation_line
229
+ if subtitle_chain_active
230
+ else _render_subtitle_line
231
+ )
232
+ renderables.append(
233
+ render_subtitle(
234
+ str(entry.text),
235
+ error=(entry.severity == "error"),
236
+ warning=(entry.severity == "warning"),
237
+ )
238
+ )
186
239
  renderables.append(Text(""))
187
240
  prev_was_assistant = False
241
+ subtitle_chain_active = True
188
242
  continue
189
243
 
190
244
  if entry.entry_type == "tool_result":
@@ -206,6 +260,7 @@ def render_history_group(
206
260
  renderables.append(line)
207
261
  renderables.append(Text(""))
208
262
  prev_was_assistant = False
263
+ subtitle_chain_active = bool(entry.subtitle)
209
264
  continue
210
265
 
211
266
  content = str(entry.text)
@@ -224,6 +279,45 @@ def render_history_group(
224
279
  renderables.append(_render_subtitle_line(entry.subtitle, error=is_error))
225
280
  renderables.append(Text(""))
226
281
  prev_was_assistant = False
282
+ subtitle_chain_active = bool(entry.subtitle)
283
+ continue
284
+
285
+ if entry.entry_type == "log":
286
+ if entry.attached:
287
+ if renderables and isinstance(renderables[-1], Text) and not renderables[-1].plain:
288
+ renderables.pop()
289
+ render_subtitle = (
290
+ _render_subtitle_continuation_line
291
+ if subtitle_chain_active
292
+ else _render_subtitle_line
293
+ )
294
+ renderables.append(
295
+ render_subtitle(
296
+ str(entry.text),
297
+ error=(entry.severity == "error"),
298
+ warning=(entry.severity == "warning"),
299
+ )
300
+ )
301
+ subtitle_chain_active = True
302
+ else:
303
+ prefix_char = HEAVY_MULTIPLICATION if entry.severity == "error" else BLACK_CIRCLE
304
+ prefix_style = "bold #FF6B6B" if entry.severity == "error" else "#E8B830"
305
+ content = str(entry.text)
306
+ content_lines = content.splitlines() or [""]
307
+ line_text = Text()
308
+ line_text.append(f"{prefix_char} ", style=prefix_style)
309
+ line_text.append(content_lines[0], style=prefix_style)
310
+ for line in content_lines[1:]:
311
+ line_text.append("\n")
312
+ line_text.append(" ")
313
+ line_text.append(line, style=prefix_style)
314
+ renderables.append(line_text)
315
+ subtitle_chain_active = False
316
+
317
+ next_is_log = next_entry is not None and next_entry.entry_type == "log"
318
+ if not next_is_log:
319
+ renderables.append(Text(""))
320
+ prev_was_assistant = False
227
321
  continue
228
322
 
229
323
  if hasattr(entry.text, "__rich_console__"):
@@ -264,6 +358,7 @@ def render_history_group(
264
358
  if not (is_assistant and next_is_assistant) and not suppress_trailing_gap:
265
359
  renderables.append(Text(""))
266
360
  prev_was_assistant = is_assistant
361
+ subtitle_chain_active = False
267
362
 
268
363
  if not renderables:
269
364
  return None
@@ -1,4 +1,4 @@
1
- """TUI logging adapter - 将日志输出到 prompt_toolkit TUI 而不破坏界面"""
1
+ """TUI logging adapter - warning/error 日志送入 renderer pending 队列。"""
2
2
  from __future__ import annotations
3
3
 
4
4
  import logging
@@ -7,9 +7,7 @@ import sys
7
7
  import threading
8
8
  import time
9
9
  from pathlib import Path
10
- from typing import TYPE_CHECKING
11
-
12
- from prompt_toolkit.application import run_in_terminal
10
+ from typing import TYPE_CHECKING, Literal
13
11
 
14
12
  if TYPE_CHECKING:
15
13
  from comate_cli.terminal_agent.event_renderer import EventRenderer
@@ -52,7 +50,7 @@ class TUILoggingHandler(logging.Handler):
52
50
  特性:
53
51
  - WARNING/ERROR 显示给用户,带颜色标识
54
52
  - 首次显示机制:相同消息只显示一次
55
- - 使用 run_in_terminal() 不破坏 prompt_toolkit 界面
53
+ - 仅入 EventRenderer pending 队列,由明确 boundary 写入 scrollback
56
54
  """
57
55
 
58
56
  def __init__(self, renderer: EventRenderer) -> None:
@@ -81,11 +79,13 @@ class TUILoggingHandler(logging.Handler):
81
79
  self._shown_messages.add(msg_key)
82
80
 
83
81
  # 按日志级别映射 severity,渲染层负责视觉区分
84
- if record.levelno >= logging.ERROR:
85
- severity = "error"
86
- else:
87
- severity = "warning"
88
- self._append_to_tui(msg, severity=severity)
82
+ severity: Literal["warning", "error"] = (
83
+ "error" if record.levelno >= logging.ERROR else "warning"
84
+ )
85
+ self._renderer.enqueue_log(
86
+ severity=severity,
87
+ message=msg,
88
+ )
89
89
 
90
90
  except Exception:
91
91
  self.handleError(record)
@@ -129,29 +129,6 @@ class TUILoggingHandler(logging.Handler):
129
129
  # 其他消息用完整内容作为 key
130
130
  return f"{record.name}:{msg[:50]}"
131
131
 
132
- def _append_to_tui(self, msg: str, *, severity: str = "warning") -> None:
133
- """将消息追加到 TUI(通过 run_in_terminal 避免破坏界面)"""
134
- def _append() -> None:
135
- self._renderer.append_system_message(msg, severity=severity) # type: ignore[arg-type]
136
-
137
- # 使用 run_in_terminal 确保不破坏 prompt_toolkit 的输入
138
- # 如果没有运行中的真实 Application,直接调用(测试或初始化阶段)
139
- try:
140
- from prompt_toolkit.application import get_app
141
- from prompt_toolkit.application.dummy import DummyApplication
142
- app = get_app()
143
- if isinstance(app, DummyApplication):
144
- # DummyApplication,直接调用
145
- _append()
146
- else:
147
- # run_in_terminal() 自身已经会调度 Future;这里不能再交给
148
- # create_background_task() 二次包装,否则会触发重复执行竞态。
149
- run_in_terminal(_append, in_executor=False)
150
- except Exception:
151
- # 没有 app 或导入失败,直接调用
152
- _append()
153
-
154
-
155
132
  class _DropTerminalStreamFilter(logging.Filter):
156
133
  def filter(self, record: logging.LogRecord) -> bool:
157
134
  del record
@@ -348,7 +325,7 @@ def setup_tui_logging(
348
325
  setattr(tui_handler, _COMATE_LOGGING_SESSION_MARKER, True)
349
326
  root.addHandler(tui_handler)
350
327
 
351
- # 3. 临时静默直写终端的 handler,避免污染 prompt_toolkit UI
328
+ # 3. 临时静默直写终端的 handler,避免污染主 TUI 输出
352
329
  muted_handlers: list[tuple[logging.Handler, logging.Filter]] = []
353
330
  for handler in list(root.handlers):
354
331
  if handler in {file_handler, tui_handler}:
@@ -8,7 +8,17 @@ if TYPE_CHECKING:
8
8
  from rich.text import Text
9
9
 
10
10
  ToolStatus = Literal["running", "success", "error"]
11
- HistoryEntryType = Literal["user", "assistant", "tool_call", "tool_result", "system", "thinking", "elapsed", "file_ref"]
11
+ HistoryEntryType = Literal[
12
+ "user",
13
+ "assistant",
14
+ "tool_call",
15
+ "tool_result",
16
+ "system",
17
+ "thinking",
18
+ "elapsed",
19
+ "file_ref",
20
+ "log",
21
+ ]
12
22
 
13
23
 
14
24
  class LoadingStateType(Enum):
@@ -81,6 +91,7 @@ class HistoryEntry:
81
91
  text: str | Text # 支持普通字符串或 Rich Text 对象
82
92
  severity: Literal["info", "warning", "error"] = "info"
83
93
  subtitle: str | None = None
94
+ attached: bool = False
84
95
 
85
96
 
86
97
  @dataclass(frozen=True)
@@ -0,0 +1,21 @@
1
+ """Tips displayed while a turn is running."""
2
+ from __future__ import annotations
3
+
4
+ LOADING_TIPS: tuple[str, ...] = (
5
+ "Use /context when a task starts to feel fuzzy.",
6
+ "Attach @files to make the model read the right surface first.",
7
+ "Use Shift+Tab to switch between act and plan mode.",
8
+ "Use /compact before a long follow-up in the same session.",
9
+ "Press Ctrl+C once to interrupt the current operation.",
10
+ "Use /rewind when the last turn took the wrong branch.",
11
+ "Ask for source-level diagnosis before changing brittle code.",
12
+ "Name the failing boundary before patching behavior.",
13
+ "Prefer one visible behavior change per small patch.",
14
+ "Keep queue messages invisible until they are consumed.",
15
+ "Check scrollback output separately from live TUI state.",
16
+ "For TUI bugs, separate layout state from history output.",
17
+ "Use @path hints when the answer depends on local files.",
18
+ "Review tests around the boundary you are about to touch.",
19
+ "Use /model when the task needs a different reasoning tier.",
20
+ "Small focused prompts usually beat broad fuzzy requests.",
21
+ )
@@ -71,6 +71,7 @@ from comate_cli.terminal_agent.slash_commands import (
71
71
  SlashCommandSpec,
72
72
  )
73
73
  from comate_cli.terminal_agent.status_bar import StatusBar
74
+ from comate_cli.terminal_agent.tips import LOADING_TIPS
74
75
  from comate_cli.terminal_agent.tui_parts import (
75
76
  CommandsMixin,
76
77
  HistorySyncMixin,
@@ -83,6 +84,8 @@ from comate_cli.terminal_agent.tui_parts import (
83
84
 
84
85
  logger = logging.getLogger(__name__)
85
86
 
87
+ _LOADING_TIP_DELAY_SECONDS = 2.5
88
+
86
89
  _TASK_POLL_INTERVAL_S = 2.0
87
90
  _INPUT_HISTORY_DIR = Path.home() / ".agent" / "history"
88
91
  _INPUT_HISTORY_MAX_ENTRIES = 200
@@ -250,6 +253,8 @@ class TerminalAgentTUI(
250
253
  else f"Flibbertigibbeting{ELLIPSIS}"
251
254
  )
252
255
  self._fallback_phrase_refresh_at = 0.0
256
+ self._loading_tip_text = ""
257
+ self._loading_tip_show_at_monotonic: float | None = None
253
258
 
254
259
  self._tool_result_flash_seconds = read_env_float(
255
260
  "AGENT_SDK_TUI_TOOL_RESULT_FLASH_SECONDS",
@@ -876,7 +881,16 @@ class TerminalAgentTUI(
876
881
  )
877
882
 
878
883
  def _set_busy(self, value: bool) -> None:
884
+ was_busy = self._busy
879
885
  self._busy = value
886
+ if value and not was_busy:
887
+ self._loading_tip_text = random.choice(LOADING_TIPS) if LOADING_TIPS else ""
888
+ self._loading_tip_show_at_monotonic = (
889
+ time.monotonic() + _LOADING_TIP_DELAY_SECONDS
890
+ )
891
+ elif not value:
892
+ self._loading_tip_text = ""
893
+ self._loading_tip_show_at_monotonic = None
880
894
  self._render_dirty = True
881
895
  self._invalidate()
882
896
 
@@ -1447,13 +1461,13 @@ class TerminalAgentTUI(
1447
1461
  if not self._compact_cancel_requested:
1448
1462
  self._compact_cancel_requested = True
1449
1463
  self._renderer.append_system_message(
1450
- "正在取消 /compact,请等待回滚完成后退出。",
1464
+ "Cancelling /compact. The app will exit after rollback finishes.",
1451
1465
  severity="warning",
1452
1466
  )
1453
1467
  self._compact_task.cancel()
1454
1468
  else:
1455
1469
  self._renderer.append_system_message(
1456
- "已请求取消 /compact,等待回滚完成。",
1470
+ "/compact cancellation already requested. Waiting for rollback to finish.",
1457
1471
  severity="warning",
1458
1472
  )
1459
1473
  self._refresh_layers()