comate-cli 0.3.2__tar.gz → 0.3.3__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.3}/PKG-INFO +1 -1
- comate_cli-0.3.3/comate_cli/terminal_agent/codenames.py +14 -0
- comate_cli-0.3.3/comate_cli/terminal_agent/error_display.py +83 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/history_printer.py +4 -8
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/logo.py +10 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/status_bar.py +9 -1
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/tui.py +22 -21
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/tui_parts/input_behavior.py +3 -3
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/tui_parts/render_panels.py +7 -1
- {comate_cli-0.3.2 → comate_cli-0.3.3}/pyproject.toml +1 -1
- comate_cli-0.3.3/tests/test_format_error.py +153 -0
- comate_cli-0.3.3/tests/test_handle_error.py +115 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_interrupt_exit_semantics.py +5 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_status_bar_transient.py +27 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_tui_mcp_init_gate.py +5 -1
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_tui_paste_placeholder.py +52 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/uv.lock +2 -2
- comate_cli-0.3.2/comate_cli/terminal_agent/error_display.py +0 -46
- {comate_cli-0.3.2 → comate_cli-0.3.3}/.gitignore +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/README.md +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/__init__.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/__main__.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/main.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/app.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/event_renderer.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/rewind_store.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/session_view.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/tool_view.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/conftest.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_completion_status_panel.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_context_command.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_event_renderer.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_history_printer.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_history_sync.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_input_history.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_logo.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_main_args.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_preflight.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_question_view.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_rewind_store.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_status_bar.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_task_poll.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_tool_view.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.3.2 → comate_cli-0.3.3}/tests/test_tui_split_invariance.py +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Version codenames — one entry per memorable release."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
CODENAMES: dict[str, str] = {
|
|
6
|
+
"0.3.3": "Update for My Brothers",
|
|
7
|
+
"0.3.2": "We are with Ukraine",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_codename(ver: str) -> str | None:
|
|
12
|
+
"""Return codename for a version string like '0.3.2' or 'v0.3.2'."""
|
|
13
|
+
stripped = ver.lstrip("v")
|
|
14
|
+
return CODENAMES.get(stripped)
|
|
@@ -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
|
|
@@ -85,17 +85,13 @@ 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
|
|
@@ -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
|
|
|
@@ -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 []
|
|
@@ -485,6 +485,8 @@ class TerminalAgentTUI(
|
|
|
485
485
|
"status.mode.plan": "bg:default #7AC9CA bold",
|
|
486
486
|
"status.hint": "bg:default #6B7280",
|
|
487
487
|
"status.transient": "bg:default italic fg:ansiyellow",
|
|
488
|
+
"status.transient.error": "bg:default bold fg:ansired",
|
|
489
|
+
"status.transient.warning": "bg:default italic fg:ansiyellow",
|
|
488
490
|
"input.placeholder": "bg:default #9CA3AF",
|
|
489
491
|
"auto-suggestion": "bg:default #94a3b8",
|
|
490
492
|
"queue": "bg:#1d222a #d8dee9",
|
|
@@ -732,6 +734,24 @@ class TerminalAgentTUI(
|
|
|
732
734
|
self._render_dirty = True
|
|
733
735
|
self._invalidate()
|
|
734
736
|
|
|
737
|
+
async def _handle_error(self, exc: Exception) -> None:
|
|
738
|
+
"""Unified error cleanup: format → display → stop animation → reset state."""
|
|
739
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
740
|
+
|
|
741
|
+
message, transient_summary, severity = format_error(exc)
|
|
742
|
+
|
|
743
|
+
self._renderer.append_system_message(message, severity=severity)
|
|
744
|
+
self._renderer.interrupt_turn()
|
|
745
|
+
await self._animation_controller.shutdown()
|
|
746
|
+
self._status_bar.show_transient(transient_summary, severity=severity)
|
|
747
|
+
self._last_turn_user_preview = None
|
|
748
|
+
self._append_run_elapsed_to_history(stop_reason="error")
|
|
749
|
+
self._run_start_time = None
|
|
750
|
+
self._interrupt_requested_at = None
|
|
751
|
+
self._set_busy(False)
|
|
752
|
+
await self._status_bar.refresh()
|
|
753
|
+
self._refresh_layers()
|
|
754
|
+
|
|
735
755
|
async def _submit_user_message(
|
|
736
756
|
self,
|
|
737
757
|
text: str,
|
|
@@ -775,21 +795,7 @@ class TerminalAgentTUI(
|
|
|
775
795
|
await self._session.send(text)
|
|
776
796
|
except Exception as exc:
|
|
777
797
|
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()
|
|
798
|
+
await self._handle_error(exc)
|
|
793
799
|
|
|
794
800
|
async def _consume_event_stream(self) -> None:
|
|
795
801
|
waiting_for_input = False
|
|
@@ -899,12 +905,7 @@ class TerminalAgentTUI(
|
|
|
899
905
|
raise
|
|
900
906
|
except Exception as exc:
|
|
901
907
|
logger.exception("session event pump failed")
|
|
902
|
-
self.
|
|
903
|
-
f"会话事件流异常终止: {exc}",
|
|
904
|
-
severity="error",
|
|
905
|
-
)
|
|
906
|
-
self._set_busy(False)
|
|
907
|
-
self._refresh_layers()
|
|
908
|
+
await self._handle_error(exc)
|
|
908
909
|
|
|
909
910
|
def _open_plan_approval_menu(self, approval: dict[str, str]) -> None:
|
|
910
911
|
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
|
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Tests for unified error formatter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestFormatError(unittest.TestCase):
|
|
8
|
+
"""Each test verifies (message, transient_summary, severity) for one error type."""
|
|
9
|
+
|
|
10
|
+
def _make_exc(self, cls_name: str, msg: str = "", **attrs):
|
|
11
|
+
"""Create a fake exception with a given class name and attributes."""
|
|
12
|
+
exc = type(cls_name, (Exception,), {})(msg)
|
|
13
|
+
for k, v in attrs.items():
|
|
14
|
+
setattr(exc, k, v)
|
|
15
|
+
return exc
|
|
16
|
+
|
|
17
|
+
def test_rate_limit(self):
|
|
18
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
19
|
+
|
|
20
|
+
exc = self._make_exc("ModelRateLimitError")
|
|
21
|
+
msg, transient, severity = format_error(exc)
|
|
22
|
+
assert severity == "warning"
|
|
23
|
+
assert "Rate limit" in msg
|
|
24
|
+
assert transient == "Rate limited"
|
|
25
|
+
|
|
26
|
+
def test_provider_401(self):
|
|
27
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
28
|
+
|
|
29
|
+
exc = self._make_exc("ModelProviderError", status_code=401)
|
|
30
|
+
msg, transient, severity = format_error(exc)
|
|
31
|
+
assert severity == "error"
|
|
32
|
+
assert "API key" in msg
|
|
33
|
+
assert transient == "Auth failed"
|
|
34
|
+
|
|
35
|
+
def test_provider_403(self):
|
|
36
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
37
|
+
|
|
38
|
+
exc = self._make_exc("ModelProviderError", status_code=403)
|
|
39
|
+
msg, transient, severity = format_error(exc)
|
|
40
|
+
assert severity == "error"
|
|
41
|
+
assert "Access denied" in msg
|
|
42
|
+
assert transient == "Access denied"
|
|
43
|
+
|
|
44
|
+
def test_provider_404(self):
|
|
45
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
46
|
+
|
|
47
|
+
exc = self._make_exc("ModelProviderError", status_code=404)
|
|
48
|
+
msg, transient, severity = format_error(exc)
|
|
49
|
+
assert severity == "error"
|
|
50
|
+
assert "not found" in msg.lower()
|
|
51
|
+
assert transient == "Model not found"
|
|
52
|
+
|
|
53
|
+
def test_provider_5xx(self):
|
|
54
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
55
|
+
|
|
56
|
+
exc = self._make_exc("ModelProviderError", status_code=502)
|
|
57
|
+
msg, transient, severity = format_error(exc)
|
|
58
|
+
assert severity == "warning"
|
|
59
|
+
assert "502" in msg
|
|
60
|
+
assert transient == "Server error"
|
|
61
|
+
|
|
62
|
+
def test_context_size_exceeded(self):
|
|
63
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
64
|
+
|
|
65
|
+
body = (
|
|
66
|
+
"Error code: 400 - {'error': {'code': 400, "
|
|
67
|
+
"'message': 'request (35099 tokens) exceeds the available context size (32768 tokens)', "
|
|
68
|
+
"'type': 'exceed_context_size_error', 'n_prompt_tokens': 35099, 'n_ctx': 32768}}"
|
|
69
|
+
)
|
|
70
|
+
exc = self._make_exc("ModelProviderError", body, status_code=400)
|
|
71
|
+
msg, transient, severity = format_error(exc)
|
|
72
|
+
assert severity == "error"
|
|
73
|
+
assert "35099" in msg
|
|
74
|
+
assert "32768" in msg
|
|
75
|
+
assert transient == "Context limit exceeded"
|
|
76
|
+
|
|
77
|
+
def test_context_size_type_without_parseable_numbers(self):
|
|
78
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
79
|
+
|
|
80
|
+
exc = self._make_exc(
|
|
81
|
+
"ModelProviderError",
|
|
82
|
+
"exceed_context_size_error without parseable numbers",
|
|
83
|
+
status_code=400,
|
|
84
|
+
)
|
|
85
|
+
msg, transient, severity = format_error(exc)
|
|
86
|
+
assert severity == "error"
|
|
87
|
+
assert transient == "API error" # Falls through to generic 400
|
|
88
|
+
|
|
89
|
+
def test_provider_400_generic(self):
|
|
90
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
91
|
+
|
|
92
|
+
exc = self._make_exc("ModelProviderError", "bad request", status_code=400)
|
|
93
|
+
msg, transient, severity = format_error(exc)
|
|
94
|
+
assert severity == "error"
|
|
95
|
+
assert "API error" in msg
|
|
96
|
+
assert transient == "API error"
|
|
97
|
+
|
|
98
|
+
def test_session_closed(self):
|
|
99
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
100
|
+
|
|
101
|
+
exc = self._make_exc("ChatSessionClosedError")
|
|
102
|
+
msg, transient, severity = format_error(exc)
|
|
103
|
+
assert severity == "error"
|
|
104
|
+
assert "Session closed" in msg
|
|
105
|
+
assert transient == "Session closed"
|
|
106
|
+
|
|
107
|
+
def test_timeout(self):
|
|
108
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
109
|
+
|
|
110
|
+
exc = Exception("request timed out after 30s")
|
|
111
|
+
msg, transient, severity = format_error(exc)
|
|
112
|
+
assert severity == "warning"
|
|
113
|
+
assert "timed out" in msg.lower()
|
|
114
|
+
assert transient == "Timed out"
|
|
115
|
+
|
|
116
|
+
def test_connection_error(self):
|
|
117
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
118
|
+
|
|
119
|
+
exc = Exception("Connection refused")
|
|
120
|
+
msg, transient, severity = format_error(exc)
|
|
121
|
+
assert severity == "error"
|
|
122
|
+
assert "Connection failed" in msg
|
|
123
|
+
assert transient == "Connection failed"
|
|
124
|
+
|
|
125
|
+
def test_generic_fallback(self):
|
|
126
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
127
|
+
|
|
128
|
+
exc = Exception("something unexpected")
|
|
129
|
+
msg, transient, severity = format_error(exc)
|
|
130
|
+
assert severity == "error"
|
|
131
|
+
assert "something unexpected" in msg
|
|
132
|
+
assert transient == "Error occurred"
|
|
133
|
+
|
|
134
|
+
def test_no_icons_in_messages(self):
|
|
135
|
+
"""All messages must be plain text, no emoji icons."""
|
|
136
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
137
|
+
|
|
138
|
+
test_cases = [
|
|
139
|
+
self._make_exc("ModelRateLimitError"),
|
|
140
|
+
self._make_exc("ModelProviderError", status_code=401),
|
|
141
|
+
self._make_exc("ChatSessionClosedError"),
|
|
142
|
+
Exception("timeout"),
|
|
143
|
+
Exception("generic error"),
|
|
144
|
+
]
|
|
145
|
+
for exc in test_cases:
|
|
146
|
+
msg, transient, severity = format_error(exc)
|
|
147
|
+
assert "⚠" not in msg, f"Icon found in: {msg}"
|
|
148
|
+
assert "✖" not in msg, f"Icon found in: {msg}"
|
|
149
|
+
assert "💡" not in msg, f"Icon found in: {msg}"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
unittest.main(verbosity=2)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Tests for unified _handle_error() in TUI."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import unittest
|
|
6
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _make_tui_stub():
|
|
10
|
+
"""Create a minimal TUI stub with mocked dependencies."""
|
|
11
|
+
tui = MagicMock()
|
|
12
|
+
tui._renderer = MagicMock()
|
|
13
|
+
tui._animation_controller = AsyncMock()
|
|
14
|
+
tui._status_bar = MagicMock()
|
|
15
|
+
tui._status_bar.refresh = AsyncMock()
|
|
16
|
+
tui._last_turn_user_preview = "some preview"
|
|
17
|
+
tui._run_start_time = 12345.0
|
|
18
|
+
tui._interrupt_requested_at = 67890.0
|
|
19
|
+
return tui
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestHandleError(unittest.TestCase):
|
|
23
|
+
def test_handle_error_stops_animation(self):
|
|
24
|
+
"""Regression: animation must be stopped on any error."""
|
|
25
|
+
from comate_cli.terminal_agent.tui import TerminalAgentTUI
|
|
26
|
+
|
|
27
|
+
tui = _make_tui_stub()
|
|
28
|
+
exc = Exception("test error")
|
|
29
|
+
|
|
30
|
+
asyncio.run(TerminalAgentTUI._handle_error(tui, exc))
|
|
31
|
+
|
|
32
|
+
tui._animation_controller.shutdown.assert_awaited_once()
|
|
33
|
+
|
|
34
|
+
def test_handle_error_sets_busy_false(self):
|
|
35
|
+
from comate_cli.terminal_agent.tui import TerminalAgentTUI
|
|
36
|
+
|
|
37
|
+
tui = _make_tui_stub()
|
|
38
|
+
exc = Exception("test error")
|
|
39
|
+
|
|
40
|
+
asyncio.run(TerminalAgentTUI._handle_error(tui, exc))
|
|
41
|
+
|
|
42
|
+
tui._set_busy.assert_called_once_with(False)
|
|
43
|
+
|
|
44
|
+
def test_handle_error_writes_to_scrollback(self):
|
|
45
|
+
from comate_cli.terminal_agent.tui import TerminalAgentTUI
|
|
46
|
+
|
|
47
|
+
tui = _make_tui_stub()
|
|
48
|
+
exc = Exception("something broke")
|
|
49
|
+
|
|
50
|
+
asyncio.run(TerminalAgentTUI._handle_error(tui, exc))
|
|
51
|
+
|
|
52
|
+
tui._renderer.append_system_message.assert_called_once()
|
|
53
|
+
call_args = tui._renderer.append_system_message.call_args
|
|
54
|
+
assert "something broke" in call_args[0][0]
|
|
55
|
+
assert call_args[1]["severity"] == "error"
|
|
56
|
+
|
|
57
|
+
def test_handle_error_sets_transient_with_severity(self):
|
|
58
|
+
from comate_cli.terminal_agent.tui import TerminalAgentTUI
|
|
59
|
+
|
|
60
|
+
tui = _make_tui_stub()
|
|
61
|
+
exc = Exception("something broke")
|
|
62
|
+
|
|
63
|
+
asyncio.run(TerminalAgentTUI._handle_error(tui, exc))
|
|
64
|
+
|
|
65
|
+
tui._status_bar.show_transient.assert_called_once()
|
|
66
|
+
call_args = tui._status_bar.show_transient.call_args
|
|
67
|
+
assert call_args[1]["severity"] == "error"
|
|
68
|
+
|
|
69
|
+
def test_handle_error_resets_run_state(self):
|
|
70
|
+
from comate_cli.terminal_agent.tui import TerminalAgentTUI
|
|
71
|
+
|
|
72
|
+
tui = _make_tui_stub()
|
|
73
|
+
exc = Exception("test")
|
|
74
|
+
|
|
75
|
+
asyncio.run(TerminalAgentTUI._handle_error(tui, exc))
|
|
76
|
+
|
|
77
|
+
assert tui._last_turn_user_preview is None
|
|
78
|
+
assert tui._run_start_time is None
|
|
79
|
+
assert tui._interrupt_requested_at is None
|
|
80
|
+
|
|
81
|
+
def test_handle_error_calls_interrupt_turn(self):
|
|
82
|
+
from comate_cli.terminal_agent.tui import TerminalAgentTUI
|
|
83
|
+
|
|
84
|
+
tui = _make_tui_stub()
|
|
85
|
+
exc = Exception("test")
|
|
86
|
+
|
|
87
|
+
asyncio.run(TerminalAgentTUI._handle_error(tui, exc))
|
|
88
|
+
|
|
89
|
+
tui._renderer.interrupt_turn.assert_called_once()
|
|
90
|
+
|
|
91
|
+
def test_handle_error_refreshes_layers(self):
|
|
92
|
+
from comate_cli.terminal_agent.tui import TerminalAgentTUI
|
|
93
|
+
|
|
94
|
+
tui = _make_tui_stub()
|
|
95
|
+
exc = Exception("test")
|
|
96
|
+
|
|
97
|
+
asyncio.run(TerminalAgentTUI._handle_error(tui, exc))
|
|
98
|
+
|
|
99
|
+
tui._refresh_layers.assert_called_once()
|
|
100
|
+
|
|
101
|
+
def test_handle_error_warning_severity_for_rate_limit(self):
|
|
102
|
+
"""Rate limit errors should propagate warning severity to transient."""
|
|
103
|
+
from comate_cli.terminal_agent.tui import TerminalAgentTUI
|
|
104
|
+
|
|
105
|
+
tui = _make_tui_stub()
|
|
106
|
+
exc = type("ModelRateLimitError", (Exception,), {})("rate limited")
|
|
107
|
+
|
|
108
|
+
asyncio.run(TerminalAgentTUI._handle_error(tui, exc))
|
|
109
|
+
|
|
110
|
+
call_args = tui._status_bar.show_transient.call_args
|
|
111
|
+
assert call_args[1]["severity"] == "warning"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
unittest.main(verbosity=2)
|
|
@@ -109,6 +109,9 @@ class _StubStatusBar:
|
|
|
109
109
|
def set_mode(self, _mode: str) -> None:
|
|
110
110
|
return None
|
|
111
111
|
|
|
112
|
+
def show_transient(self, _message: str, *, severity: str = "error") -> None:
|
|
113
|
+
return None
|
|
114
|
+
|
|
112
115
|
|
|
113
116
|
class _StubKeyBindingHost(KeyBindingsMixin):
|
|
114
117
|
def __init__(self) -> None:
|
|
@@ -285,6 +288,8 @@ class TestInterruptExitSemantics(unittest.IsolatedAsyncioTestCase):
|
|
|
285
288
|
tui._open_plan_approval_menu = lambda approval: None
|
|
286
289
|
tui._enter_question_mode = lambda questions: None
|
|
287
290
|
tui._exit_question_mode = lambda: None
|
|
291
|
+
tui._queued_messages = []
|
|
292
|
+
tui._append_run_elapsed_to_history = lambda stop_reason=None: None
|
|
288
293
|
|
|
289
294
|
await tui._consume_event_stream()
|
|
290
295
|
|
|
@@ -56,6 +56,33 @@ class TestStatusBarTransient(unittest.TestCase):
|
|
|
56
56
|
sb.show_transient("second", duration_s=10.0)
|
|
57
57
|
assert sb.transient_message == "second"
|
|
58
58
|
|
|
59
|
+
def test_show_transient_default_severity_is_info(self) -> None:
|
|
60
|
+
sb = _make_status_bar()
|
|
61
|
+
sb.show_transient("hello")
|
|
62
|
+
assert sb.transient_severity == "info"
|
|
63
|
+
|
|
64
|
+
def test_show_transient_with_error_severity(self) -> None:
|
|
65
|
+
sb = _make_status_bar()
|
|
66
|
+
sb.show_transient("fail", severity="error")
|
|
67
|
+
assert sb.transient_message == "fail"
|
|
68
|
+
assert sb.transient_severity == "error"
|
|
69
|
+
|
|
70
|
+
def test_show_transient_with_warning_severity(self) -> None:
|
|
71
|
+
sb = _make_status_bar()
|
|
72
|
+
sb.show_transient("warn", severity="warning")
|
|
73
|
+
assert sb.transient_severity == "warning"
|
|
74
|
+
|
|
75
|
+
def test_transient_severity_returns_info_when_no_message(self) -> None:
|
|
76
|
+
sb = _make_status_bar()
|
|
77
|
+
assert sb.transient_severity == "info"
|
|
78
|
+
|
|
79
|
+
def test_clear_transient_resets_severity(self) -> None:
|
|
80
|
+
sb = _make_status_bar()
|
|
81
|
+
sb.show_transient("err", duration_s=0.0, severity="error")
|
|
82
|
+
import time; time.sleep(0.01)
|
|
83
|
+
sb.clear_transient_if_expired()
|
|
84
|
+
assert sb.transient_severity == "info"
|
|
85
|
+
|
|
59
86
|
|
|
60
87
|
if __name__ == "__main__":
|
|
61
88
|
unittest.main(verbosity=2)
|
|
@@ -55,13 +55,17 @@ class TestTUIMcpInitGate(unittest.IsolatedAsyncioTestCase):
|
|
|
55
55
|
class _FakeStatusBar:
|
|
56
56
|
def __init__(self) -> None:
|
|
57
57
|
self.transient_calls: list[tuple[str, float]] = []
|
|
58
|
-
def show_transient(self, msg: str, duration_s: float = 5.0
|
|
58
|
+
def show_transient(self, msg: str, duration_s: float = 5.0,
|
|
59
|
+
severity: str = "info") -> None:
|
|
59
60
|
self.transient_calls.append((msg, duration_s))
|
|
60
61
|
def clear_transient_if_expired(self) -> bool:
|
|
61
62
|
return False
|
|
62
63
|
@property
|
|
63
64
|
def has_transient(self) -> bool:
|
|
64
65
|
return False
|
|
66
|
+
@property
|
|
67
|
+
def transient_severity(self) -> str:
|
|
68
|
+
return "info"
|
|
65
69
|
|
|
66
70
|
fake_status_bar = _FakeStatusBar()
|
|
67
71
|
tui._status_bar = fake_status_bar
|
|
@@ -414,5 +414,57 @@ class TestTUIPastePlaceholder(unittest.TestCase):
|
|
|
414
414
|
self.assertIsNone(hint)
|
|
415
415
|
|
|
416
416
|
|
|
417
|
+
def test_leading_space_escapes_slash_command(self) -> None:
|
|
418
|
+
"""Leading space before / should send as user message, not slash command."""
|
|
419
|
+
tui = _build_tui(threshold=5)
|
|
420
|
+
tui._busy = False
|
|
421
|
+
tui._ui_mode = UIMode.NORMAL
|
|
422
|
+
tui._clear_input_area = lambda **_kw: None
|
|
423
|
+
|
|
424
|
+
captured: dict[str, str | None] = {"text": None, "display_text": None}
|
|
425
|
+
|
|
426
|
+
async def _fake_submit_user_message(
|
|
427
|
+
text: str,
|
|
428
|
+
*,
|
|
429
|
+
display_text: str | None = None,
|
|
430
|
+
) -> None:
|
|
431
|
+
captured["text"] = text
|
|
432
|
+
captured["display_text"] = display_text
|
|
433
|
+
|
|
434
|
+
dispatched: list[str] = []
|
|
435
|
+
|
|
436
|
+
async def _fake_execute_command(command: str) -> None:
|
|
437
|
+
dispatched.append(command)
|
|
438
|
+
|
|
439
|
+
tui._submit_user_message = _fake_submit_user_message
|
|
440
|
+
tui._execute_command = _fake_execute_command
|
|
441
|
+
|
|
442
|
+
def _run_immediately(coro: object) -> None:
|
|
443
|
+
asyncio.run(coro)
|
|
444
|
+
|
|
445
|
+
tui._schedule_background = _run_immediately
|
|
446
|
+
tui._input_area = _FakeInputArea(" /aaa")
|
|
447
|
+
|
|
448
|
+
tui._submit_from_input()
|
|
449
|
+
|
|
450
|
+
self.assertEqual(dispatched, [])
|
|
451
|
+
self.assertEqual(captured["text"], "/aaa")
|
|
452
|
+
|
|
453
|
+
def test_leading_space_escapes_slash_command_when_busy(self) -> None:
|
|
454
|
+
"""Leading space before / should enqueue as user message when busy."""
|
|
455
|
+
tui = _build_tui(threshold=5)
|
|
456
|
+
tui._busy = True
|
|
457
|
+
tui._ui_mode = UIMode.NORMAL
|
|
458
|
+
tui._clear_input_area = lambda **_kw: None
|
|
459
|
+
tui._execute_command = lambda _: None
|
|
460
|
+
tui._schedule_background = lambda _: None
|
|
461
|
+
|
|
462
|
+
tui._input_area = _FakeInputArea(" /help")
|
|
463
|
+
tui._submit_from_input()
|
|
464
|
+
|
|
465
|
+
self.assertEqual(len(tui._queued_messages), 1)
|
|
466
|
+
self.assertEqual(tui._queued_messages[0], "/help")
|
|
467
|
+
|
|
468
|
+
|
|
417
469
|
if __name__ == "__main__":
|
|
418
470
|
unittest.main(verbosity=2)
|
|
@@ -364,7 +364,7 @@ wheels = [
|
|
|
364
364
|
|
|
365
365
|
[[package]]
|
|
366
366
|
name = "comate-agent-sdk"
|
|
367
|
-
version = "0.3.
|
|
367
|
+
version = "0.3.2"
|
|
368
368
|
source = { editable = "../../" }
|
|
369
369
|
dependencies = [
|
|
370
370
|
{ name = "aiohttp" },
|
|
@@ -429,7 +429,7 @@ dev = [
|
|
|
429
429
|
|
|
430
430
|
[[package]]
|
|
431
431
|
name = "comate-cli"
|
|
432
|
-
version = "0.3.
|
|
432
|
+
version = "0.3.2"
|
|
433
433
|
source = { editable = "." }
|
|
434
434
|
dependencies = [
|
|
435
435
|
{ name = "comate-agent-sdk" },
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"""Unified error formatter for terminal agent"""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def format_error(exc: Exception) -> tuple[str, str | None]:
|
|
6
|
-
"""Convert exception to user-friendly message.
|
|
7
|
-
|
|
8
|
-
Returns:
|
|
9
|
-
(error_message, suggestion) - Error message and optional suggestion
|
|
10
|
-
"""
|
|
11
|
-
exc_type = type(exc).__name__
|
|
12
|
-
exc_msg = str(exc)
|
|
13
|
-
|
|
14
|
-
# LLM Provider errors
|
|
15
|
-
if exc_type == "ModelRateLimitError":
|
|
16
|
-
return "⚠️ Rate limit exceeded", "Wait a moment, or use /model to switch"
|
|
17
|
-
|
|
18
|
-
if exc_type == "ModelProviderError":
|
|
19
|
-
code = getattr(exc, 'status_code', None)
|
|
20
|
-
if code == 404:
|
|
21
|
-
return "⚠️ Model not found or invalid API path", "Check .agent/settings.json"
|
|
22
|
-
if code == 401:
|
|
23
|
-
return "⚠️ Invalid or expired API key", "Check api_key in .agent/settings.json"
|
|
24
|
-
if code == 403:
|
|
25
|
-
return "⚠️ Access denied to this model", "Check API key permissions"
|
|
26
|
-
if code and code >= 500:
|
|
27
|
-
return f"⚠️ Server error ({code})", "Try again later"
|
|
28
|
-
return f"⚠️ API error: {_truncate(exc_msg, 80)}", None
|
|
29
|
-
|
|
30
|
-
# Session errors
|
|
31
|
-
if exc_type == "ChatSessionClosedError":
|
|
32
|
-
return "⚠️ Session closed", "Please restart the CLI"
|
|
33
|
-
|
|
34
|
-
# Network errors (generic detection)
|
|
35
|
-
lower_msg = exc_msg.lower()
|
|
36
|
-
if "timeout" in lower_msg or "timed out" in lower_msg:
|
|
37
|
-
return "⚠️ Request timed out", "Check network connection, or try again"
|
|
38
|
-
if "connection" in lower_msg:
|
|
39
|
-
return "⚠️ Connection failed", "Check network and API endpoint"
|
|
40
|
-
|
|
41
|
-
# Generic fallback
|
|
42
|
-
return f"⚠️ Error: {_truncate(exc_msg, 60)}", "You can continue typing"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _truncate(s: str, max_len: int) -> str:
|
|
46
|
-
return s[:max_len] + "..." if len(s) > max_len else s
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{comate_cli-0.3.2 → comate_cli-0.3.3}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|