comate-cli 0.2.4__tar.gz → 0.2.6__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 (98) hide show
  1. {comate_cli-0.2.4 → comate_cli-0.2.6}/.gitignore +1 -0
  2. {comate_cli-0.2.4 → comate_cli-0.2.6}/PKG-INFO +1 -1
  3. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/main.py +6 -1
  4. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/event_renderer.py +99 -44
  5. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tool_view.py +42 -0
  6. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui.py +42 -0
  7. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/history_sync.py +20 -2
  8. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/render_panels.py +48 -11
  9. {comate_cli-0.2.4 → comate_cli-0.2.6}/pyproject.toml +1 -1
  10. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_completion_status_panel.py +1 -1
  11. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_event_renderer.py +69 -9
  12. comate_cli-0.2.6/tests/test_task_panel_format.py +96 -0
  13. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_task_panel_key_bindings.py +8 -8
  14. comate_cli-0.2.6/tests/test_task_panel_rendering.py +116 -0
  15. comate_cli-0.2.6/tests/test_task_poll.py +64 -0
  16. comate_cli-0.2.6/tests/test_tool_view.py +149 -0
  17. comate_cli-0.2.6/uv.lock +2253 -0
  18. comate_cli-0.2.4/tests/test_tool_view.py +0 -64
  19. comate_cli-0.2.4/uv.lock +0 -2226
  20. {comate_cli-0.2.4 → comate_cli-0.2.6}/README.md +0 -0
  21. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/__init__.py +0 -0
  22. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/__main__.py +0 -0
  23. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/mcp_cli.py +0 -0
  24. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/__init__.py +0 -0
  25. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/animations.py +0 -0
  26. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/app.py +0 -0
  27. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/assistant_render.py +0 -0
  28. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  29. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/env_utils.py +0 -0
  30. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/error_display.py +0 -0
  31. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  32. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/history_printer.py +0 -0
  33. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/input_geometry.py +0 -0
  34. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  35. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/logging_adapter.py +0 -0
  36. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/logo.py +0 -0
  37. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/markdown_render.py +0 -0
  38. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/mention_completer.py +0 -0
  39. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/message_style.py +0 -0
  40. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/models.py +0 -0
  41. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/preflight.py +0 -0
  42. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/question_view.py +0 -0
  43. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/resume_selector.py +0 -0
  44. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/rewind_store.py +0 -0
  45. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  46. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  47. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/selection_menu.py +0 -0
  48. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/session_view.py +0 -0
  49. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/slash_commands.py +0 -0
  50. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/startup.py +0 -0
  51. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/status_bar.py +0 -0
  52. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/text_effects.py +0 -0
  53. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tips.py +0 -0
  54. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  55. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
  56. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
  57. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
  58. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  59. {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  60. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/conftest.py +0 -0
  61. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_app_mcp_preload.py +0 -0
  62. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_app_preflight_gate.py +0 -0
  63. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_app_shutdown.py +0 -0
  64. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_app_usage_line.py +0 -0
  65. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_cli_project_root.py +0 -0
  66. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_compact_command_semantics.py +0 -0
  67. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_completion_context_activation.py +0 -0
  68. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_context_command.py +0 -0
  69. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_custom_slash_commands.py +0 -0
  70. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_history_sync.py +0 -0
  71. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_input_behavior.py +0 -0
  72. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_interrupt_exit_semantics.py +0 -0
  73. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_layout_coordinator.py +0 -0
  74. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_logging_adapter.py +0 -0
  75. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_logo.py +0 -0
  76. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_main_args.py +0 -0
  77. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_mcp_cli.py +0 -0
  78. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_mcp_slash_command.py +0 -0
  79. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_mention_completer.py +0 -0
  80. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_preflight.py +0 -0
  81. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_preflight_copilot.py +0 -0
  82. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_question_key_bindings.py +0 -0
  83. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_question_view.py +0 -0
  84. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_resume_selector.py +0 -0
  85. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_rewind_command_semantics.py +0 -0
  86. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_rewind_store.py +0 -0
  87. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_rpc_protocol.py +0 -0
  88. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_rpc_stdio_bridge.py +0 -0
  89. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_selection_menu.py +0 -0
  90. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_skills_slash_command.py +0 -0
  91. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_slash_argument_hint.py +0 -0
  92. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_slash_completer.py +0 -0
  93. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_slash_registry.py +0 -0
  94. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_status_bar.py +0 -0
  95. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_tui_elapsed_status.py +0 -0
  96. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_tui_mcp_init_gate.py +0 -0
  97. {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_tui_paste_placeholder.py +0 -0
  98. {comate_cli-0.2.4 → comate_cli-0.2.6}/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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comate-cli
3
- Version: 0.2.4
3
+ Version: 0.2.6
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
@@ -6,7 +6,10 @@ import logging
6
6
  import signal
7
7
  import subprocess
8
8
  import sys
9
- import termios
9
+ try:
10
+ import termios
11
+ except ImportError:
12
+ termios = None # Windows 不支持
10
13
  import threading
11
14
 
12
15
  logger = logging.getLogger(__name__)
@@ -24,6 +27,8 @@ class _TerminalStateGuard:
24
27
  self._attrs: list[int | list[int | bytes]] | None = None
25
28
  self._enabled = False
26
29
 
30
+ if termios is None:
31
+ return
27
32
  stdin = sys.__stdin__
28
33
  if stdin is None:
29
34
  return
@@ -32,7 +32,7 @@ from rich.console import RenderableType
32
32
  from rich.text import Text
33
33
 
34
34
  from comate_cli.terminal_agent.models import HistoryEntry, LoadingState
35
- 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
36
36
  from comate_cli.terminal_agent.env_utils import read_env_int
37
37
 
38
38
  logger = logging.getLogger(__name__)
@@ -111,23 +111,25 @@ def _task_sort_key(task: dict[str, Any]) -> tuple[int, int]:
111
111
 
112
112
 
113
113
  def _format_task_row(task: dict[str, Any]) -> str:
114
- task_id = str(task.get("id", "?")).strip() or "?"
114
+ """格式化单条 task 行。
115
+
116
+ 符号约定:
117
+ - ✓ completed
118
+ - ◼ in_progress
119
+ - ▢ pending (unblocked)
120
+ - ▫ pending (blocked) — 视觉相似的中间符号,渲染层统一显示为 ▢ 但应用不同颜色
121
+ """
115
122
  subject = str(task.get("subject", "")).strip() or "(untitled)"
116
- owner = str(task.get("owner", "")).strip()
117
123
  status = str(task.get("status", "pending")).strip().lower()
118
124
  open_blocked_by = task.get("open_blocked_by", [])
119
- owner_suffix = f" owner={owner}" if owner else ""
120
- blocked_suffix = ""
121
- if isinstance(open_blocked_by, list) and open_blocked_by:
122
- blocked_suffix = f" blocked by #{','.join(str(item) for item in open_blocked_by)}"
123
125
 
124
126
  if status == "completed":
125
- return f" [#{task_id}] {subject}{owner_suffix}"
127
+ return f"✓ {subject}"
126
128
  if status == "in_progress":
127
- return f" [#{task_id}] {subject}{owner_suffix}"
128
- if blocked_suffix:
129
- return f" [#{task_id}] {subject}{owner_suffix}{blocked_suffix}"
130
- 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}"
131
133
 
132
134
 
133
135
  @dataclass
@@ -154,6 +156,7 @@ class _RunningTool:
154
156
  subagent_description: str = ""
155
157
  nested_tools: list[tuple[str, _SubagentTool]] = field(default_factory=list)
156
158
  show_init: bool = False
159
+ subagent_model_name: str = ""
157
160
 
158
161
 
159
162
  class EventRenderer:
@@ -162,6 +165,7 @@ class EventRenderer:
162
165
  def __init__(self, project_root: Path | None = None) -> None:
163
166
  self._history: list[HistoryEntry] = []
164
167
  self._running_tools: dict[str, _RunningTool] = {}
168
+ self._tool_call_args: dict[str, dict[str, Any]] = {}
165
169
  self._thinking_content: str = ""
166
170
  self._assistant_buffer = ""
167
171
  self._loading_state: LoadingState = LoadingState.idle()
@@ -237,6 +241,7 @@ class EventRenderer:
237
241
  self._history = []
238
242
  self._recent_team_event_keys.clear()
239
243
  self._running_tools.clear()
244
+ self._tool_call_args.clear()
240
245
  self._thinking_content = ""
241
246
  self._assistant_buffer = ""
242
247
  self._loading_state = LoadingState.idle()
@@ -384,10 +389,13 @@ class EventRenderer:
384
389
  self._flush_assistant_segment()
385
390
  self._history.append(HistoryEntry(entry_type="assistant", text=normalized))
386
391
 
387
- def tool_panel_entries(self, *, max_lines: int | None = None) -> list[tuple[int, str]]:
392
+ def tool_panel_entries(
393
+ self, *, max_lines: int | None = None
394
+ ) -> list[tuple[int, str | list[tuple[str, str]]]]:
388
395
  """Return panel entries for running tools.
389
396
 
390
- Each entry is a tuple: (indent_level, text_without_dot_prefix).
397
+ Each entry is a tuple: (indent_level, content).
398
+ content is either a plain str or a list of (style, text) prompt_toolkit fragments.
391
399
  indent_level == 0 means a primary tool line; >0 means nested status.
392
400
  indent_level < 0 means a meta line (no dot prefix).
393
401
  """
@@ -426,13 +434,23 @@ class EventRenderer:
426
434
  if state.progress_tokens > 0
427
435
  else ""
428
436
  )
