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.
- {comate_cli-0.2.4 → comate_cli-0.2.6}/.gitignore +1 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/PKG-INFO +1 -1
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/main.py +6 -1
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/event_renderer.py +99 -44
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tool_view.py +42 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui.py +42 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/history_sync.py +20 -2
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/render_panels.py +48 -11
- {comate_cli-0.2.4 → comate_cli-0.2.6}/pyproject.toml +1 -1
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_completion_status_panel.py +1 -1
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_event_renderer.py +69 -9
- comate_cli-0.2.6/tests/test_task_panel_format.py +96 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_task_panel_key_bindings.py +8 -8
- comate_cli-0.2.6/tests/test_task_panel_rendering.py +116 -0
- comate_cli-0.2.6/tests/test_task_poll.py +64 -0
- comate_cli-0.2.6/tests/test_tool_view.py +149 -0
- comate_cli-0.2.6/uv.lock +2253 -0
- comate_cli-0.2.4/tests/test_tool_view.py +0 -64
- comate_cli-0.2.4/uv.lock +0 -2226
- {comate_cli-0.2.4 → comate_cli-0.2.6}/README.md +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/__init__.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/__main__.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/app.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/history_printer.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/rewind_store.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/session_view.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/conftest.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_context_command.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_history_sync.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_logo.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_main_args.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_preflight.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_question_view.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_rewind_store.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_status_bar.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.2.4 → comate_cli-0.2.6}/tests/test_tui_split_invariance.py +0 -0
|
@@ -6,7 +6,10 @@ import logging
|
|
|
6
6
|
import signal
|
|
7
7
|
import subprocess
|
|
8
8
|
import sys
|
|
9
|
-
|
|
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
|
-
|
|
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"
|
|
127
|
+
return f"✓ {subject}"
|
|
126
128
|
if status == "in_progress":
|
|
127
|
-
return f"
|
|
128
|
-
if
|
|
129
|
-
return f"
|
|
130
|
-
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}"
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
f" · {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
782
|
-
elif line.startswith("
|
|
783
|
-
result.append("
|
|
784
|
-
result.append(line[
|
|
785
|
-
elif line.startswith("
|
|
786
|
-
|
|
787
|
-
result.append(
|
|
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
|
-
#
|
|
865
|
-
|
|
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
|
-
|
|
871
|
-
|
|
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
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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 -
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
|
@@ -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"
|
|
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]:
|