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.
Files changed (107) hide show
  1. {comate_cli-0.3.2 → comate_cli-0.3.4}/PKG-INFO +1 -1
  2. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/app.py +7 -6
  3. comate_cli-0.3.4/comate_cli/terminal_agent/codenames.py +15 -0
  4. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/custom_slash_commands.py +2 -2
  5. comate_cli-0.3.4/comate_cli/terminal_agent/error_display.py +83 -0
  6. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/event_renderer.py +28 -0
  7. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/history_printer.py +13 -9
  8. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/logo.py +10 -0
  9. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/models.py +1 -1
  10. comate_cli-0.3.4/comate_cli/terminal_agent/path_context_hint.py +88 -0
  11. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/preflight.py +3 -3
  12. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/status_bar.py +9 -1
  13. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui.py +41 -22
  14. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/input_behavior.py +3 -3
  15. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/render_panels.py +7 -1
  16. {comate_cli-0.3.2 → comate_cli-0.3.4}/pyproject.toml +1 -1
  17. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_event_renderer.py +99 -0
  18. comate_cli-0.3.4/tests/test_format_error.py +153 -0
  19. comate_cli-0.3.4/tests/test_handle_error.py +115 -0
  20. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_history_printer.py +17 -0
  21. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_interrupt_exit_semantics.py +5 -0
  22. comate_cli-0.3.4/tests/test_path_context_hint.py +106 -0
  23. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_status_bar_transient.py +27 -0
  24. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_tui_mcp_init_gate.py +5 -1
  25. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_tui_paste_placeholder.py +52 -0
  26. comate_cli-0.3.4/uv.lock +2215 -0
  27. comate_cli-0.3.2/comate_cli/terminal_agent/error_display.py +0 -46
  28. comate_cli-0.3.2/uv.lock +0 -2231
  29. {comate_cli-0.3.2 → comate_cli-0.3.4}/.gitignore +0 -0
  30. {comate_cli-0.3.2 → comate_cli-0.3.4}/README.md +0 -0
  31. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/__init__.py +0 -0
  32. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/__main__.py +0 -0
  33. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/main.py +0 -0
  34. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/mcp_cli.py +0 -0
  35. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/__init__.py +0 -0
  36. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/animations.py +0 -0
  37. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/assistant_render.py +0 -0
  38. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/env_utils.py +0 -0
  39. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  40. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/input_geometry.py +0 -0
  41. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  42. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/logging_adapter.py +0 -0
  43. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/markdown_render.py +0 -0
  44. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/mention_completer.py +0 -0
  45. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/message_style.py +0 -0
  46. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/question_view.py +0 -0
  47. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/resume_selector.py +0 -0
  48. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/rewind_store.py +0 -0
  49. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  50. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  51. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/selection_menu.py +0 -0
  52. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/session_view.py +0 -0
  53. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/slash_commands.py +0 -0
  54. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/startup.py +0 -0
  55. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/text_effects.py +0 -0
  56. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tips.py +0 -0
  57. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tool_view.py +0 -0
  58. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  59. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
  60. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
  61. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
  62. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  63. {comate_cli-0.3.2 → comate_cli-0.3.4}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  64. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/conftest.py +0 -0
  65. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_app_mcp_preload.py +0 -0
  66. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_app_preflight_gate.py +0 -0
  67. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_app_print_mode.py +0 -0
  68. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_app_shutdown.py +0 -0
  69. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_app_usage_line.py +0 -0
  70. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_cli_project_root.py +0 -0
  71. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_compact_command_semantics.py +0 -0
  72. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_completion_context_activation.py +0 -0
  73. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_completion_status_panel.py +0 -0
  74. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_context_command.py +0 -0
  75. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_custom_slash_commands.py +0 -0
  76. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_history_sync.py +0 -0
  77. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_input_behavior.py +0 -0
  78. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_input_history.py +0 -0
  79. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_layout_coordinator.py +0 -0
  80. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_logging_adapter.py +0 -0
  81. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_logo.py +0 -0
  82. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_main_args.py +0 -0
  83. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_mcp_cli.py +0 -0
  84. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_mcp_slash_command.py +0 -0
  85. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_mention_completer.py +0 -0
  86. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_preflight.py +0 -0
  87. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_preflight_copilot.py +0 -0
  88. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_question_key_bindings.py +0 -0
  89. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_question_view.py +0 -0
  90. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_resume_selector.py +0 -0
  91. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_rewind_command_semantics.py +0 -0
  92. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_rewind_store.py +0 -0
  93. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_rpc_protocol.py +0 -0
  94. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_rpc_stdio_bridge.py +0 -0
  95. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_selection_menu.py +0 -0
  96. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_skills_slash_command.py +0 -0
  97. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_slash_argument_hint.py +0 -0
  98. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_slash_completer.py +0 -0
  99. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_slash_registry.py +0 -0
  100. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_status_bar.py +0 -0
  101. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_task_panel_format.py +0 -0
  102. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_task_panel_key_bindings.py +0 -0
  103. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_task_panel_rendering.py +0 -0
  104. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_task_poll.py +0 -0
  105. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_tool_view.py +0 -0
  106. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_tui_elapsed_status.py +0 -0
  107. {comate_cli-0.3.2 → comate_cli-0.3.4}/tests/test_tui_split_invariance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comate-cli
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Comate terminal CLI built on comate-agent-sdk
5
5
  Project-URL: Homepage, https://github.com/AndyLee1024/agent-sdk
6
6
  Project-URL: Repository, https://github.com/AndyLee1024/agent-sdk
@@ -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
- _FILE_REF_PATTERN = re.compile(r"(?<!\S)@([^\s]+)")
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 _FILE_REF_PATTERN.finditer(text):
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
- prefix_char = ""
89
- prefix_style = "#FF9FC6"
88
+ text_style = "bold #FF6B6B"
90
89
  elif entry.severity == "warning":
91
- prefix_char = ""
92
- prefix_style = "#B8B630"
90
+ text_style = "#E8B830"
93
91
  else:
94
- prefix_char = ""
95
- prefix_style = "dim"
92
+ text_style = "dim"
96
93
  line_text = Text()
97
- line_text.append(f"{prefix_char} ", style=prefix_style)
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.5",
166
- "MID": "MiniMax-M2.5",
167
- "HIGH": "MiniMax-M2.5",
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) -> None:
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
- from comate_cli.terminal_agent.error_display import format_error
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._renderer.append_system_message(
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 display_text.lstrip().startswith("/"):
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 display_text.lstrip().startswith("/"):
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 display_text.lstrip().startswith("/"):
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
- ("class:status.transient", transient),
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
  ]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "comate-cli"
7
- version = "0.3.2"
7
+ version = "0.3.4"
8
8
  description = "Comate terminal CLI built on comate-agent-sdk"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"