429
- # 格式:SubagentName(描述)
437
+ # 格式:SubagentName(描述) · model(dim) · elapsed · tools · tokens
430
438
  subagent_name = state.subagent_name or "Agent"
431
439
  description = state.subagent_description or state.title
432
440
  title = f"{subagent_name}({description})"
433
441
  tool_count = len(state.nested_tools)
434
442
  tool_count_suffix = f" · +{tool_count} tool uses" if tool_count > 0 else ""
435
- entries.append((0, f"{title} · {elapsed}{tool_count_suffix}{tokens_suffix}"))
443
+ rest = f" · {elapsed}{tool_count_suffix}{tokens_suffix}"
444
+ if state.subagent_model_name:
445
+ # Return styled fragments: model_name in dim
446
+ frags: list[tuple[str, str]] = [
447
+ ("", title),
448
+ ("class:dim", f" · {state.subagent_model_name}"),
449
+ ("", rest),
450
+ ]
451
+ entries.append((0, frags))
452
+ else:
453
+ entries.append((0, f"{title}{rest}"))
436
454
 
437
455
  # 嵌套工具调用(最多显示最近 3 个)
438
456
  for child_id, child_tool in state.nested_tools[-3:]:
@@ -464,7 +482,7 @@ class EventRenderer:
464
482
  return lines
