comate-cli 0.2.3__tar.gz → 0.2.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 (97) hide show
  1. {comate_cli-0.2.3 → comate_cli-0.2.5}/.gitignore +2 -1
  2. {comate_cli-0.2.3 → comate_cli-0.2.5}/PKG-INFO +1 -1
  3. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/app.py +1 -1
  4. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/event_renderer.py +136 -31
  5. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tool_view.py +42 -0
  6. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui.py +81 -1
  7. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/commands.py +1 -1
  8. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/history_sync.py +67 -0
  9. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/render_panels.py +37 -8
  10. {comate_cli-0.2.3 → comate_cli-0.2.5}/pyproject.toml +1 -1
  11. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_completion_status_panel.py +1 -1
  12. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_event_renderer.py +132 -6
  13. comate_cli-0.2.5/tests/test_task_panel_format.py +96 -0
  14. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_task_panel_key_bindings.py +8 -8
  15. comate_cli-0.2.5/tests/test_task_panel_rendering.py +116 -0
  16. comate_cli-0.2.5/tests/test_task_poll.py +64 -0
  17. comate_cli-0.2.5/tests/test_tool_view.py +149 -0
  18. {comate_cli-0.2.3 → comate_cli-0.2.5}/uv.lock +2 -2
  19. comate_cli-0.2.3/tests/test_tool_view.py +0 -64
  20. {comate_cli-0.2.3 → comate_cli-0.2.5}/README.md +0 -0
  21. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/__init__.py +0 -0
  22. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/__main__.py +0 -0
  23. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/main.py +0 -0
  24. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/mcp_cli.py +0 -0
  25. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/__init__.py +0 -0
  26. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/animations.py +0 -0
  27. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/assistant_render.py +0 -0
  28. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  29. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/env_utils.py +0 -0
  30. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/error_display.py +0 -0
  31. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  32. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/history_printer.py +0 -0
  33. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/input_geometry.py +0 -0
  34. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  35. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/logging_adapter.py +0 -0
  36. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/logo.py +0 -0
  37. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/markdown_render.py +0 -0
  38. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/mention_completer.py +0 -0
  39. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/message_style.py +0 -0
  40. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/models.py +0 -0
  41. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/preflight.py +0 -0
  42. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/question_view.py +0 -0
  43. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/resume_selector.py +0 -0
  44. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/rewind_store.py +0 -0
  45. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  46. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  47. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/selection_menu.py +0 -0
  48. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/session_view.py +0 -0
  49. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/slash_commands.py +0 -0
  50. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/startup.py +0 -0
  51. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/status_bar.py +0 -0
  52. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/text_effects.py +0 -0
  53. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tips.py +0 -0
  54. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  55. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
  56. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
  57. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  58. {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  59. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/conftest.py +0 -0
  60. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_app_mcp_preload.py +0 -0
  61. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_app_preflight_gate.py +0 -0
  62. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_app_shutdown.py +0 -0
  63. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_app_usage_line.py +0 -0
  64. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_cli_project_root.py +0 -0
  65. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_compact_command_semantics.py +0 -0
  66. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_completion_context_activation.py +0 -0
  67. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_context_command.py +0 -0
  68. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_custom_slash_commands.py +0 -0
  69. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_history_sync.py +0 -0
  70. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_input_behavior.py +0 -0
  71. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_interrupt_exit_semantics.py +0 -0
  72. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_layout_coordinator.py +0 -0
  73. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_logging_adapter.py +0 -0
  74. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_logo.py +0 -0
  75. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_main_args.py +0 -0
  76. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_mcp_cli.py +0 -0
  77. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_mcp_slash_command.py +0 -0
  78. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_mention_completer.py +0 -0
  79. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_preflight.py +0 -0
  80. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_preflight_copilot.py +0 -0
  81. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_question_key_bindings.py +0 -0
  82. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_question_view.py +0 -0
  83. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_resume_selector.py +0 -0
  84. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_rewind_command_semantics.py +0 -0
  85. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_rewind_store.py +0 -0
  86. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_rpc_protocol.py +0 -0
  87. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_rpc_stdio_bridge.py +0 -0
  88. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_selection_menu.py +0 -0
  89. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_skills_slash_command.py +0 -0
  90. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_slash_argument_hint.py +0 -0
  91. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_slash_completer.py +0 -0
  92. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_slash_registry.py +0 -0
  93. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_status_bar.py +0 -0
  94. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_tui_elapsed_status.py +0 -0
  95. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_tui_mcp_init_gate.py +0 -0
  96. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_tui_paste_placeholder.py +0 -0
  97. {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_tui_split_invariance.py +0 -0
@@ -5,6 +5,7 @@ __pycache__/
5
5
  .agent
6
6
  .agents
7
7
  set_env.sh
8
+ .worktrees/
8
9
  # C extensions
9
10
  *.so
10
11
 
@@ -222,4 +223,4 @@ __marimo__/
222
223
  ./tmp/
223
224
 
224
225
  # OS generated files
225
- .DS_Store
226
+ .DS_Store.worktrees/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comate-cli
3
- Version: 0.2.3
3
+ Version: 0.2.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
@@ -230,7 +230,7 @@ def _format_resume_hint(session_id: str | None) -> str | None:
230
230
  async def _preload_mcp_in_tui(session: ChatSession) -> None:
231
231
  """在 TUI 内异步加载 MCP,初始化阶段不输出 scrollback 文案。"""
232
232
  runtime = session.runtime
233
- if not bool(runtime.options.mcp_enabled):
233
+ if not bool(runtime.config.mcp_enabled):
234
234
  return
235
235
 
236
236
  try:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  import time
5
+ from collections import deque
5
6
  from dataclasses import dataclass, field
6
7
  from pathlib import Path
7
8
  from typing import Any, Literal
@@ -17,6 +18,7 @@ from comate_agent_sdk.agent.events import (
17
18
  SubagentStopEvent,
18
19
  SubagentToolCallEvent,
19
20
  SubagentToolResultEvent,
21
+ TeamMessageEvent,
20
22
  TextEvent,
21
23
  ThinkingEvent,
22
24
  TaskUpdatedEvent,
@@ -30,7 +32,7 @@ from rich.console import RenderableType
30
32
  from rich.text import Text
31
33
 
32
34
  from comate_cli.terminal_agent.models import HistoryEntry, LoadingState
33
- from comate_cli.terminal_agent.tool_view import summarize_tool_args, resolve_display_tool_name
35
+ from comate_cli.terminal_agent.tool_view import summarize_tool_args, resolve_display_tool_name, should_show_tool_in_scrollback
34
36
  from comate_cli.terminal_agent.env_utils import read_env_int
35
37
 
36
38
  logger = logging.getLogger(__name__)
@@ -38,6 +40,7 @@ logger = logging.getLogger(__name__)
38
40
  _DEFAULT_TOOL_ERROR_SUMMARY_MAX_LEN = 160
39
41
  _DEFAULT_TOOL_PANEL_MAX_LINES = 4
40
42
  _DEFAULT_TASK_PANEL_MAX_LINES = 6
43
+ _RECENT_TEAM_EVENT_CACHE_SIZE = 128
41
44
 
42
45
 
43
46
  def _truncate(content: str, max_len: int = 120) -> str:
@@ -108,23 +111,25 @@ def _task_sort_key(task: dict[str, Any]) -> tuple[int, int]:
108
111
 
109
112
 
110
113
  def _format_task_row(task: dict[str, Any]) -> str:
111
- task_id = str(task.get("id", "?")).strip() or "?"
114
+ """格式化单条 task 行。
115
+
116
+ 符号约定:
117
+ - ✓ completed
118
+ - ◼ in_progress
119
+ - ▢ pending (unblocked)
120
+ - ▫ pending (blocked) — 视觉相似的中间符号,渲染层统一显示为 ▢ 但应用不同颜色
121
+ """
112
122
  subject = str(task.get("subject", "")).strip() or "(untitled)"
113
- owner = str(task.get("owner", "")).strip()
114
123
  status = str(task.get("status", "pending")).strip().lower()
115
124
  open_blocked_by = task.get("open_blocked_by", [])
116
- owner_suffix = f" owner={owner}" if owner else ""
117
- blocked_suffix = ""
118
- if isinstance(open_blocked_by, list) and open_blocked_by:
119
- blocked_suffix = f" blocked by #{','.join(str(item) for item in open_blocked_by)}"
120
125
 
121
126
  if status == "completed":
122
- return f" [#{task_id}] {subject}{owner_suffix}"
127
+ return f"✓ {subject}"
123
128
  if status == "in_progress":
124
- return f" [#{task_id}] {subject}{owner_suffix}"
125
- if blocked_suffix:
126
- return f" [#{task_id}] {subject}{owner_suffix}{blocked_suffix}"
127
- return f" [#{task_id}] {subject}{owner_suffix}"
129
+ return f" {subject}"
130
+ if isinstance(open_blocked_by, list) and open_blocked_by:
131
+ return f" {subject}"
132
+ return f" {subject}"
128
133
 
129
134
 
130
135
  @dataclass
@@ -159,6 +164,7 @@ class EventRenderer:
159
164
  def __init__(self, project_root: Path | None = None) -> None:
160
165
  self._history: list[HistoryEntry] = []
161
166
  self._running_tools: dict[str, _RunningTool] = {}
167
+ self._tool_call_args: dict[str, dict[str, Any]] = {}
162
168
  self._thinking_content: str = ""
163
169
  self._assistant_buffer = ""
164
170
  self._loading_state: LoadingState = LoadingState.idle()
@@ -183,6 +189,9 @@ class EventRenderer:
183
189
  50,
184
190
  )
185
191
  self._latest_diff_lines: list[str] | None = None
192
+ self._recent_team_event_keys: deque[tuple[str, str, str, str, str, str]] = deque(
193
+ maxlen=_RECENT_TEAM_EVENT_CACHE_SIZE
194
+ )
186
195
 
187
196
  def start_turn(self) -> None:
188
197
  self._flush_assistant_segment()
@@ -229,7 +238,9 @@ class EventRenderer:
229
238
  def reset_history_view(self) -> None:
230
239
  """重置 history 视图状态(用于会话切换后的重新加载)。"""
231
240
  self._history = []
241
+ self._recent_team_event_keys.clear()
232
242
  self._running_tools.clear()
243
+ self._tool_call_args.clear()
233
244
  self._thinking_content = ""
234
245
  self._assistant_buffer = ""
235
246
  self._loading_state = LoadingState.idle()
@@ -294,6 +305,74 @@ class EventRenderer:
294
305
  HistoryEntry(entry_type="system", text=normalized, severity=severity)
295
306
  )
296
307
 
308
+ @staticmethod
309
+ def _team_event_key(
310
+ *,
311
+ agent_name: str,
312
+ from_agent: str,
313
+ to_agent: str | None,
314
+ message_type: str,
315
+ timestamp: str,
316
+ content_preview: str,
317
+ ) -> tuple[str, str, str, str, str, str]:
318
+ return (
319
+ str(agent_name or "").strip(),
320
+ str(from_agent or "").strip(),
321
+ str(to_agent or "").strip(),
322
+ str(message_type or "").strip(),
323
+ str(timestamp or "").strip(),
324
+ str(content_preview or "").strip(),
325
+ )
326
+
327
+ def append_team_message_event(
328
+ self,
329
+ *,
330
+ agent_name: str,
331
+ from_agent: str,
332
+ to_agent: str | None,
333
+ message_type: str,
334
+ content_preview: str,
335
+ timestamp: str,
336
+ ) -> bool:
337
+ event_key = self._team_event_key(
338
+ agent_name=agent_name,
339
+ from_agent=from_agent,
340
+ to_agent=to_agent,
341
+ message_type=message_type,
342
+ timestamp=timestamp,
343
+ content_preview=content_preview,
344
+ )
345
+ if event_key in self._recent_team_event_keys:
346
+ logger.debug(
347
+ "[team-diag] renderer_dedupe "
348
+ "agent=%r from=%r to=%r type=%r timestamp=%r preview=%r",
349
+ agent_name,
350
+ from_agent,
351
+ to_agent,
352
+ message_type,
353
+ timestamp,
354
+ content_preview,
355
+ )
356
+ return False
357
+
358
+ self._recent_team_event_keys.append(event_key)
359
+ target = to_agent or "[broadcast]"
360
+ preview_str = f' "{content_preview}"' if content_preview else ""
361
+ text = f"[team] {from_agent} → {target}: ({message_type}){preview_str}"
362
+ logger.debug(
363
+ "[team-diag] renderer_append "
364
+ "agent=%r from=%r to=%r type=%r timestamp=%r preview=%r history_len_before=%d",
365
+ agent_name,
366
+ from_agent,
367
+ to_agent,
368
+ message_type,
369
+ timestamp,
370
+ content_preview,
371
+ len(self._history),
372
+ )
373
+ self.append_system_message(text)
374
+ return True
375
+
297
376
  def append_elapsed_message(self, content: str) -> None:
298
377
  """追加一条灰色无前缀的计时统计行到 history scrollback."""
299
378
  normalized = content.strip()
@@ -389,7 +468,7 @@ class EventRenderer:
389
468
  return lines
390
469
 
391
470
  clipped = lines[: normalized_limit - 1]
392
- clipped.append(f" … (+{len(lines) - (normalized_limit - 1)})")
471
+ clipped.append(f"… (+{len(lines) - (normalized_limit - 1)})")
393
472
  return clipped
394
473
 
395
474
  def full_task_panel_lines(self) -> list[str]:
@@ -655,7 +734,16 @@ class EventRenderer:
655
734
  return
656
735
 
657
736
  all_completed = all(str(item.get("status", "")).strip().lower() == "completed" for item in normalized)
658
- header = "Tasks" if list_id == "default" else f"Tasks · {list_id}"
737
+ # 标题:显示 ID 最小的 in_progress task subject,否则 "Tasks"
738
+ in_progress_tasks = [
739
+ t for t in normalized
740
+ if str(t.get("status", "")).strip().lower() == "in_progress"
741
+ ]
742
+ if in_progress_tasks:
743
+ in_progress_tasks.sort(key=lambda t: int(t.get("id", 0)))
744
+ header = str(in_progress_tasks[0].get("subject", "")).strip() or "Tasks"
745
+ else:
746
+ header = "Tasks"
659
747
  if not all_completed:
660
748
  if not self._current_tasks:
661
749
  self._task_started_at_monotonic = time.monotonic()
@@ -701,17 +789,21 @@ class EventRenderer:
701
789
 
702
790
  for task in tasks:
703
791
  line = _format_task_row(task)
704
- if line.startswith(" ✓ "):
792
+ if line.startswith("✓ "):
705
793
  result.append(" ✓ ")
706
- result.append(line[4:], style="green strike")
707
- elif line.startswith(" "):
708
- result.append(" ")
709
- result.append(line[4:], style="yellow")
710
- elif line.startswith(" "):
711
- result.append(" ◌ ")
712
- result.append(line[4:], style="dim")
794
+ result.append(line[2:], style="green strike")
795
+ elif line.startswith(" "):
796
+ result.append(" ")
797
+ result.append(line[2:], style="#F97316")
798
+ elif line.startswith(" "):
799
+ # blocked pending: display as ▢ + dim
800
+ result.append(" ▢ ")
801
+ result.append(line[2:], style="dim")
802
+ elif line.startswith("▢ "):
803
+ result.append(" ▢ ")
804
+ result.append(line[2:])
713
805
  else:
714
- result.append(line)
806
+ result.append(f" {line}")
715
807
  result.append("\n")
716
808
 
717
809
  # 移除末尾的换行
@@ -786,18 +878,15 @@ class EventRenderer:
786
878
  case ToolCallEvent(tool=tool_name, args=arguments, tool_call_id=tool_call_id):
787
879
  args_dict = arguments if isinstance(arguments, dict) else {"_raw": str(arguments)}
788
880
  self._thinking_content = ""
789
- # Task* / AskUserQuestion / ExitPlanMode: skip normal tool tracking.
790
- if tool_name.lower() in ("taskcreate", "tasklist", "taskget", "taskupdate", "askuserquestion", "exitplanmode"):
881
+ # Store args for ToolResult phase lookup.
882
+ self._tool_call_args[tool_call_id] = args_dict
883
+ if not should_show_tool_in_scrollback(tool_name, args_dict):
791
884
  self._rebuild_loading_line()
792
885
  return (False, None)
793
886
  self._append_tool_call(tool_name, args_dict, tool_call_id)
794
887
  case ToolResultEvent(tool=tool_name, result=result, tool_call_id=tool_call_id, is_error=is_error, metadata=metadata):
795
- if tool_name.lower() in ("taskcreate", "tasklist", "taskupdate") and not is_error:
796
- # Successful task coordination updates should not spam scrollback.
797
- self._rebuild_loading_line()
798
- return (False, None)
799
- if tool_name.lower() in ("askuserquestion", "exitplanmode") and not is_error:
800
- # Success is already visible via UserQuestionEvent / PlanApprovalRequiredEvent.
888
+ stored_args = self._tool_call_args.pop(tool_call_id, {})
889
+ if not should_show_tool_in_scrollback(tool_name, stored_args, is_result=True, is_error=is_error):
801
890
  self._running_tools.pop(tool_call_id, None)
802
891
  self._rebuild_loading_line()
803
892
  return (False, None)
@@ -941,6 +1030,22 @@ class EventRenderer:
941
1030
  self._history.append(
942
1031
  HistoryEntry(entry_type="system", text=text)
943
1032
  )
1033
+ case TeamMessageEvent(
1034
+ agent_name=agent_name,
1035
+ from_agent=from_agent,
1036
+ to_agent=to_agent,
1037
+ message_type=message_type,
1038
+ content_preview=content_preview,
1039
+ timestamp=timestamp,
1040
+ ):
1041
+ self.append_team_message_event(
1042
+ agent_name=agent_name,
1043
+ from_agent=from_agent,
1044
+ to_agent=to_agent,
1045
+ message_type=message_type,
1046
+ content_preview=content_preview,
1047
+ timestamp=timestamp,
1048
+ )
944
1049
  case _:
945
1050
  logger.debug("Unhandled event type: %s", type(event).__name__)
946
1051
 
@@ -60,6 +60,48 @@ _MAX_FANCY_LINE_LEN = 100
60
60
  _ALLOWED_STATUS = {"pending", "in_progress", "completed"}
61
61
  _ALLOWED_PRIORITY = {"high", "medium", "low"}
62
62
 
63
+ # Tools that are always hidden from scrollback (have dedicated event channels).
64
+ _ALWAYS_HIDDEN_TOOLS: frozenset[str] = frozenset({"askuserquestion", "exitplanmode"})
65
+
66
+ # Task coordination tools hidden from scrollback unless erroring.
67
+ _SILENT_TASK_TOOLS: frozenset[str] = frozenset({"tasklist", "taskget"})
68
+
69
+
70
+ def should_show_tool_in_scrollback(
71
+ tool_name: str,
72
+ args: dict[str, Any],
73
+ is_result: bool = False,
74
+ is_error: bool = False,
75
+ ) -> bool:
76
+ """Determine whether a tool call/result should appear in scrollback.
77
+
78
+ Centralises the visibility rules so that both the live event pipeline
79
+ (event_renderer) and the resume replay pipeline (history_sync) share
80
+ one source of truth.
81
+ """
82
+ lowered = tool_name.lower()
83
+
84
+ # AskUserQuestion / ExitPlanMode: only show errors.
85
+ if lowered in _ALWAYS_HIDDEN_TOOLS:
86
+ return is_result and is_error
87
+
88
+ # TaskCreate: always visible.
89
+ if lowered == "taskcreate":
90
+ return True
91
+
92
+ # TaskUpdate: visible only when status=completed, or on error.
93
+ if lowered == "taskupdate":
94
+ if str(args.get("status", "")).lower() == "completed":
95
+ return True
96
+ return is_result and is_error
97
+
98
+ # TaskList / TaskGet: only show errors.
99
+ if lowered in _SILENT_TASK_TOOLS:
100
+ return is_result and is_error
101
+
102
+ # All other tools: always visible.
103
+ return True
104
+
63
105
 
64
106
  def _parse_todos_value(value: Any) -> list[dict[str, Any]] | None:
65
107
  if isinstance(value, list):
@@ -15,7 +15,7 @@ from contextlib import suppress
15
15
  from pathlib import Path
16
16
  from typing import Any
17
17
 
18
- from prompt_toolkit.application import Application
18
+ from prompt_toolkit.application import Application, run_in_terminal
19
19
  from prompt_toolkit.completion import (
20
20
  ThreadedCompleter,
21
21
  merge_completers,
@@ -36,6 +36,7 @@ from comate_agent_sdk.agent.events import (
36
36
  PlanApprovalRequiredEvent,
37
37
  SessionInitEvent,
38
38
  StopEvent,
39
+ TeamMessageEvent,
39
40
  )
40
41
 
41
42
  from comate_cli.terminal_agent.animations import (
@@ -74,6 +75,8 @@ from comate_cli.terminal_agent.tui_parts import (
74
75
 
75
76
  logger = logging.getLogger(__name__)
76
77
 
78
+ _TASK_POLL_INTERVAL_S = 2.0
79
+
77
80
 
78
81
  class TerminalAgentTUI(
79
82
  KeyBindingsMixin,
@@ -95,8 +98,46 @@ class TerminalAgentTUI(
95
98
  except Exception:
96
99
  pass
97
100
  self._renderer = renderer
101
+ self._task_poll_next_at = time.monotonic() + _TASK_POLL_INTERVAL_S
98
102
  self._rewind_store = RewindStore(session=self._session, project_root=Path.cwd())
99
103
 
104
+ # Team inbox 消息直连 scrollback(绕开 session_event_queue,实现实时显示)
105
+ _renderer = self._renderer
106
+ def _on_team_message(event: TeamMessageEvent) -> None:
107
+ logger.debug(
108
+ "[team-diag] tui_append "
109
+ "session_id=%r recv_agent=%r from=%r to=%r type=%r timestamp=%r "
110
+ "preview=%r history_len_before=%d",
111
+ self._session.session_id,
112
+ event.agent_name,
113
+ event.from_agent,
114
+ event.to_agent,
115
+ event.message_type,
116
+ event.timestamp,
117
+ event.content_preview,
118
+ len(self._renderer.history_entries()),
119
+ )
120
+ def _write() -> None:
121
+ _renderer.append_team_message_event(
122
+ agent_name=str(event.agent_name or ""),
123
+ from_agent=str(event.from_agent or ""),
124
+ to_agent=event.to_agent,
125
+ message_type=str(event.message_type or ""),
126
+ content_preview=str(event.content_preview or ""),
127
+ timestamp=str(event.timestamp or ""),
128
+ )
129
+ try:
130
+ from prompt_toolkit.application import get_app
131
+ from prompt_toolkit.application.dummy import DummyApplication
132
+ app = get_app()
133
+ if not isinstance(app, DummyApplication):
134
+ app.create_background_task(run_in_terminal(_write, in_executor=False))
135
+ return
136
+ except Exception:
137
+ pass
138
+ _write()
139
+ self._session._agent._team.team_message_event_callback = _on_team_message
140
+
100
141
  self._tool_panel_max_lines = read_env_int(
101
142
  "AGENT_SDK_TUI_TOOL_PANEL_MAX_LINES",
102
143
  4,
@@ -999,6 +1040,34 @@ class TerminalAgentTUI(
999
1040
  self._sync_focus_for_mode()
1000
1041
  self._render_dirty = True
1001
1042
 
1043
+ def _fetch_tasks_from_store(self) -> tuple[list[dict], str] | None:
1044
+ """从 TaskStore 读取最新 task 列表(纯 I/O,线程安全)。
1045
+
1046
+ 返回 (task_dicts, list_id) 或 None。
1047
+ 不修改任何共享状态——状态更新由调用方在主线程完成。
1048
+ """
1049
+ try:
1050
+ agent = getattr(self._session, "_agent", None)
1051
+ if agent is None:
1052
+ return None
1053
+ store = getattr(agent, "_task_store", None)
1054
+ if store is None:
1055
+ return None
1056
+ tasks = store.list_tasks()
1057
+ if not tasks:
1058
+ return None
1059
+ # 转换 TaskItem 为 dict(与 chat_session.py 格式一致)
1060
+ task_dicts: list[dict] = []
1061
+ for task in tasks:
1062
+ payload = task.model_dump(mode="json")
1063
+ payload["open_blocked_by"] = store.get_open_blocked_by(task.id)
1064
+ task_dicts.append(payload)
1065
+ list_id = store.list_id_value()
1066
+ return task_dicts, list_id
1067
+ except Exception:
1068
+ logger.debug("task store fetch failed", exc_info=True)
1069
+ return None
1070
+
1002
1071
  async def _ui_tick(self) -> None:
1003
1072
  try:
1004
1073
  while not self._closing:
@@ -1053,6 +1122,17 @@ class TerminalAgentTUI(
1053
1122
  or self._renderer.has_running_tools()
1054
1123
  )
1055
1124
  sleep_s = 1 / 6 if fast else 1 / 4
1125
+
1126
+ # 定时轮询 TaskStore 刷新 task 面板
1127
+ now_poll = time.monotonic()
1128
+ if now_poll >= self._task_poll_next_at:
1129
+ self._task_poll_next_at = now_poll + _TASK_POLL_INTERVAL_S
1130
+ poll_result = await asyncio.to_thread(self._fetch_tasks_from_store)
1131
+ if poll_result is not None:
1132
+ task_dicts, list_id = poll_result
1133
+ self._renderer._update_tasks(task_dicts, list_id=list_id)
1134
+ self._render_dirty = True
1135
+
1056
1136
  await asyncio.sleep(sleep_s)
1057
1137
  except asyncio.CancelledError:
1058
1138
  return
@@ -875,7 +875,7 @@ class CommandsMixin:
875
875
  agent_level = self._session._agent.level
876
876
  if agent_level:
877
877
  current_level = agent_level
878
- llm_levels = self._session._agent.options.llm_levels
878
+ llm_levels = self._session._agent._runtime_state.llm_levels
879
879
  except Exception:
880
880
  pass
881
881
 
@@ -18,6 +18,7 @@ from comate_cli.terminal_agent.history_printer import (
18
18
  from comate_cli.terminal_agent.logo import print_logo
19
19
  from comate_cli.terminal_agent.markdown_render import render_markdown_to_plain
20
20
  from comate_cli.terminal_agent.models import HistoryEntry
21
+ from comate_cli.terminal_agent.tool_view import should_show_tool_in_scrollback
21
22
 
22
23
  console = Console()
23
24
  logger = logging.getLogger(__name__)
@@ -76,6 +77,7 @@ class HistorySyncMixin:
76
77
 
77
78
  # 维护 tool_call_id 映射,用于匹配工具调用和结果
78
79
  tool_call_info: dict[str, tuple[str, str]] = {} # tool_call_id → (tool_name, signature)
80
+ tool_call_args: dict[str, dict[str, Any]] = {}
79
81
 
80
82
  for item in history:
81
83
  if item.item_type == ItemType.USER_MESSAGE:
@@ -103,6 +105,7 @@ class HistorySyncMixin:
103
105
  signature = self._build_resume_tool_signature(tool_name, args_dict)
104
106
  # 存储映射,不调用 restore_tool_call(避免加入 _running_tools)
105
107
  tool_call_info[tc.id] = (tool_name, signature)
108
+ tool_call_args[tc.id] = args_dict
106
109
 
107
110
  # 显示 assistant text
108
111
  assistant_text = self._extract_assistant_text(item).strip()
@@ -127,6 +130,11 @@ class HistorySyncMixin:
127
130
  tool_name = getattr(message, "tool_name", item.tool_name or "UnknownTool")
128
131
  signature = f"{tool_name}()"
129
132
 
133
+ # Apply shared visibility filter.
134
+ resume_args = tool_call_args.get(tool_call_id, {}) if tool_call_id else {}
135
+ if not should_show_tool_in_scrollback(tool_name, resume_args, is_result=True, is_error=is_error):
136
+ continue
137
+
130
138
  # Extract diff from raw_envelope for Edit/MultiEdit
131
139
  diff_lines: list[str] | None = None
132
140
  if tool_name in ("Edit", "MultiEdit") and not is_error:
@@ -220,6 +228,25 @@ class HistorySyncMixin:
220
228
  pending = self._pending_history_entries()
221
229
  if not pending:
222
230
  return
231
+ team_pending = [
232
+ entry
233
+ for entry in pending
234
+ if entry.entry_type == "system" and isinstance(entry.text, str) and entry.text.startswith("[team] ")
235
+ ]
236
+ if team_pending:
237
+ logger.debug(
238
+ "[team-diag] history_async_drain "
239
+ "session_id=%r pending_count=%d team_pending_count=%d printed_index=%d",
240
+ getattr(self._session, "session_id", ""),
241
+ len(pending),
242
+ len(team_pending),
243
+ self._printed_history_index,
244
+ )
245
+ for entry in team_pending:
246
+ logger.debug(
247
+ "[team-diag] history_async_entry text=%r",
248
+ entry.text,
249
+ )
223
250
  group = render_history_group(
224
251
  console,
225
252
  pending,
@@ -245,6 +272,25 @@ class HistorySyncMixin:
245
272
  pending = self._pending_history_entries()
246
273
  if not pending:
247
274
  return
275
+ team_pending = [
276
+ entry
277
+ for entry in pending
278
+ if entry.entry_type == "system" and isinstance(entry.text, str) and entry.text.startswith("[team] ")
279
+ ]
280
+ if team_pending:
281
+ logger.debug(
282
+ "[team-diag] history_sync_drain "
283
+ "session_id=%r pending_count=%d team_pending_count=%d printed_index=%d",
284
+ getattr(self._session, "session_id", ""),
285
+ len(pending),
286
+ len(team_pending),
287
+ self._printed_history_index,
288
+ )
289
+ for entry in team_pending:
290
+ logger.debug(
291
+ "[team-diag] history_sync_entry text=%r",
292
+ entry.text,
293
+ )
248
294
  group = render_history_group(
249
295
  console,
250
296
  pending,
@@ -259,9 +305,30 @@ class HistorySyncMixin:
259
305
  entries = self._renderer.history_entries()
260
306
  if self._printed_history_index >= len(entries):
261
307
  return []
308
+ previous_index = self._printed_history_index
262
309
  pending = entries[self._printed_history_index :]
263
310
  self._printed_history_index = len(entries)
264
311
  # 根据 _show_thinking 开关过滤 thinking 条目
265
312
  if not getattr(self, "_show_thinking", False):
266
313
  pending = [e for e in pending if e.entry_type != "thinking"]
314
+ team_pending = [
315
+ entry
316
+ for entry in pending
317
+ if entry.entry_type == "system" and isinstance(entry.text, str) and entry.text.startswith("[team] ")
318
+ ]
319
+ if team_pending:
320
+ logger.debug(
321
+ "[team-diag] history_pending "
322
+ "session_id=%r previous_index=%d new_index=%d total_entries=%d team_pending_count=%d",
323
+ getattr(self._session, "session_id", ""),
324
+ previous_index,
325
+ self._printed_history_index,
326
+ len(entries),
327
+ len(team_pending),
328
+ )
329
+ for entry in team_pending:
330
+ logger.debug(
331
+ "[team-diag] history_pending_entry text=%r",
332
+ entry.text,
333
+ )
267
334
  return pending
@@ -239,7 +239,7 @@ class RenderPanelsMixin:
239
239
  )
240
240
  if len(all_lines) > len(collapsed_lines) and collapsed_lines:
241
241
  last_line = collapsed_lines[-1]
242
- if last_line.startswith(" … (+"):
242
+ if last_line.startswith("… (+"):
243
243
  collapsed_lines[-1] = f"{last_line}, Ctrl+Y to expand"
244
244
  return collapsed_lines
245
245
 
@@ -558,13 +558,42 @@ class RenderPanelsMixin:
558
558
  if title_elapsed_suffix:
559
559
  fragments.append(("fg:#6B7280", title_elapsed_suffix))
560
560
  else:
561
- clipped = fit_single_line(line, width - 1)
562
- style = "fg:#CBD5E1"
563
- if "✓" in line:
564
- style = "fg:#86EFAC"
565
- elif "" in line:
566
- style = "fg:#FDE68A"
567
- fragments.append((style, clipped))
561
+ clipped = fit_single_line(line, width - 3) # 留空间给 prefix + space
562
+ # prefix 列:第一条任务行用 ⎿,后续用空格
563
+ is_first_task_row = idx == 1
564
+ prefix_char = "" if is_first_task_row else " "
565
+ prefix_style = "fg:#555555"
566
+
567
+ # 解析符号和文本,应用分色
568
+ if clipped.startswith("✓ "):
569
+ symbol, text = "✓", clipped[2:]
570
+ symbol_style = "fg:#86EFAC"
571
+ text_style = "fg:#6B7280 strike"
572
+ elif clipped.startswith("◼ "):
573
+ symbol, text = "◼", clipped[2:]
574
+ symbol_style = "fg:#F97316"
575
+ text_style = "fg:#E2E8F0"
576
+ elif clipped.startswith("▫ "):
577
+ # blocked pending: ▫ 是中间符号,渲染时显示为 ▢
578
+ symbol, text = "▢", clipped[2:]
579
+ symbol_style = "fg:#4B5563"
580
+ text_style = "fg:#4B5563"
581
+ elif clipped.startswith("▢ "):
582
+ symbol, text = "▢", clipped[2:]
583
+ symbol_style = "fg:#6B7280"
584
+ text_style = "fg:#6B7280"
585
+ else:
586
+ # 溢出行 "… (+N)" 等
587
+ symbol, text = "", clipped
588
+ symbol_style = ""
589
+ text_style = "fg:#6B7280"
590
+
591
+ fragments.append((prefix_style, prefix_char))
592
+ fragments.append(("", " "))
593
+ if symbol:
594
+ fragments.append((symbol_style, symbol))
595
+ fragments.append(("", " "))
596
+ fragments.append((text_style, text))
568
597
  if idx != last_index:
569
598
  fragments.append(("", "\n"))
570
599
  return fragments
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "comate-cli"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  description = "Comate terminal CLI built on comate-agent-sdk"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"