comate-cli 0.3.2__tar.gz → 0.3.4__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.3.2 → comate_cli-0.3.4}/PKG-INFO +1 -1
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/app.py +7 -6
- comate_cli-0.3.4/comate_cli/terminal_agent/codenames.py +15 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/custom_slash_commands.py +2 -2
- comate_cli-0.3.4/comate_cli/terminal_agent/error_display.py +83 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/event_renderer.py +28 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/history_printer.py +13 -9
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/logo.py +10 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/models.py +1 -1
- comate_cli-0.3.4/comate_cli/terminal_agent/path_context_hint.py +88 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/preflight.py +3 -3
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/status_bar.py +9 -1
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui.py +41 -22
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/input_behavior.py +3 -3
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/render_panels.py +7 -1
- {comate_cli-0.3.2 → comate_cli-0.3.4}/pyproject.toml +1 -1
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_event_renderer.py +99 -0
- comate_cli-0.3.4/tests/test_format_error.py +153 -0
- comate_cli-0.3.4/tests/test_handle_error.py +115 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_history_printer.py +17 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_interrupt_exit_semantics.py +5 -0
- comate_cli-0.3.4/tests/test_path_context_hint.py +106 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_status_bar_transient.py +27 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_tui_mcp_init_gate.py +5 -1
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_tui_paste_placeholder.py +52 -0
- comate_cli-0.3.4/uv.lock +2215 -0
- comate_cli-0.3.2/comate_cli/terminal_agent/error_display.py +0 -46
- comate_cli-0.3.2/uv.lock +0 -2231
- {comate_cli-0.3.2 → comate_cli-0.3.4}/.gitignore +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/README.md +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/__init__.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/__main__.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/main.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/rewind_store.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/session_view.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tool_view.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/conftest.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_completion_status_panel.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_context_command.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_history_sync.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_input_history.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_logo.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_main_args.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_preflight.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_question_view.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_rewind_store.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_status_bar.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_task_poll.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_tool_view.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_tui_split_invariance.py +0 -0
|
@@ -334,6 +334,13 @@ async def run(
|
|
|
334
334
|
return
|
|
335
335
|
resume_session_id = selected_session_id
|
|
336
336
|
|
|
337
|
+
# 在 _resolve_session 前安装 TUILoggingHandler,确保 session 初始化期间
|
|
338
|
+
# 的 logger.warning()(如 user_instruction token 超限)能被 TUI 系统捕获,
|
|
339
|
+
# 而非 fallthrough 到 Python lastResort StreamHandler 以原始文本输出到 stderr。
|
|
340
|
+
renderer = EventRenderer(project_root=project_root)
|
|
341
|
+
from comate_cli.terminal_agent.logging_adapter import setup_tui_logging
|
|
342
|
+
logging_session = setup_tui_logging(renderer)
|
|
343
|
+
|
|
337
344
|
session, mode = _resolve_session(agent, resume_session_id, cwd=project_root)
|
|
338
345
|
|
|
339
346
|
# 版本检查:带超时,不阻塞启动
|
|
@@ -349,12 +356,6 @@ async def run(
|
|
|
349
356
|
if mode == "resume":
|
|
350
357
|
await status_bar.refresh()
|
|
351
358
|
|
|
352
|
-
renderer = EventRenderer(project_root=project_root)
|
|
353
|
-
|
|
354
|
-
# 配置 TUI logging handler(将 SDK 日志输出到 TUI)
|
|
355
|
-
from comate_cli.terminal_agent.logging_adapter import setup_tui_logging
|
|
356
|
-
logging_session = setup_tui_logging(renderer)
|
|
357
|
-
|
|
358
359
|
tui = TerminalAgentTUI(session, status_bar, renderer)
|
|
359
360
|
tui.add_resume_history(mode)
|
|
360
361
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Version codenames — one entry per memorable release."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
CODENAMES: dict[str, str] = {
|
|
6
|
+
"0.3.4": "",
|
|
7
|
+
"0.3.3": "Update for My Brothers",
|
|
8
|
+
"0.3.2": "We are with Ukraine",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_codename(ver: str) -> str | None:
|
|
13
|
+
"""Return codename for a version string like '0.3.2' or 'v0.3.2'."""
|
|
14
|
+
stripped = ver.lstrip("v")
|
|
15
|
+
return CODENAMES.get(stripped)
|
|
@@ -21,7 +21,7 @@ DEFAULT_BASH_TIMEOUT_SECONDS = 10.0
|
|
|
21
21
|
_COMMAND_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")
|
|
22
22
|
_ARG_PLACEHOLDER_PATTERN = re.compile(r"\$ARGUMENTS\[(\d+)\]|\$(\d+)|\$ARGUMENTS")
|
|
23
23
|
_BASH_PATTERN = re.compile(r"!\`([^`\n]+)\`")
|
|
24
|
-
|
|
24
|
+
FILE_REF_PATTERN = re.compile(r"(?<!\S)@([^\s]+)")
|
|
25
25
|
_MARKER_PREFIX = "<<__COMATE_CUSTOM_BLOCK_"
|
|
26
26
|
_MARKER_SUFFIX = "__>>"
|
|
27
27
|
|
|
@@ -354,7 +354,7 @@ def _replace_file_reference_markers(
|
|
|
354
354
|
cursor = 0
|
|
355
355
|
marker_index = len(marker_map)
|
|
356
356
|
|
|
357
|
-
for match in
|
|
357
|
+
for match in FILE_REF_PATTERN.finditer(text):
|
|
358
358
|
parts.append(text[cursor : match.start()])
|
|
359
359
|
raw_path = match.group(1).strip()
|
|
360
360
|
if not raw_path:
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Unified error formatter for terminal agent."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def format_error(exc: Exception) -> tuple[str, str, str]:
|
|
8
|
+
"""Convert exception to user-friendly (message, transient_summary, severity).
|
|
9
|
+
|
|
10
|
+
- message: full error text for scrollback (plain English, no icons)
|
|
11
|
+
- transient_summary: short text for status bar transient notification
|
|
12
|
+
- severity: "error" or "warning"
|
|
13
|
+
"""
|
|
14
|
+
exc_type = type(exc).__name__
|
|
15
|
+
exc_msg = str(exc)
|
|
16
|
+
|
|
17
|
+
# LLM Provider errors
|
|
18
|
+
if exc_type == "ModelRateLimitError":
|
|
19
|
+
return "Rate limit exceeded", "Rate limited", "warning"
|
|
20
|
+
|
|
21
|
+
if exc_type == "ModelProviderError":
|
|
22
|
+
code = getattr(exc, "status_code", None)
|
|
23
|
+
|
|
24
|
+
# Context size exceeded (400 with specific body pattern)
|
|
25
|
+
if code == 400:
|
|
26
|
+
ctx = _parse_context_size_error(exc_msg)
|
|
27
|
+
if ctx:
|
|
28
|
+
return (
|
|
29
|
+
f"Request ({ctx[0]} tokens) exceeds context limit ({ctx[1]})",
|
|
30
|
+
"Context limit exceeded",
|
|
31
|
+
"error",
|
|
32
|
+
)
|
|
33
|
+
return f"API error: {_truncate(exc_msg, 80)}", "API error", "error"
|
|
34
|
+
|
|
35
|
+
if code == 401:
|
|
36
|
+
return "Invalid or expired API key", "Auth failed", "error"
|
|
37
|
+
if code == 403:
|
|
38
|
+
return "Access denied to this model", "Access denied", "error"
|
|
39
|
+
if code == 404:
|
|
40
|
+
return "Model not found or invalid API path", "Model not found", "error"
|
|
41
|
+
if code and code >= 500:
|
|
42
|
+
return f"Server error ({code})", "Server error", "warning"
|
|
43
|
+
return f"API error: {_truncate(exc_msg, 80)}", "API error", "error"
|
|
44
|
+
|
|
45
|
+
# Session errors
|
|
46
|
+
if exc_type == "ChatSessionClosedError":
|
|
47
|
+
return "Session closed", "Session closed", "error"
|
|
48
|
+
|
|
49
|
+
# Network errors (heuristic)
|
|
50
|
+
lower_msg = exc_msg.lower()
|
|
51
|
+
if "timeout" in lower_msg or "timed out" in lower_msg:
|
|
52
|
+
return "Request timed out", "Timed out", "warning"
|
|
53
|
+
if "connection" in lower_msg:
|
|
54
|
+
return "Connection failed", "Connection failed", "error"
|
|
55
|
+
|
|
56
|
+
# Generic fallback
|
|
57
|
+
return f"Error: {_truncate(exc_msg, 80)}", "Error occurred", "error"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_context_size_error(msg: str) -> tuple[str, str] | None:
|
|
61
|
+
"""Extract (prompt_tokens, ctx_limit) from context size error body.
|
|
62
|
+
|
|
63
|
+
Matches patterns like 'n_prompt_tokens': 35099, 'n_ctx': 32768
|
|
64
|
+
or 'request (35099 tokens) exceeds the available context size (32768 tokens)'.
|
|
65
|
+
"""
|
|
66
|
+
m = re.search(r"n_prompt_tokens['\"]?:\s*(\d+)", msg)
|
|
67
|
+
n = re.search(r"n_ctx['\"]?:\s*(\d+)", msg)
|
|
68
|
+
if m and n:
|
|
69
|
+
return m.group(1), n.group(1)
|
|
70
|
+
|
|
71
|
+
m2 = re.search(r"request\s*\((\d+)\s*tokens?\)", msg, re.IGNORECASE)
|
|
72
|
+
n2 = re.search(r"context\s*size\s*\((\d+)\s*tokens?\)", msg, re.IGNORECASE)
|
|
73
|
+
if m2 and n2:
|
|
74
|
+
return m2.group(1), n2.group(1)
|
|
75
|
+
|
|
76
|
+
if "exceed_context_size_error" in msg:
|
|
77
|
+
return None # Type matches but can't parse numbers — fall through to generic 400
|
|
78
|
+
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _truncate(s: str, max_len: int) -> str:
|
|
83
|
+
return s[:max_len] + "..." if len(s) > max_len else s
|
|
@@ -34,6 +34,7 @@ from rich.text import Text
|
|
|
34
34
|
from comate_cli.terminal_agent.models import HistoryEntry, LoadingState
|
|
35
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
|
+
from comate_cli.terminal_agent.custom_slash_commands import FILE_REF_PATTERN
|
|
37
38
|
|
|
38
39
|
logger = logging.getLogger(__name__)
|
|
39
40
|
|
|
@@ -41,6 +42,7 @@ _DEFAULT_TOOL_ERROR_SUMMARY_MAX_LEN = 160
|
|
|
41
42
|
_DEFAULT_TOOL_PANEL_MAX_LINES = 4
|
|
42
43
|
_DEFAULT_TASK_PANEL_MAX_LINES = 6
|
|
43
44
|
_RECENT_TEAM_EVENT_CACHE_SIZE = 128
|
|
45
|
+
_FILE_REF_MAX_COUNT_BYTES = 10 * 1024 * 1024 # 10 MB
|
|
44
46
|
|
|
45
47
|
|
|
46
48
|
def _truncate(content: str, max_len: int = 120) -> str:
|
|
@@ -205,6 +207,32 @@ class EventRenderer:
|
|
|
205
207
|
return
|
|
206
208
|
self._flush_assistant_segment()
|
|
207
209
|
self._history.append(HistoryEntry(entry_type="user", text=normalized))
|
|
210
|
+
self._maybe_append_file_ref_hint(normalized)
|
|
211
|
+
|
|
212
|
+
def _maybe_append_file_ref_hint(self, text: str) -> None:
|
|
213
|
+
"""Append a dim ⎿ hint for the first valid @path reference."""
|
|
214
|
+
if self._project_root is None:
|
|
215
|
+
return
|
|
216
|
+
for match in FILE_REF_PATTERN.finditer(text):
|
|
217
|
+
raw_path = match.group(1)
|
|
218
|
+
candidate = self._project_root / raw_path
|
|
219
|
+
try:
|
|
220
|
+
if candidate.is_dir():
|
|
221
|
+
self._history.append(HistoryEntry(entry_type="file_ref", text=f"Listed directory {raw_path}"))
|
|
222
|
+
return
|
|
223
|
+
if candidate.is_file():
|
|
224
|
+
hint = f"Read {raw_path}"
|
|
225
|
+
if candidate.stat().st_size <= _FILE_REF_MAX_COUNT_BYTES:
|
|
226
|
+
try:
|
|
227
|
+
with open(candidate, "rb") as fh:
|
|
228
|
+
line_count = sum(1 for _ in fh)
|
|
229
|
+
hint += f" ({line_count} lines)"
|
|
230
|
+
except OSError:
|
|
231
|
+
pass
|
|
232
|
+
self._history.append(HistoryEntry(entry_type="file_ref", text=hint))
|
|
233
|
+
return
|
|
234
|
+
except OSError:
|
|
235
|
+
continue
|
|
208
236
|
|
|
209
237
|
def close(self) -> None:
|
|
210
238
|
return
|
|
@@ -13,7 +13,7 @@ from comate_cli.terminal_agent.models import HistoryEntry
|
|
|
13
13
|
def _render_subtitle_line(subtitle: str, *, error: bool = False) -> Text:
|
|
14
14
|
"""Render a ⎿ subtitle line for tool results."""
|
|
15
15
|
line = Text()
|
|
16
|
-
line.append(" ⎿", style="#555555")
|
|
16
|
+
line.append(" ⎿ ", style="#555555")
|
|
17
17
|
if error:
|
|
18
18
|
line.append(f" {subtitle}", style="bold red")
|
|
19
19
|
else:
|
|
@@ -85,21 +85,25 @@ def render_history_group(
|
|
|
85
85
|
# System entries: 按 severity 区分视觉样式
|
|
86
86
|
if entry.entry_type == "system":
|
|
87
87
|
if entry.severity == "error":
|
|
88
|
-
|
|
89
|
-
prefix_style = "#FF9FC6"
|
|
88
|
+
text_style = "bold #FF6B6B"
|
|
90
89
|
elif entry.severity == "warning":
|
|
91
|
-
|
|
92
|
-
prefix_style = "#B8B630"
|
|
90
|
+
text_style = "#E8B830"
|
|
93
91
|
else:
|
|
94
|
-
|
|
95
|
-
prefix_style = "dim"
|
|
92
|
+
text_style = "dim"
|
|
96
93
|
line_text = Text()
|
|
97
|
-
line_text.append(
|
|
98
|
-
line_text.append(str(entry.text), style=prefix_style)
|
|
94
|
+
line_text.append(str(entry.text), style=text_style)
|
|
99
95
|
renderables.append(line_text)
|
|
100
96
|
renderables.append(Text(""))
|
|
101
97
|
continue
|
|
102
98
|
|
|
99
|
+
# File reference hint: reuse subtitle style, attached to preceding user message
|
|
100
|
+
if entry.entry_type == "file_ref":
|
|
101
|
+
if renderables and isinstance(renderables[-1], Text) and not renderables[-1].plain:
|
|
102
|
+
renderables.pop()
|
|
103
|
+
renderables.append(_render_subtitle_line(str(entry.text)))
|
|
104
|
+
renderables.append(Text(""))
|
|
105
|
+
continue
|
|
106
|
+
|
|
103
107
|
if entry.entry_type == "tool_result":
|
|
104
108
|
prefix_char = "✖" if entry.severity == "error" else "●"
|
|
105
109
|
prefix_style = "bold red" if entry.severity == "error" else "bold green"
|
|
@@ -8,6 +8,8 @@ from rich.text import Text
|
|
|
8
8
|
|
|
9
9
|
from comate_agent_sdk.utils.paths import PathInput, normalize_path_input
|
|
10
10
|
|
|
11
|
+
from .codenames import get_codename
|
|
12
|
+
|
|
11
13
|
_LOGO_LINES: tuple[str, ...] = (
|
|
12
14
|
"",
|
|
13
15
|
" ██████╗ ██████╗ ███╗ ███╗ █████╗ ████████╗███████╗",
|
|
@@ -59,6 +61,7 @@ def print_logo(console: Console, *, project_root: PathInput | None = None) -> No
|
|
|
59
61
|
line_count = len(_LOGO_LINES)
|
|
60
62
|
|
|
61
63
|
ver = _resolve_version()
|
|
64
|
+
codename = get_codename(ver)
|
|
62
65
|
logo_text = Text()
|
|
63
66
|
for idx, line in enumerate(_LOGO_LINES):
|
|
64
67
|
ratio = idx / max(line_count - 1, 1)
|
|
@@ -66,6 +69,13 @@ def print_logo(console: Console, *, project_root: PathInput | None = None) -> No
|
|
|
66
69
|
logo_text.append(line, style=f"bold rgb({r},{g},{b})")
|
|
67
70
|
if idx == line_count - 1:
|
|
68
71
|
logo_text.append(f" {ver}", style="dim")
|
|
72
|
+
if codename:
|
|
73
|
+
er, eg, eb = end_rgb
|
|
74
|
+
logo_text.append(": ", style="dim")
|
|
75
|
+
logo_text.append(
|
|
76
|
+
codename,
|
|
77
|
+
style=f"italic rgb({er},{eg},{eb})",
|
|
78
|
+
)
|
|
69
79
|
if idx < line_count - 1:
|
|
70
80
|
logo_text.append("\n")
|
|
71
81
|
|
|
@@ -8,7 +8,7 @@ if TYPE_CHECKING:
|
|
|
8
8
|
from rich.text import Text
|
|
9
9
|
|
|
10
10
|
ToolStatus = Literal["running", "success", "error"]
|
|
11
|
-
HistoryEntryType = Literal["user", "assistant", "tool_call", "tool_result", "system", "thinking", "elapsed"]
|
|
11
|
+
HistoryEntryType = Literal["user", "assistant", "tool_call", "tool_result", "system", "thinking", "elapsed", "file_ref"]
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class LoadingStateType(Enum):
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Generate lightweight path-context hints for @path mentions in user messages."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from comate_cli.terminal_agent.custom_slash_commands import FILE_REF_PATTERN
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
_MAX_MENTIONS = 10
|
|
12
|
+
_LINE_COUNT_MAX_BYTES = 256 * 1024 # 256KB
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_path_context_hint(text: str, project_root: Path) -> str | None:
|
|
16
|
+
"""Parse @path mentions from text and return a hint string, or None if no valid paths."""
|
|
17
|
+
resolved_root = project_root.expanduser().resolve()
|
|
18
|
+
matches = list(FILE_REF_PATTERN.finditer(text))
|
|
19
|
+
if not matches:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
hint_lines: list[str] = []
|
|
23
|
+
for match in matches[:_MAX_MENTIONS]:
|
|
24
|
+
raw_path = match.group(1).strip()
|
|
25
|
+
if not raw_path:
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
candidate = (resolved_root / raw_path).resolve()
|
|
29
|
+
# Safety: skip paths that escape project root
|
|
30
|
+
try:
|
|
31
|
+
candidate.relative_to(resolved_root)
|
|
32
|
+
except ValueError:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
if candidate.is_dir():
|
|
37
|
+
hint_lines.append(_describe_dir(raw_path, candidate))
|
|
38
|
+
elif candidate.is_file():
|
|
39
|
+
hint_lines.append(_describe_file(raw_path, candidate))
|
|
40
|
+
else:
|
|
41
|
+
# Not found — only include if path looks like a filesystem path
|
|
42
|
+
if "/" in raw_path or "." in raw_path:
|
|
43
|
+
hint_lines.append(f"- @{raw_path} → not found")
|
|
44
|
+
except OSError:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
if not hint_lines:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
return "<system_reminder>\n" + "\n".join(hint_lines) + "\n</system_reminder>"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _describe_dir(raw_path: str, candidate: Path) -> str:
|
|
54
|
+
try:
|
|
55
|
+
entries = list(candidate.iterdir())
|
|
56
|
+
except OSError:
|
|
57
|
+
return f"- @{raw_path} → directory (unreadable)"
|
|
58
|
+
|
|
59
|
+
if not entries:
|
|
60
|
+
return f"- @{raw_path} → directory (empty)"
|
|
61
|
+
|
|
62
|
+
files = sum(1 for e in entries if e.is_file())
|
|
63
|
+
subdirs = sum(1 for e in entries if e.is_dir())
|
|
64
|
+
|
|
65
|
+
parts: list[str] = []
|
|
66
|
+
if files > 0:
|
|
67
|
+
parts.append(f"{files} file{'s' if files != 1 else ''}")
|
|
68
|
+
if subdirs > 0:
|
|
69
|
+
parts.append(f"{subdirs} subdir{'s' if subdirs != 1 else ''}")
|
|
70
|
+
return f"- @{raw_path} → directory ({', '.join(parts)})" if parts else f"- @{raw_path} → directory ({len(entries)} entries)"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _describe_file(raw_path: str, candidate: Path) -> str:
|
|
74
|
+
suffix = candidate.suffix or "no ext"
|
|
75
|
+
try:
|
|
76
|
+
size = candidate.stat().st_size
|
|
77
|
+
except OSError:
|
|
78
|
+
return f"- @{raw_path} → file ({suffix})"
|
|
79
|
+
|
|
80
|
+
if size > _LINE_COUNT_MAX_BYTES:
|
|
81
|
+
return f"- @{raw_path} → file ({suffix}, ~large)"
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
with open(candidate, "rb") as fh:
|
|
85
|
+
line_count = sum(1 for _ in fh)
|
|
86
|
+
return f"- @{raw_path} → file ({suffix}, {line_count} lines)"
|
|
87
|
+
except OSError:
|
|
88
|
+
return f"- @{raw_path} → file ({suffix})"
|
|
@@ -162,9 +162,9 @@ PROVIDER_PRESETS: tuple[ProviderPreset, ...] = (
|
|
|
162
162
|
provider="minimax",
|
|
163
163
|
base_url="https://api.minimaxi.com/anthropic",
|
|
164
164
|
models={
|
|
165
|
-
"LOW": "MiniMax-M2.
|
|
166
|
-
"MID": "MiniMax-M2.
|
|
167
|
-
"HIGH": "MiniMax-M2.
|
|
165
|
+
"LOW": "MiniMax-M2.7",
|
|
166
|
+
"MID": "MiniMax-M2.7",
|
|
167
|
+
"HIGH": "MiniMax-M2.7",
|
|
168
168
|
},
|
|
169
169
|
),
|
|
170
170
|
ProviderPreset(
|
|
@@ -30,6 +30,7 @@ class StatusBar:
|
|
|
30
30
|
self._git_diff_stats: GitDiffStats | None = None
|
|
31
31
|
self._git_diff_cache_time: float = 0.0
|
|
32
32
|
self._transient_message: str | None = None
|
|
33
|
+
self._transient_severity: str = "info"
|
|
33
34
|
self._transient_until: float | None = None
|
|
34
35
|
|
|
35
36
|
@staticmethod
|
|
@@ -258,9 +259,11 @@ class StatusBar:
|
|
|
258
259
|
fragments.append(("", " "))
|
|
259
260
|
return fragments
|
|
260
261
|
|
|
261
|
-
def show_transient(self, message: str, duration_s: float = 5.0
|
|
262
|
+
def show_transient(self, message: str, duration_s: float = 5.0,
|
|
263
|
+
severity: str = "info") -> None:
|
|
262
264
|
"""Set a transient message that auto-clears after *duration_s* seconds."""
|
|
263
265
|
self._transient_message = message
|
|
266
|
+
self._transient_severity = severity
|
|
264
267
|
self._transient_until = time.monotonic() + duration_s
|
|
265
268
|
|
|
266
269
|
def clear_transient_if_expired(self) -> bool:
|
|
@@ -271,6 +274,7 @@ class StatusBar:
|
|
|
271
274
|
"""
|
|
272
275
|
if self._transient_until is not None and time.monotonic() >= self._transient_until:
|
|
273
276
|
self._transient_message = None
|
|
277
|
+
self._transient_severity = "info"
|
|
274
278
|
self._transient_until = None
|
|
275
279
|
return True
|
|
276
280
|
return False
|
|
@@ -283,5 +287,9 @@ class StatusBar:
|
|
|
283
287
|
def has_transient(self) -> bool:
|
|
284
288
|
return self._transient_message is not None
|
|
285
289
|
|
|
290
|
+
@property
|
|
291
|
+
def transient_severity(self) -> str:
|
|
292
|
+
return self._transient_severity if self._transient_message else "info"
|
|
293
|
+
|
|
286
294
|
def helper_toolbar(self) -> list[tuple[str, str]]:
|
|
287
295
|
return []
|
|
@@ -47,6 +47,7 @@ from comate_cli.terminal_agent.animations import (
|
|
|
47
47
|
from comate_cli.terminal_agent.env_utils import read_env_float, read_env_int
|
|
48
48
|
from comate_cli.terminal_agent.event_renderer import EventRenderer
|
|
49
49
|
from comate_cli.terminal_agent.mention_completer import LocalFileMentionCompleter
|
|
50
|
+
from comate_cli.terminal_agent.path_context_hint import build_path_context_hint
|
|
50
51
|
from comate_cli.terminal_agent.custom_slash_commands import (
|
|
51
52
|
CustomSlashCommand,
|
|
52
53
|
discover_custom_slash_commands,
|
|
@@ -450,10 +451,18 @@ class TerminalAgentTUI(
|
|
|
450
451
|
|
|
451
452
|
self._main_container = HSplit(
|
|
452
453
|
[
|
|
453
|
-
self._todo_container,
|
|
454
454
|
self._loading_container,
|
|
455
|
+
ConditionalContainer(
|
|
456
|
+
content=Window(height=1, style="class:loading"),
|
|
457
|
+
filter=Condition(
|
|
458
|
+
lambda: self._renderer.has_running_tools()
|
|
459
|
+
and self._renderer.has_active_todos()
|
|
460
|
+
),
|
|
461
|
+
),
|
|
462
|
+
self._todo_container,
|
|
455
463
|
Window(height=1, style="class:loading"),
|
|
456
464
|
self._diff_panel_container,
|
|
465
|
+
Window(height=1, style="class:input.separator"),
|
|
457
466
|
self._queue_container,
|
|
458
467
|
Window(height=1, char="─", style="class:input.separator"),
|
|
459
468
|
self._input_container,
|
|
@@ -485,6 +494,8 @@ class TerminalAgentTUI(
|
|
|
485
494
|
"status.mode.plan": "bg:default #7AC9CA bold",
|
|
486
495
|
"status.hint": "bg:default #6B7280",
|
|
487
496
|
"status.transient": "bg:default italic fg:ansiyellow",
|
|
497
|
+
"status.transient.error": "bg:default bold fg:ansired",
|
|
498
|
+
"status.transient.warning": "bg:default italic fg:ansiyellow",
|
|
488
499
|
"input.placeholder": "bg:default #9CA3AF",
|
|
489
500
|
"auto-suggestion": "bg:default #94a3b8",
|
|
490
501
|
"queue": "bg:#1d222a #d8dee9",
|
|
@@ -732,6 +743,24 @@ class TerminalAgentTUI(
|
|
|
732
743
|
self._render_dirty = True
|
|
733
744
|
self._invalidate()
|
|
734
745
|
|
|
746
|
+
async def _handle_error(self, exc: Exception) -> None:
|
|
747
|
+
"""Unified error cleanup: format → display → stop animation → reset state."""
|
|
748
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
749
|
+
|
|
750
|
+
message, transient_summary, severity = format_error(exc)
|
|
751
|
+
|
|
752
|
+
self._renderer.append_system_message(message, severity=severity)
|
|
753
|
+
self._renderer.interrupt_turn()
|
|
754
|
+
await self._animation_controller.shutdown()
|
|
755
|
+
self._status_bar.show_transient(transient_summary, severity=severity)
|
|
756
|
+
self._last_turn_user_preview = None
|
|
757
|
+
self._append_run_elapsed_to_history(stop_reason="error")
|
|
758
|
+
self._run_start_time = None
|
|
759
|
+
self._interrupt_requested_at = None
|
|
760
|
+
self._set_busy(False)
|
|
761
|
+
await self._status_bar.refresh()
|
|
762
|
+
self._refresh_layers()
|
|
763
|
+
|
|
735
764
|
async def _submit_user_message(
|
|
736
765
|
self,
|
|
737
766
|
text: str,
|
|
@@ -771,25 +800,20 @@ class TerminalAgentTUI(
|
|
|
771
800
|
|
|
772
801
|
# 启动提交动画
|
|
773
802
|
await self._animation_controller.start()
|
|
803
|
+
|
|
804
|
+
# @path context hint injection
|
|
805
|
+
try:
|
|
806
|
+
hint = build_path_context_hint(text, Path.cwd())
|
|
807
|
+
if hint is not None:
|
|
808
|
+
self._session._agent._context.add_system_reminder_message(hint)
|
|
809
|
+
except Exception:
|
|
810
|
+
logger.debug("path context hint injection failed", exc_info=True)
|
|
811
|
+
|
|
774
812
|
try:
|
|
775
813
|
await self._session.send(text)
|
|
776
814
|
except Exception as exc:
|
|
777
815
|
logger.exception("send failed")
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
error_msg, suggestion = format_error(exc)
|
|
781
|
-
self._renderer.append_system_message(error_msg, severity="error")
|
|
782
|
-
if suggestion:
|
|
783
|
-
self._renderer.append_system_message(f"💡 {suggestion}")
|
|
784
|
-
self._renderer.interrupt_turn()
|
|
785
|
-
await self._animation_controller.shutdown()
|
|
786
|
-
self._last_turn_user_preview = None
|
|
787
|
-
self._append_run_elapsed_to_history(stop_reason="error")
|
|
788
|
-
self._run_start_time = None
|
|
789
|
-
self._interrupt_requested_at = None
|
|
790
|
-
self._set_busy(False)
|
|
791
|
-
await self._status_bar.refresh()
|
|
792
|
-
self._refresh_layers()
|
|
816
|
+
await self._handle_error(exc)
|
|
793
817
|
|
|
794
818
|
async def _consume_event_stream(self) -> None:
|
|
795
819
|
waiting_for_input = False
|
|
@@ -899,12 +923,7 @@ class TerminalAgentTUI(
|
|
|
899
923
|
raise
|
|
900
924
|
except Exception as exc:
|
|
901
925
|
logger.exception("session event pump failed")
|
|
902
|
-
self.
|
|
903
|
-
f"会话事件流异常终止: {exc}",
|
|
904
|
-
severity="error",
|
|
905
|
-
)
|
|
906
|
-
self._set_busy(False)
|
|
907
|
-
self._refresh_layers()
|
|
926
|
+
await self._handle_error(exc)
|
|
908
927
|
|
|
909
928
|
def _open_plan_approval_menu(self, approval: dict[str, str]) -> None:
|
|
910
929
|
plan_path = str(approval.get("plan_path", "")).strip()
|
|
@@ -322,7 +322,7 @@ class InputBehaviorMixin:
|
|
|
322
322
|
# busy 或 initializing 时:非斜杠命令 → 入队,斜杠命令 → 交由命令分发决定
|
|
323
323
|
is_busy = self._busy or self._initializing
|
|
324
324
|
if is_busy:
|
|
325
|
-
if
|
|
325
|
+
if raw_text.startswith("/"):
|
|
326
326
|
normalized_slash = display_text.strip()
|
|
327
327
|
parsed = parse_slash_command_call(normalized_slash)
|
|
328
328
|
registry = getattr(self, "_slash_registry", None)
|
|
@@ -351,7 +351,7 @@ class InputBehaviorMixin:
|
|
|
351
351
|
self._schedule_background(self._execute_command(normalized_slash))
|
|
352
352
|
return
|
|
353
353
|
|
|
354
|
-
if not
|
|
354
|
+
if not raw_text.startswith("/"):
|
|
355
355
|
queue_size = len(self._queued_messages)
|
|
356
356
|
if queue_size >= int(self._message_queue_max_size):
|
|
357
357
|
self._renderer.append_system_message(
|
|
@@ -364,7 +364,7 @@ class InputBehaviorMixin:
|
|
|
364
364
|
self._invalidate()
|
|
365
365
|
return
|
|
366
366
|
|
|
367
|
-
if
|
|
367
|
+
if raw_text.startswith("/"):
|
|
368
368
|
self._schedule_background(self._execute_command(display_text.strip()))
|
|
369
369
|
return
|
|
370
370
|
|
|
@@ -67,7 +67,13 @@ class RenderPanelsMixin:
|
|
|
67
67
|
padding = max(1, width - left_w - right_w - 2)
|
|
68
68
|
|
|
69
69
|
frags: list[tuple[str, str]] = [
|
|
70
|
-
(
|
|
70
|
+
(
|
|
71
|
+
{
|
|
72
|
+
"error": "class:status.transient.error",
|
|
73
|
+
"warning": "class:status.transient.warning",
|
|
74
|
+
}.get(self._status_bar.transient_severity, "class:status.transient"),
|
|
75
|
+
transient,
|
|
76
|
+
),
|
|
71
77
|
("class:status", " " * padding),
|
|
72
78
|
("class:status", right_text),
|
|
73
79
|
]
|