465
483
 
466
484
  clipped = lines[: normalized_limit - 1]
467
- clipped.append(f" … (+{len(lines) - (normalized_limit - 1)})")
485
+ clipped.append(f"… (+{len(lines) - (normalized_limit - 1)})")
468
486
  return clipped
469
487
 
470
488
  def full_task_panel_lines(self) -> list[str]:
@@ -495,6 +513,7 @@ class EventRenderer:
495
513
  signature: str,
496
514
  is_error: bool = False,
497
515
  diff_lines: list[str] | None = None,
516
+ model_name: str = "",
498
517
  ) -> None:
499
518
  """Append a tool result to history as a static entry (no timer).
500
519
 
@@ -502,17 +521,27 @@ class EventRenderer:
502
521
  signature: 工具签名,例如 "Read(path=xxx)"
503
522
  is_error: 是否为错误结果
504
523
  diff_lines: optional diff lines for Edit/MultiEdit
524
+ model_name: model name for Agent tools (rendered dim)
505
525
  """
506
526
  sev: Literal["info", "warning", "error"] = "error" if is_error else "info"
507
527
  if not is_error and diff_lines and len(diff_lines) > 0:
508
528
  self._latest_diff_lines = diff_lines
509
529
  text_obj = Text(signature)
530
+ if model_name:
531
+ text_obj.append(f" · {model_name}", style="dim")
510
532
  text_obj.append("\n")
