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.
Files changed (128) hide show
  1. {comate_cli-0.4.2 → comate_cli-0.4.4}/PKG-INFO +6 -1
  2. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/app.py +26 -0
  3. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/event_renderer.py +16 -0
  4. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui.py +7 -0
  5. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/commands.py +59 -2
  6. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/input_behavior.py +3 -0
  7. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/key_bindings.py +10 -0
  8. comate_cli-0.4.4/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +137 -0
  9. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/ui_mode.py +1 -0
  10. {comate_cli-0.4.2 → comate_cli-0.4.4}/pyproject.toml +6 -1
  11. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_event_renderer.py +44 -0
  12. comate_cli-0.4.4/uv.lock +2241 -0
  13. comate_cli-0.4.2/uv.lock +0 -2231
  14. {comate_cli-0.4.2 → comate_cli-0.4.4}/.gitignore +0 -0
  15. {comate_cli-0.4.2 → comate_cli-0.4.4}/README.md +0 -0
  16. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/__init__.py +0 -0
  17. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/__main__.py +0 -0
  18. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/main.py +0 -0
  19. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/mcp_cli.py +0 -0
  20. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/__init__.py +0 -0
  21. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/animations.py +0 -0
  22. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/assistant_render.py +0 -0
  23. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/codenames.py +0 -0
  24. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  25. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/env_utils.py +0 -0
  26. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/error_display.py +0 -0
  27. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  28. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/history_printer.py +0 -0
  29. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/input_geometry.py +0 -0
  30. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  31. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/logging_adapter.py +0 -0
  32. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/logo.py +0 -0
  33. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/markdown_render.py +0 -0
  34. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/mention_completer.py +0 -0
  35. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/message_style.py +0 -0
  36. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/models.py +0 -0
  37. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/path_context_hint.py +0 -0
  38. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
  39. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
  40. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
  41. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
  42. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
  43. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
  44. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
  45. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
  46. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
  47. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
  48. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
  49. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
  50. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
  51. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/preflight.py +0 -0
  52. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/question_view.py +0 -0
  53. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/resume_selector.py +0 -0
  54. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/rewind_store.py +0 -0
  55. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  56. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  57. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/selection_menu.py +0 -0
  58. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/session_view.py +0 -0
  59. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/slash_commands.py +0 -0
  60. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/startup.py +0 -0
  61. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/status_bar.py +0 -0
  62. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/text_effects.py +0 -0
  63. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tips.py +0 -0
  64. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tool_view.py +0 -0
  65. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  66. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
  67. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
  68. {comate_cli-0.4.2 → comate_cli-0.4.4}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  69. {comate_cli-0.4.2 → comate_cli-0.4.4}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
  70. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/conftest.py +0 -0
  71. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_app_mcp_preload.py +0 -0
  72. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_app_preflight_gate.py +0 -0
  73. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_app_print_mode.py +0 -0
  74. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_app_shutdown.py +0 -0
  75. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_app_usage_line.py +0 -0
  76. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_cli_project_root.py +0 -0
  77. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_compact_command_semantics.py +0 -0
  78. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_completion_context_activation.py +0 -0
  79. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_completion_status_panel.py +0 -0
  80. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_context_command.py +0 -0
  81. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_custom_slash_commands.py +0 -0
  82. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_discover_tab.py +0 -0
  83. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_errors_tab.py +0 -0
  84. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_format_error.py +0 -0
  85. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_handle_error.py +0 -0
  86. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_history_printer.py +0 -0
  87. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_history_sync.py +0 -0
  88. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_input_behavior.py +0 -0
  89. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_input_history.py +0 -0
  90. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_installed_tab.py +0 -0
  91. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_interrupt_exit_semantics.py +0 -0
  92. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_layout_coordinator.py +0 -0
  93. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_logging_adapter.py +0 -0
  94. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_logo.py +0 -0
  95. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_main_args.py +0 -0
  96. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_marketplaces_tab.py +0 -0
  97. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_mcp_cli.py +0 -0
  98. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_mcp_slash_command.py +0 -0
  99. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_mention_completer.py +0 -0
  100. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_path_context_hint.py +0 -0
  101. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_plugin_slash_commands.py +0 -0
  102. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_plugin_tui_components.py +0 -0
  103. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_preflight.py +0 -0
  104. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_preflight_copilot.py +0 -0
  105. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_question_key_bindings.py +0 -0
  106. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_question_view.py +0 -0
  107. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_resume_selector.py +0 -0
  108. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_rewind_command_semantics.py +0 -0
  109. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_rewind_store.py +0 -0
  110. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_rpc_protocol.py +0 -0
  111. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_rpc_stdio_bridge.py +0 -0
  112. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_selection_menu.py +0 -0
  113. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_skills_slash_command.py +0 -0
  114. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_slash_argument_hint.py +0 -0
  115. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_slash_completer.py +0 -0
  116. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_slash_registry.py +0 -0
  117. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_status_bar.py +0 -0
  118. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_status_bar_transient.py +0 -0
  119. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_task_panel_format.py +0 -0
  120. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_task_panel_key_bindings.py +0 -0
  121. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_task_panel_rendering.py +0 -0
  122. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_task_poll.py +0 -0
  123. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_tool_view.py +0 -0
  124. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_tui_elapsed_status.py +0 -0
  125. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_tui_mcp_init_gate.py +0 -0
  126. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_tui_paste_placeholder.py +0 -0
  127. {comate_cli-0.4.2 → comate_cli-0.4.4}/tests/test_tui_split_invariance.py +0 -0
  128. {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.2
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
- # The callback handles the actual switch
1321
- self._exit_selection_mode()
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
@@ -9,3 +9,4 @@ class UIMode(Enum):
9
9
  SELECTION = "selection"
10
10
  PLUGIN = "plugin"
11
11
  MARKETPLACE_INSTALL = "marketplace_install"
12
+ MCP_CONNECTING = "mcp_connecting"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "comate-cli"
7
- version = "0.4.2"
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")