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.
- {comate_cli-0.2.3 → comate_cli-0.2.5}/.gitignore +2 -1
- {comate_cli-0.2.3 → comate_cli-0.2.5}/PKG-INFO +1 -1
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/app.py +1 -1
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/event_renderer.py +136 -31
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tool_view.py +42 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui.py +81 -1
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/commands.py +1 -1
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/history_sync.py +67 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/render_panels.py +37 -8
- {comate_cli-0.2.3 → comate_cli-0.2.5}/pyproject.toml +1 -1
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_completion_status_panel.py +1 -1
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_event_renderer.py +132 -6
- comate_cli-0.2.5/tests/test_task_panel_format.py +96 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_task_panel_key_bindings.py +8 -8
- comate_cli-0.2.5/tests/test_task_panel_rendering.py +116 -0
- comate_cli-0.2.5/tests/test_task_poll.py +64 -0
- comate_cli-0.2.5/tests/test_tool_view.py +149 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/uv.lock +2 -2
- comate_cli-0.2.3/tests/test_tool_view.py +0 -64
- {comate_cli-0.2.3 → comate_cli-0.2.5}/README.md +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/__init__.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/__main__.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/main.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/history_printer.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/rewind_store.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/session_view.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/conftest.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_context_command.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_history_sync.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_logo.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_main_args.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_preflight.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_question_view.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_rewind_store.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_status_bar.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.2.3 → comate_cli-0.2.5}/tests/test_tui_split_invariance.py +0 -0
|
@@ -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.
|
|
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
|
-
|
|
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"
|
|
127
|
+
return f"✓ {subject}"
|
|
123
128
|
if status == "in_progress":
|
|
124
|
-
return f"
|
|
125
|
-
if
|
|
126
|
-
return f"
|
|
127
|
-
return f"
|
|
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"
|
|
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
|
-
|
|
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[
|
|
707
|
-
elif line.startswith("
|
|
708
|
-
result.append("
|
|
709
|
-
result.append(line[
|
|
710
|
-
elif line.startswith("
|
|
711
|
-
|
|
712
|
-
result.append(
|
|
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
|
-
#
|
|
790
|
-
|
|
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
|
-
|
|
796
|
-
|
|
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.
|
|
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 -
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|