511
533
  text_obj.append(self._render_diff_text(diff_lines, max_lines=self._diff_max_lines))
512
534
  self._history.append(
513
535
  HistoryEntry(entry_type="tool_result", text=text_obj, severity="info")
514
536
  )
515
537
  return
538
+ if model_name:
539
+ text_obj = Text(signature)
540
+ text_obj.append(f" · {model_name}", style="dim")
541
+ self._history.append(
542
+ HistoryEntry(entry_type="tool_result", text=text_obj, severity=sev)
543
+ )
544
+ return
516
545
  self._history.append(
517
546
  HistoryEntry(
518
547
  entry_type="tool_result",
@@ -573,14 +602,19 @@ class EventRenderer:
573
602
  task_title = f"{subagent_name}({description})"
574
603
  else:
575
604
  task_title = subagent_name
605
+
606
+ model_name = ""
607
+ if metadata and isinstance(metadata, dict):
608
+ model_name = metadata.get("model_name", "")
609
+
576
610
  tool_count = len(state.nested_tools)
577
- tool_count_suffix = f" · +{tool_count} tool uses" if tool_count > 0 else ""
578
- tokens_suffix = (
579
- f" · {_format_tokens(state.progress_tokens)}"
580
- if state.progress_tokens > 0
581
- else ""
582
- )
583
- base = f"{task_title}{tool_count_suffix}{tokens_suffix}"
611
+ base = Text(task_title)
612
+ if model_name:
613
+ base.append(f" · {model_name}", style="dim")
614
+ if tool_count > 0:
615
+ base.append(f" · +{tool_count} tool uses")
616
+ if state.progress_tokens > 0:
617
+ base.append(f" · {_format_tokens(state.progress_tokens)}")
584
618
  else:
585
619
  display_name = state.display_tool_name or state.tool_name
586
620
  summary = state.args_summary
@@ -603,7 +637,10 @@ class EventRenderer:
603
637
  diff_lines = metadata.get("diff")
604
638
  if isinstance(diff_lines, list) and len(diff_lines) > 0:
605
639
  self._latest_diff_lines = diff_lines
606
- text_obj = Text(f"{base}{error_suffix}")
640
+ if isinstance(base, Text):
641
+ text_obj = base
642
+ else:
643
+ text_obj = Text(f"{base}{error_suffix}")
607
644
  text_obj.append("\n")
608
645
  text_obj.append(self._render_diff_text(diff_lines, max_lines=self._diff_max_lines))
609
646
  self._history.append(
@@ -611,7 +648,12 @@ class EventRenderer:
611
648
  )
612
649
  return
613
650
 
614
- self._history.append(HistoryEntry(entry_type="tool_result", text=f"{base}{error_suffix}", severity=sev))
651
+ if isinstance(base, Text):
652
+ if error_suffix:
653
+ base.append(error_suffix)
654
+ self._history.append(HistoryEntry(entry_type="tool_result", text=base, severity=sev))
655
+ else:
656
+ self._history.append(HistoryEntry(entry_type="tool_result", text=f"{base}{error_suffix}", severity=sev))
615
657
 
616
658
  @staticmethod
617
659
  def _render_diff_text(diff_lines: list[str], max_lines: int = 50) -> Text:
@@ -730,7 +772,16 @@ class EventRenderer:
730
772
  return
731
773
 
732
774
  all_completed = all(str(item.get("status", "")).strip().lower() == "completed" for item in normalized)
733
- header = "Tasks" if list_id == "default" else f"Tasks · {list_id}"
775
+ # 标题:显示 ID 最小的 in_progress task subject,否则 "Tasks"
776
+ in_progress_tasks = [
777
+ t for t in normalized
778
+ if str(t.get("status", "")).strip().lower() == "in_progress"
779
+ ]
780
+ if in_progress_tasks:
781
+ in_progress_tasks.sort(key=lambda t: int(t.get("id", 0)))
782
+ header = str(in_progress_tasks[0].get("subject", "")).strip() or "Tasks"
783
+ else:
784
+ header = "Tasks"
734
785
  if not all_completed:
735
786
  if not self._current_tasks:
736
787
  self._task_started_at_monotonic = time.monotonic()
@@ -776,17 +827,21 @@ class EventRenderer:
776
827
 
777
828
  for task in tasks:
778
829
  line = _format_task_row(task)
779
- if line.startswith(" ✓ "):
830
+ if line.startswith("✓ "):
780
831
  result.append(" ✓ ")
781
- result.append(line[4:], style="green strike")
782
- elif line.startswith(" "):
783
- result.append(" ")
784
- result.append(line[4:], style="yellow")
785
- elif line.startswith(" "):
786
- result.append(" ◌ ")
787
- result.append(line[4:], style="dim")
832
+ result.append(line[2:], style="green strike")
833
+ elif line.startswith(" "):
834
+ result.append(" ")
835
+ result.append(line[2:], style="#F97316")
836
+ elif line.startswith(" "):
837
+ # blocked pending: display as ▢ + dim
838
+ result.append(" ▢ ")
839
+ result.append(line[2:], style="dim")
840
+ elif line.startswith("▢ "):
841
+ result.append(" ▢ ")
842
+ result.append(line[2:])
788
843
  else:
789
- result.append(line)
844
+ result.append(f" {line}")
790
845
  result.append("\n")
791
846
 
792
847
  # 移除末尾的换行
@@ -861,18 +916,15 @@ class EventRenderer:
861
916
  case ToolCallEvent(tool=tool_name, args=arguments, tool_call_id=tool_call_id):
862
917
  args_dict = arguments if isinstance(arguments, dict) else {"_raw": str(arguments)}
863
918
  self._thinking_content = ""
864
- # Task* / AskUserQuestion / ExitPlanMode: skip normal tool tracking.
865
- if tool_name.lower() in ("taskcreate", "tasklist", "taskget", "taskupdate", "askuserquestion", "exitplanmode"):
919
+ # Store args for ToolResult phase lookup.
920
+ self._tool_call_args[tool_call_id] = args_dict
921
+ if not should_show_tool_in_scrollback(tool_name, args_dict):
866
922
  self._rebuild_loading_line()
867
923
  return (False, None)
868
924
  self._append_tool_call(tool_name, args_dict, tool_call_id)
869
925
  case ToolResultEvent(tool=tool_name, result=result, tool_call_id=tool_call_id, is_error=is_error, metadata=metadata):
870
- if tool_name.lower() in ("taskcreate", "tasklist", "taskupdate") and not is_error:
871
- # Successful task coordination updates should not spam scrollback.
872
- self._rebuild_loading_line()
873
- return (False, None)
874
- if tool_name.lower() in ("askuserquestion", "exitplanmode") and not is_error:
875
- # Success is already visible via UserQuestionEvent / PlanApprovalRequiredEvent.
926
+ stored_args = self._tool_call_args.pop(tool_call_id, {})
927
+ if not should_show_tool_in_scrollback(tool_name, stored_args, is_result=True, is_error=is_error):
876
928
  self._running_tools.pop(tool_call_id, None)
877
929
  self._rebuild_loading_line()
878
930
  return (False, None)
@@ -902,6 +954,7 @@ class EventRenderer:
902
954
  status=status,
903
955
  elapsed_ms=elapsed_ms,
904
956
  tokens=tokens,
957
+ model_name=model_name,
905
958
  ):
906
959
  state = self._running_tools.get(tool_call_id)
907
960
  if state is not None:
@@ -914,6 +967,8 @@ class EventRenderer:
914
967
  state.subagent_name = str(subagent_name or "").strip()
915
968
  state.subagent_status = str(status or "").strip()
916
969
  state.subagent_description = str(description or "").strip()
970
+ if model_name:
971
+ state.subagent_model_name = str(model_name)
917
972
  # 首个有效子事件后移除 init 占位。
918
973
  progress_status = str(status or "").strip().lower()
919
974
  has_activity = bool(tokens and int(tokens) > 0) or bool(
@@ -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):
@@ -75,6 +75,8 @@ from comate_cli.terminal_agent.tui_parts import (
75
75
 
76
76
  logger = logging.getLogger(__name__)
77
77
 
78
+ _TASK_POLL_INTERVAL_S = 2.0
79
+
78
80
 
79
81
  class TerminalAgentTUI(
80
82
  KeyBindingsMixin,
@@ -96,6 +98,7 @@ class TerminalAgentTUI(
96
98
  except Exception:
97
99
  pass
98
100
  self._renderer = renderer
101
+ self._task_poll_next_at = time.monotonic() + _TASK_POLL_INTERVAL_S
99
102
  self._rewind_store = RewindStore(session=self._session, project_root=Path.cwd())
100
103
 
101
104
  # Team inbox 消息直连 scrollback(绕开 session_event_queue,实现实时显示)
@@ -1037,6 +1040,34 @@ class TerminalAgentTUI(
1037
1040
  self._sync_focus_for_mode()
1038
1041
  self._render_dirty = True
1039
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
+
1040
1071
  async def _ui_tick(self) -> None:
1041
1072
  try:
1042
1073
  while not self._closing:
@@ -1091,6 +1122,17 @@ class TerminalAgentTUI(
1091
1122
  or self._renderer.has_running_tools()
1092
1123
  )
1093
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
+
1094
1136
  await asyncio.sleep(sleep_s)
1095
1137
  except asyncio.CancelledError:
1096
1138
  return
@@ -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,11 +130,18 @@ 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
+
138
+ # Extract metadata from ContextItem
139
+ item_metadata = getattr(item, "metadata", {}) or {}
140
+
130
141
  # Extract diff from raw_envelope for Edit/MultiEdit
131
142
  diff_lines: list[str] | None = None
132
143
  if tool_name in ("Edit", "MultiEdit") and not is_error:
133
- raw_envelope = getattr(item, "metadata", {}) or {}
134
- envelope = raw_envelope.get("tool_raw_envelope")
144
+ envelope = item_metadata.get("tool_raw_envelope")
135
145
  if isinstance(envelope, dict):
136
146
  data = envelope.get("data", {})
137
147
  if isinstance(data, dict):
@@ -139,11 +149,19 @@ class HistorySyncMixin:
139
149
  if isinstance(diff, list) and len(diff) > 0:
140
150
  diff_lines = diff
141
151
 
152
+ # Extract model_name for Agent tools
153
+ model_name = ""
154
+ if tool_name.lower() == "agent":
155
+ exec_meta = item_metadata.get("tool_execution_meta")
156
+ if isinstance(exec_meta, dict):
157
+ model_name = exec_meta.get("model_name", "")
158
+
142
159
  # 直接追加静态 HistoryEntry(无计时器)
143
160
  self._renderer.append_static_tool_result(
144
161
  signature,
145
162
  is_error,
146
163
  diff_lines=diff_lines,
164
+ model_name=model_name,
147
165
  )
148
166
  continue
149
167
 
@@ -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
 
@@ -296,15 +296,23 @@ class RenderPanelsMixin:
296
296
  fragments.extend(self._rich_text_to_pt_fragments(renderable))
297
297
  fragments.append(("", "\n"))
298
298
 
299
+ dim_style = "fg:#6B7280"
299
300
  last_index = len(entries) - 1
300
301
  for idx, (indent, line) in enumerate(entries):
301
302
  if indent < 0:
302
303
  clipped = fit_single_line(line, width - 1)
303
304
  fragments.append((nested_style, clipped))
304
305
  elif indent == 0:
305
- clipped = fit_single_line(line, max(width - 2, 8))
306
- fragments.append((dot_style, f"{dot_glyph} "))
307
- fragments.append((primary_style, clipped))
306
+ if isinstance(line, list):
307
+ # Styled fragments from tool_panel_entries
308
+ fragments.append((dot_style, f"{dot_glyph} "))
309
+ for frag_style, frag_text in line:
310
+ resolved = dim_style if frag_style == "class:dim" else (frag_style or primary_style)
311
+ fragments.append((resolved, frag_text))
312
+ else:
313
+ clipped = fit_single_line(line, max(width - 2, 8))
314
+ fragments.append((dot_style, f"{dot_glyph} "))
315
+ fragments.append((primary_style, clipped))
308
316
  else:
309
317
  padding = " " * indent
310
318
  clipped = fit_single_line(line, max(width - get_cwidth(padding), 8))
@@ -558,13 +566,42 @@ class RenderPanelsMixin:
558
566
  if title_elapsed_suffix:
559
567
  fragments.append(("fg:#6B7280", title_elapsed_suffix))
560
568
  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))
569
+ clipped = fit_single_line(line, width - 3) # 留空间给 prefix + space
570
+ # prefix 列:第一条任务行用 ⎿,后续用空格
571
+ is_first_task_row = idx == 1
572
+ prefix_char = "" if is_first_task_row else " "
573
+ prefix_style = "fg:#555555"
574
+
575
+ # 解析符号和文本,应用分色
576
+ if clipped.startswith("✓ "):
577
+ symbol, text = "✓", clipped[2:]
578
+ symbol_style = "fg:#86EFAC"
579
+ text_style = "fg:#6B7280 strike"
580
+ elif clipped.startswith("◼ "):
581
+ symbol, text = "◼", clipped[2:]
582
+ symbol_style = "fg:#F97316"
583
+ text_style = "fg:#E2E8F0"
584
+ elif clipped.startswith("▫ "):
585
+ # blocked pending: ▫ 是中间符号,渲染时显示为 ▢
586
+ symbol, text = "▢", clipped[2:]
587
+ symbol_style = "fg:#4B5563"
588
+ text_style = "fg:#4B5563"
589
+ elif clipped.startswith("▢ "):
590
+ symbol, text = "▢", clipped[2:]
591
+ symbol_style = "fg:#6B7280"
592
+ text_style = "fg:#6B7280"
593
+ else:
594
+ # 溢出行 "… (+N)" 等
595
+ symbol, text = "", clipped
596
+ symbol_style = ""
597
+ text_style = "fg:#6B7280"
598
+
599
+ fragments.append((prefix_style, prefix_char))
600
+ fragments.append(("", " "))
601
+ if symbol:
602
+ fragments.append((symbol_style, symbol))
603
+ fragments.append(("", " "))
604
+ fragments.append((text_style, text))
568
605
  if idx != last_index:
569
606
  fragments.append(("", "\n"))
570
607
  return fragments
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "comate-cli"
7
- version = "0.2.4"
7
+ version = "0.2.6"
8
8
  description = "Comate terminal CLI built on comate-agent-sdk"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -68,7 +68,7 @@ class _FakeRenderer:
68
68
  if len(self.todo_lines) <= max_lines:
69
69
  return list(self.todo_lines)
70
70
  clipped = list(self.todo_lines[: max_lines - 1])
71
- clipped.append(f" … (+{len(self.todo_lines) - (max_lines - 1)})")
71
+ clipped.append(f"… (+{len(self.todo_lines) - (max_lines - 1)})")
72
72
  return clipped
73
73
 
74
74
  def todo_all_lines(self) -> list[str]: