comate-cli 0.4.2__tar.gz → 0.4.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.4.2 → comate_cli-0.4.4}/PKG-INFO +6 -1
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/app.py +26 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/event_renderer.py +16 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui.py +7 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/commands.py +59 -2
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/input_behavior.py +3 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/key_bindings.py +10 -0
- comate_cli-0.4.4/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +137 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/ui_mode.py +1 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/pyproject.toml +6 -1
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_event_renderer.py +44 -0
- comate_cli-0.4.4/uv.lock +2241 -0
- comate_cli-0.4.2/uv.lock +0 -2231
- {comate_cli-0.4.2 → comate_cli-0.4.4}/.gitignore +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/README.md +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/__init__.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/__main__.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/main.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/history_printer.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/rewind_store.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/session_view.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tool_view.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/conftest.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_completion_status_panel.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_context_command.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_format_error.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_handle_error.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_history_printer.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_history_sync.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_input_history.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_logo.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_main_args.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_preflight.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_question_view.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_rewind_store.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_status_bar.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_task_poll.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_tool_view.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_update_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: comate-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -14,8 +14,13 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Requires-Python: >=3.11
|
|
17
|
+
Requires-Dist: charset-normalizer==3.4.7
|
|
17
18
|
Requires-Dist: comate-agent-sdk<1.0.0,>=0.0.2
|
|
19
|
+
Requires-Dist: curl-cffi==0.13.0
|
|
20
|
+
Requires-Dist: packaging>=21.0
|
|
21
|
+
Requires-Dist: pillow==12.2.0
|
|
18
22
|
Requires-Dist: prompt-toolkit>=3.0
|
|
23
|
+
Requires-Dist: requests==2.32.5
|
|
19
24
|
Requires-Dist: rich>=14.0
|
|
20
25
|
Description-Content-Type: text/markdown
|
|
21
26
|
|
|
@@ -292,6 +292,31 @@ async def _run_print_mode(
|
|
|
292
292
|
await _graceful_shutdown(session)
|
|
293
293
|
|
|
294
294
|
|
|
295
|
+
def _install_event_loop_exception_handler() -> None:
|
|
296
|
+
"""Install a custom exception handler to suppress known MCP transport race conditions.
|
|
297
|
+
|
|
298
|
+
The MCP SDK's streamable_http_client uses anyio TaskGroups internally.
|
|
299
|
+
When connections are cleaned up from a different asyncio Task than the one
|
|
300
|
+
that created them, anyio raises RuntimeError("Attempted to exit cancel scope
|
|
301
|
+
in a different task"). This handler downgrades that specific error to a
|
|
302
|
+
warning log instead of crashing the TUI.
|
|
303
|
+
"""
|
|
304
|
+
loop = asyncio.get_running_loop()
|
|
305
|
+
_default = loop.get_exception_handler()
|
|
306
|
+
|
|
307
|
+
def _handler(loop: asyncio.AbstractEventLoop, context: dict) -> None:
|
|
308
|
+
exc = context.get("exception")
|
|
309
|
+
if isinstance(exc, RuntimeError) and "cancel scope" in str(exc):
|
|
310
|
+
logger.warning("MCP transport cleanup race detected (suppressed): %s", exc)
|
|
311
|
+
return
|
|
312
|
+
if _default is not None:
|
|
313
|
+
_default(loop, context)
|
|
314
|
+
else:
|
|
315
|
+
loop.default_exception_handler(context)
|
|
316
|
+
|
|
317
|
+
loop.set_exception_handler(_handler)
|
|
318
|
+
|
|
319
|
+
|
|
295
320
|
async def run(
|
|
296
321
|
*,
|
|
297
322
|
rpc_stdio: bool = False,
|
|
@@ -299,6 +324,7 @@ async def run(
|
|
|
299
324
|
resume_select: bool = False,
|
|
300
325
|
print_message: str | None = None,
|
|
301
326
|
) -> None:
|
|
327
|
+
_install_event_loop_exception_handler()
|
|
302
328
|
project_root = _resolve_cli_project_root()
|
|
303
329
|
preflight_result = await run_preflight_if_needed(
|
|
304
330
|
console=console,
|
|
@@ -578,6 +578,22 @@ class EventRenderer:
|
|
|
578
578
|
return "Successfully loaded skill"
|
|
579
579
|
|
|
580
580
|
if lowered == "read":
|
|
581
|
+
if isinstance(metadata, dict):
|
|
582
|
+
lines_returned = metadata.get("lines_returned")
|
|
583
|
+
start_line = metadata.get("start_line")
|
|
584
|
+
end_line = metadata.get("end_line")
|
|
585
|
+
total_lines = metadata.get("total_lines")
|
|
586
|
+
if (
|
|
587
|
+
isinstance(lines_returned, int)
|
|
588
|
+
and lines_returned >= 0
|
|
589
|
+
and isinstance(start_line, int)
|
|
590
|
+
and start_line >= 0
|
|
591
|
+
and isinstance(end_line, int)
|
|
592
|
+
and end_line >= 0
|
|
593
|
+
and isinstance(total_lines, int)
|
|
594
|
+
and total_lines >= 0
|
|
595
|
+
):
|
|
596
|
+
return f"Read {lines_returned} lines · Lines {start_line}-{end_line} of {total_lines}"
|
|
581
597
|
lines = result.split("\n") if result else []
|
|
582
598
|
if lines and lines[-1] == "":
|
|
583
599
|
lines = lines[:-1]
|
|
@@ -54,6 +54,7 @@ from comate_cli.terminal_agent.custom_slash_commands import (
|
|
|
54
54
|
)
|
|
55
55
|
from comate_cli.terminal_agent.question_view import AskUserQuestionUI
|
|
56
56
|
from comate_cli.terminal_agent.plugins.marketplace_install_view import MarketplaceInstallView
|
|
57
|
+
from comate_cli.terminal_agent.tui_parts.mcp_connecting_view import McpConnectingView
|
|
57
58
|
from comate_cli.terminal_agent.plugins.plugin_picker import PluginPickerUI
|
|
58
59
|
from comate_cli.terminal_agent.rewind_store import RewindStore
|
|
59
60
|
from comate_cli.terminal_agent.selection_menu import (
|
|
@@ -350,6 +351,7 @@ class TerminalAgentTUI(
|
|
|
350
351
|
self._question_ui = AskUserQuestionUI()
|
|
351
352
|
self._plugin_ui = PluginPickerUI()
|
|
352
353
|
self._install_view = MarketplaceInstallView()
|
|
354
|
+
self._mcp_connecting_view = McpConnectingView()
|
|
353
355
|
self._selection_ui = SelectionMenuUI()
|
|
354
356
|
self._todo_control = FormattedTextControl(text=self._todo_text)
|
|
355
357
|
self._loading_control = FormattedTextControl(text=self._loading_text)
|
|
@@ -460,6 +462,10 @@ class TerminalAgentTUI(
|
|
|
460
462
|
content=self._install_view.container,
|
|
461
463
|
filter=Condition(lambda: self._ui_mode == UIMode.MARKETPLACE_INSTALL),
|
|
462
464
|
)
|
|
465
|
+
self._mcp_connecting_container = ConditionalContainer(
|
|
466
|
+
content=self._mcp_connecting_view.container,
|
|
467
|
+
filter=Condition(lambda: self._ui_mode == UIMode.MCP_CONNECTING),
|
|
468
|
+
)
|
|
463
469
|
|
|
464
470
|
self._main_container = HSplit(
|
|
465
471
|
[
|
|
@@ -482,6 +488,7 @@ class TerminalAgentTUI(
|
|
|
482
488
|
self._selection_container,
|
|
483
489
|
self._plugin_container,
|
|
484
490
|
self._install_view_container,
|
|
491
|
+
self._mcp_connecting_container,
|
|
485
492
|
Window(height=1, char="─", style="class:input.separator"),
|
|
486
493
|
self._bottom_container,
|
|
487
494
|
]
|
|
@@ -384,6 +384,13 @@ class CommandsMixin:
|
|
|
384
384
|
"description": f"{tool_count} tools",
|
|
385
385
|
}
|
|
386
386
|
)
|
|
387
|
+
options.append(
|
|
388
|
+
{
|
|
389
|
+
"value": "reconnect",
|
|
390
|
+
"label": "Reconnect",
|
|
391
|
+
"description": "Reconnect to this server",
|
|
392
|
+
}
|
|
393
|
+
)
|
|
387
394
|
options.append(
|
|
388
395
|
{
|
|
389
396
|
"value": "back",
|
|
@@ -409,6 +416,9 @@ class CommandsMixin:
|
|
|
409
416
|
)
|
|
410
417
|
)
|
|
411
418
|
return
|
|
419
|
+
if selected == "reconnect":
|
|
420
|
+
self._start_mcp_reconnect(alias, rows_by_alias)
|
|
421
|
+
return
|
|
412
422
|
self._schedule_background(
|
|
413
423
|
self._open_mcp_server_list_menu_async(
|
|
414
424
|
rows=list(rows_by_alias.values()),
|
|
@@ -441,6 +451,52 @@ class CommandsMixin:
|
|
|
441
451
|
self._sync_focus_for_mode()
|
|
442
452
|
self._invalidate()
|
|
443
453
|
|
|
454
|
+
def _start_mcp_reconnect(self, alias: str, rows_by_alias: dict[str, dict[str, Any]]) -> None:
|
|
455
|
+
"""启动 MCP server 重连流程。"""
|
|
456
|
+
agent_runtime = self._session._agent
|
|
457
|
+
|
|
458
|
+
def on_done(success: bool, error_message: str) -> None:
|
|
459
|
+
if success:
|
|
460
|
+
mgr = getattr(agent_runtime, "_mcp_manager", None)
|
|
461
|
+
tool_count = 0
|
|
462
|
+
if mgr:
|
|
463
|
+
tool_count = sum(
|
|
464
|
+
1 for info in (getattr(mgr, "tool_infos", None) or [])
|
|
465
|
+
if getattr(info, "server_alias", "") == alias
|
|
466
|
+
)
|
|
467
|
+
message = f"Reconnected to {alias}. ({tool_count} tools loaded)"
|
|
468
|
+
else:
|
|
469
|
+
message = f"Failed to reconnect to {alias}: {error_message}"
|
|
470
|
+
|
|
471
|
+
# 将 /mcp 命令和结果刷入 scrollback
|
|
472
|
+
self._renderer.seed_user_message("/mcp")
|
|
473
|
+
self._renderer.append_subtitle(message)
|
|
474
|
+
|
|
475
|
+
if success:
|
|
476
|
+
# 成功:回到 NORMAL 模式,不返回菜单
|
|
477
|
+
self._ui_mode = UIMode.NORMAL
|
|
478
|
+
self._sync_focus_for_mode()
|
|
479
|
+
self._refresh_layers()
|
|
480
|
+
else:
|
|
481
|
+
# 失败:回到 detail 菜单,方便用户再次重试
|
|
482
|
+
refreshed_rows = self._collect_mcp_cached_rows()
|
|
483
|
+
if refreshed_rows:
|
|
484
|
+
refreshed_by_alias = {str(r["alias"]): r for r in refreshed_rows}
|
|
485
|
+
rows_by_alias.update(refreshed_by_alias)
|
|
486
|
+
|
|
487
|
+
self._schedule_background(
|
|
488
|
+
self._open_mcp_server_detail_menu_async(
|
|
489
|
+
rows_by_alias=rows_by_alias,
|
|
490
|
+
alias=alias,
|
|
491
|
+
)
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
retry_coro = agent_runtime.retry_mcp_server(alias)
|
|
495
|
+
self._mcp_connecting_view.enter(alias, retry_coro, on_done)
|
|
496
|
+
self._ui_mode = UIMode.MCP_CONNECTING
|
|
497
|
+
self._sync_focus_for_mode()
|
|
498
|
+
self._invalidate()
|
|
499
|
+
|
|
444
500
|
async def _open_mcp_server_list_menu_async(
|
|
445
501
|
self,
|
|
446
502
|
*,
|
|
@@ -1317,8 +1373,9 @@ class CommandsMixin:
|
|
|
1317
1373
|
self._exit_selection_mode()
|
|
1318
1374
|
return
|
|
1319
1375
|
|
|
1320
|
-
#
|
|
1321
|
-
self.
|
|
1376
|
+
# 回调可能已经切换了 ui_mode(如 MCP_CONNECTING),尊重回调的意图
|
|
1377
|
+
if self._ui_mode == UIMode.SELECTION:
|
|
1378
|
+
self._exit_selection_mode()
|
|
1322
1379
|
|
|
1323
1380
|
async def _append_usage_snapshot(self) -> None:
|
|
1324
1381
|
usage = await self._session.get_usage()
|
|
@@ -137,6 +137,9 @@ class InputBehaviorMixin:
|
|
|
137
137
|
if self._ui_mode == UIMode.MARKETPLACE_INSTALL:
|
|
138
138
|
self._app.layout.focus(self._install_view.focus_target())
|
|
139
139
|
return
|
|
140
|
+
if self._ui_mode == UIMode.MCP_CONNECTING:
|
|
141
|
+
self._app.layout.focus(self._mcp_connecting_view.focus_target())
|
|
142
|
+
return
|
|
140
143
|
self._app.layout.focus(self._input_area.window)
|
|
141
144
|
|
|
142
145
|
def _handle_question_action(self, action: QuestionAction | None) -> None:
|
|
@@ -18,6 +18,7 @@ class KeyBindingsMixin:
|
|
|
18
18
|
selection_mode = Condition(lambda: self._ui_mode == UIMode.SELECTION)
|
|
19
19
|
plugin_mode = Condition(lambda: self._ui_mode == UIMode.PLUGIN)
|
|
20
20
|
install_mode = Condition(lambda: self._ui_mode == UIMode.MARKETPLACE_INSTALL)
|
|
21
|
+
mcp_connecting_mode = Condition(lambda: self._ui_mode == UIMode.MCP_CONNECTING)
|
|
21
22
|
diff_panel_open = Condition(lambda: self._diff_panel_visible)
|
|
22
23
|
todo_panel_expanded = Condition(
|
|
23
24
|
lambda: self._todo_panel_expanded and self._renderer.has_active_todos()
|
|
@@ -182,6 +183,15 @@ class KeyBindingsMixin:
|
|
|
182
183
|
self._exit_install_view()
|
|
183
184
|
self._invalidate()
|
|
184
185
|
|
|
186
|
+
@bindings.add("escape", filter=mcp_connecting_mode)
|
|
187
|
+
def _mcp_connecting_escape(event) -> None:
|
|
188
|
+
del event
|
|
189
|
+
should_close = self._mcp_connecting_view.handle_escape()
|
|
190
|
+
if should_close:
|
|
191
|
+
self._ui_mode = UIMode.NORMAL
|
|
192
|
+
self._sync_focus_for_mode()
|
|
193
|
+
self._invalidate()
|
|
194
|
+
|
|
185
195
|
@bindings.add("enter", filter=question_mode)
|
|
186
196
|
def _question_enter(event) -> None:
|
|
187
197
|
del event
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""MCP Server 重连面板 — 轻量级 TUI 组件。
|
|
2
|
+
|
|
3
|
+
显示连接进度,支持 Esc 取消,20 秒超时。
|
|
4
|
+
完成后通过 on_done 回调通知调用方。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from prompt_toolkit.application.current import get_app_or_none
|
|
13
|
+
from prompt_toolkit.layout.containers import Window
|
|
14
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
_PANEL_HEIGHT = 8
|
|
19
|
+
|
|
20
|
+
_H = "\u2500" # ─
|
|
21
|
+
_TL = "\u256d" # ╭
|
|
22
|
+
_TR = "\u256e" # ╮
|
|
23
|
+
_BL = "\u2570" # ╰
|
|
24
|
+
_BR = "\u256f" # ╯
|
|
25
|
+
_V = "\u2502" # │
|
|
26
|
+
|
|
27
|
+
_CONNECT_TIMEOUT_S = 20.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class McpConnectingView:
|
|
31
|
+
"""轻量 MCP 重连面板,嵌入主 TUI 底部交互带。"""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._alias: str = ""
|
|
35
|
+
self._task: asyncio.Task[None] | None = None
|
|
36
|
+
self._on_done: callable | None = None
|
|
37
|
+
self._retry_coro = None
|
|
38
|
+
|
|
39
|
+
self._content_window = Window(
|
|
40
|
+
content=FormattedTextControl(self._render, focusable=True),
|
|
41
|
+
height=_PANEL_HEIGHT,
|
|
42
|
+
dont_extend_height=True,
|
|
43
|
+
always_hide_cursor=True,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def container(self) -> Window:
|
|
48
|
+
return self._content_window
|
|
49
|
+
|
|
50
|
+
def focus_target(self) -> Window:
|
|
51
|
+
return self._content_window
|
|
52
|
+
|
|
53
|
+
def enter(
|
|
54
|
+
self,
|
|
55
|
+
alias: str,
|
|
56
|
+
retry_coro,
|
|
57
|
+
on_done: callable,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""初始化并启动连接。"""
|
|
60
|
+
self._alias = alias
|
|
61
|
+
self._retry_coro = retry_coro
|
|
62
|
+
self._on_done = on_done
|
|
63
|
+
self._task = asyncio.create_task(self._do_connect())
|
|
64
|
+
self._task.add_done_callback(self._on_task_done)
|
|
65
|
+
|
|
66
|
+
def handle_escape(self) -> bool:
|
|
67
|
+
"""Esc 取消连接。返回 True 表示面板应关闭。"""
|
|
68
|
+
if self._task and not self._task.done():
|
|
69
|
+
self._task.cancel()
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
async def _do_connect(self) -> None:
|
|
73
|
+
try:
|
|
74
|
+
result = await asyncio.wait_for(
|
|
75
|
+
self._retry_coro,
|
|
76
|
+
timeout=_CONNECT_TIMEOUT_S,
|
|
77
|
+
)
|
|
78
|
+
if result:
|
|
79
|
+
self._on_done(True, "")
|
|
80
|
+
else:
|
|
81
|
+
self._on_done(False, "connection failed")
|
|
82
|
+
except asyncio.TimeoutError:
|
|
83
|
+
self._on_done(False, f"timed out after {int(_CONNECT_TIMEOUT_S)}s")
|
|
84
|
+
except asyncio.CancelledError:
|
|
85
|
+
raise
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
self._on_done(False, str(exc))
|
|
88
|
+
|
|
89
|
+
def _on_task_done(self, task: asyncio.Task[None]) -> None:
|
|
90
|
+
self._task = None
|
|
91
|
+
app = get_app_or_none()
|
|
92
|
+
if app is not None:
|
|
93
|
+
app.invalidate()
|
|
94
|
+
|
|
95
|
+
def _render(self) -> list[tuple[str, str]]:
|
|
96
|
+
app = get_app_or_none()
|
|
97
|
+
width = 80
|
|
98
|
+
if app is not None:
|
|
99
|
+
try:
|
|
100
|
+
width = app.output.get_size().columns
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
inner_w = max(20, width - 4)
|
|
104
|
+
|
|
105
|
+
lines: list[list[tuple[str, str]]] = []
|
|
106
|
+
lines.append([("bold", f"Connecting to {self._alias}\u2026")])
|
|
107
|
+
lines.append([])
|
|
108
|
+
lines.append([("class:dim", " \u273d Establishing connection to MCP server")])
|
|
109
|
+
lines.append([])
|
|
110
|
+
lines.append([("class:dim", " This may take a few moments.")])
|
|
111
|
+
|
|
112
|
+
content_lines = _PANEL_HEIGHT - 3
|
|
113
|
+
while len(lines) < content_lines:
|
|
114
|
+
lines.append([])
|
|
115
|
+
|
|
116
|
+
fragments: list[tuple[str, str]] = []
|
|
117
|
+
|
|
118
|
+
fragments.append(("class:plugin.divider", f"{_TL}{_H * (width - 2)}{_TR}"))
|
|
119
|
+
fragments.append(("", "\n"))
|
|
120
|
+
|
|
121
|
+
for line_frags in lines[:content_lines]:
|
|
122
|
+
fragments.append(("class:plugin.divider", f"{_V} "))
|
|
123
|
+
line_len = sum(len(text) for _, text in line_frags)
|
|
124
|
+
for style, text in line_frags:
|
|
125
|
+
fragments.append((style, text))
|
|
126
|
+
pad = inner_w - line_len
|
|
127
|
+
if pad > 0:
|
|
128
|
+
fragments.append(("", " " * pad))
|
|
129
|
+
fragments.append(("class:plugin.divider", f" {_V}"))
|
|
130
|
+
fragments.append(("", "\n"))
|
|
131
|
+
|
|
132
|
+
fragments.append(("class:plugin.divider", f"{_BL}{_H * (width - 2)}{_BR}"))
|
|
133
|
+
fragments.append(("", "\n"))
|
|
134
|
+
|
|
135
|
+
fragments.append(("class:dim", " Esc to cancel"))
|
|
136
|
+
|
|
137
|
+
return fragments
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "comate-cli"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.4"
|
|
8
8
|
description = "Comate terminal CLI built on comate-agent-sdk"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -25,6 +25,11 @@ dependencies = [
|
|
|
25
25
|
"comate-agent-sdk>=0.0.2,<1.0.0",
|
|
26
26
|
"rich>=14.0",
|
|
27
27
|
"prompt-toolkit>=3.0",
|
|
28
|
+
"packaging>=21.0",
|
|
29
|
+
"pillow==12.2.0",
|
|
30
|
+
"charset-normalizer==3.4.7",
|
|
31
|
+
"curl-cffi==0.13.0",
|
|
32
|
+
"requests==2.32.5",
|
|
28
33
|
]
|
|
29
34
|
|
|
30
35
|
[project.urls]
|
|
@@ -722,6 +722,19 @@ class TestBuildToolSubtitle(unittest.TestCase):
|
|
|
722
722
|
subtitle = self._build("Read", result)
|
|
723
723
|
self.assertEqual(subtitle, "Read 3 lines")
|
|
724
724
|
|
|
725
|
+
def test_read_prefers_structured_metadata(self) -> None:
|
|
726
|
+
subtitle = self._build(
|
|
727
|
+
"Read",
|
|
728
|
+
"formatted body that should not drive subtitle",
|
|
729
|
+
metadata={
|
|
730
|
+
"lines_returned": 437,
|
|
731
|
+
"start_line": 501,
|
|
732
|
+
"end_line": 937,
|
|
733
|
+
"total_lines": 937,
|
|
734
|
+
},
|
|
735
|
+
)
|
|
736
|
+
self.assertEqual(subtitle, "Read 437 lines · Lines 501-937 of 937")
|
|
737
|
+
|
|
725
738
|
def test_write_counts_lines(self) -> None:
|
|
726
739
|
result = "line1\nline2\n"
|
|
727
740
|
subtitle = self._build("Write", result)
|
|
@@ -819,6 +832,37 @@ class TestToolResultSubtitleInjection(unittest.TestCase):
|
|
|
819
832
|
self.assertEqual(len(entries), 1)
|
|
820
833
|
self.assertEqual(entries[0].subtitle, "Read 3 lines")
|
|
821
834
|
|
|
835
|
+
def test_read_tool_result_uses_metadata_over_visible_text_line_count(self) -> None:
|
|
836
|
+
renderer = EventRenderer()
|
|
837
|
+
renderer.handle_event(
|
|
838
|
+
ToolCallEvent(
|
|
839
|
+
tool="Read",
|
|
840
|
+
args={"file_path": "comate_agent_sdk/mcp/manager.py", "offset_line": 500, "limit_lines": 500},
|
|
841
|
+
tool_call_id="call-read-structured",
|
|
842
|
+
)
|
|
843
|
+
)
|
|
844
|
+
renderer.handle_event(
|
|
845
|
+
ToolResultEvent(
|
|
846
|
+
tool="Read",
|
|
847
|
+
result="# Read: comate_agent_sdk/mcp/manager.py\n\nLines 501-937 of 937 (TRUNCATED: line_clip)\n\n"
|
|
848
|
+
+ "\n".join(f"visible-line-{idx}" for idx in range(219)),
|
|
849
|
+
tool_call_id="call-read-structured",
|
|
850
|
+
is_error=False,
|
|
851
|
+
metadata={
|
|
852
|
+
"offset_line": 500,
|
|
853
|
+
"limit_lines": 500,
|
|
854
|
+
"lines_returned": 437,
|
|
855
|
+
"total_lines": 937,
|
|
856
|
+
"has_more": False,
|
|
857
|
+
"start_line": 501,
|
|
858
|
+
"end_line": 937,
|
|
859
|
+
},
|
|
860
|
+
)
|
|
861
|
+
)
|
|
862
|
+
entries = [e for e in renderer.history_entries() if e.entry_type == "tool_result"]
|
|
863
|
+
self.assertEqual(len(entries), 1)
|
|
864
|
+
self.assertEqual(entries[0].subtitle, "Read 437 lines · Lines 501-937 of 937")
|
|
865
|
+
|
|
822
866
|
def test_error_result_has_subtitle_with_error_summary(self) -> None:
|
|
823
867
|
renderer = EventRenderer()
|
|
824
868
|
renderer._append_tool_call("Read", {"file_path": "/tmp/test.py"}, "call-2")
|