comate-cli 0.7.0a1__tar.gz → 0.7.0a3__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.7.0a1 → comate_cli-0.7.0a3}/CHANGELOG.md +1 -1
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/PKG-INFO +1 -1
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/error_display.py +3 -7
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/logging_adapter.py +0 -5
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/mention_completer.py +72 -11
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/resume_selector.py +3 -3
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui.py +43 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/commands.py +36 -34
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/pyproject.toml +1 -1
- comate_cli-0.7.0a3/tests/test_completion_context_activation.py +119 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_mention_completer.py +171 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/uv.lock +1 -1
- comate_cli-0.7.0a1/tests/test_completion_context_activation.py +0 -56
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/.gitignore +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/README.md +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/bash-exit-code-green-dot-bug.md +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/__init__.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/__main__.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/main.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/app.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/event_renderer.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/figures.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/history_printer.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/resume_picker.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/resume_preview.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tool_result_formatters.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tool_result_store.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tool_view.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/docs/hooks.md +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/conftest.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_btw_slash_command.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_completion_status_panel.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_context_command.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_event_renderer.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_event_renderer_boundary.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_event_renderer_e2e.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_event_renderer_log_boundary.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_event_renderer_log_queue.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_event_renderer_streaming.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_format_error.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_handle_error.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_history_printer.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_history_printer_log.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_history_sync.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_input_history.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_logo.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_main_args.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_markdown_render.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_preflight.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_question_view.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_resume_picker.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_resume_preview.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_session_query_token_summary.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_status_bar.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_task_poll.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tool_result_formatters.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tool_result_store.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tool_result_viewer.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tool_result_viewer_key_bindings.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tool_view.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_transcript_viewer.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_esc_queue.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_queue_preview.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_queue_sdk_source.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_team_messages.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
- {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/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.7.
|
|
3
|
+
Version: 0.7.0a3
|
|
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
|
|
@@ -30,7 +30,7 @@ def format_error(exc: Exception) -> tuple[str, str, str]:
|
|
|
30
30
|
"Context limit exceeded",
|
|
31
31
|
"error",
|
|
32
32
|
)
|
|
33
|
-
return f"API error: {
|
|
33
|
+
return f"API error: {exc_msg}", "API error", "error"
|
|
34
34
|
|
|
35
35
|
if code == 401:
|
|
36
36
|
return "Invalid or expired API key", "Auth failed", "error"
|
|
@@ -40,7 +40,7 @@ def format_error(exc: Exception) -> tuple[str, str, str]:
|
|
|
40
40
|
return "Model not found or invalid API path", "Model not found", "error"
|
|
41
41
|
if code and code >= 500:
|
|
42
42
|
return f"Server error ({code})", "Server error", "warning"
|
|
43
|
-
return f"API error: {
|
|
43
|
+
return f"API error: {exc_msg}", "API error", "error"
|
|
44
44
|
|
|
45
45
|
# Session errors
|
|
46
46
|
if exc_type == "ChatSessionClosedError":
|
|
@@ -54,7 +54,7 @@ def format_error(exc: Exception) -> tuple[str, str, str]:
|
|
|
54
54
|
return "Connection failed", "Connection failed", "error"
|
|
55
55
|
|
|
56
56
|
# Generic fallback
|
|
57
|
-
return f"Error: {
|
|
57
|
+
return f"Error: {exc_msg}", "Error occurred", "error"
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def _parse_context_size_error(msg: str) -> tuple[str, str] | None:
|
|
@@ -77,7 +77,3 @@ def _parse_context_size_error(msg: str) -> tuple[str, str] | None:
|
|
|
77
77
|
return None # Type matches but can't parse numbers — fall through to generic 400
|
|
78
78
|
|
|
79
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
|
|
@@ -113,11 +113,6 @@ class TUILoggingHandler(logging.Handler):
|
|
|
113
113
|
parts = msg.split("timeout_ms=")
|
|
114
114
|
msg = parts[0].rstrip(": ,")
|
|
115
115
|
|
|
116
|
-
# 限制长度
|
|
117
|
-
max_len = 100
|
|
118
|
-
if len(msg) > max_len:
|
|
119
|
-
msg = msg[:max_len] + "..."
|
|
120
|
-
|
|
121
116
|
return msg
|
|
122
117
|
|
|
123
118
|
def _get_message_key(self, record: logging.LogRecord) -> str:
|
|
@@ -3,8 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
5
|
import subprocess
|
|
6
|
+
import threading
|
|
6
7
|
import time
|
|
7
|
-
from collections.abc import Iterable
|
|
8
|
+
from collections.abc import Callable, Iterable
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from pathlib import Path, PurePosixPath
|
|
10
11
|
|
|
@@ -112,6 +113,9 @@ class LocalFileMentionCompleter(Completer):
|
|
|
112
113
|
|
|
113
114
|
self._deep_cache_time: float = 0.0
|
|
114
115
|
self._deep_cached_paths: list[str] = []
|
|
116
|
+
self._deep_cache_lock = threading.RLock()
|
|
117
|
+
self._deep_warmup_lock = threading.Lock()
|
|
118
|
+
self._deep_warmup_thread: threading.Thread | None = None
|
|
115
119
|
self._directory_cached_paths: dict[str, tuple[float, list[str]]] = {}
|
|
116
120
|
|
|
117
121
|
@classmethod
|
|
@@ -300,16 +304,68 @@ class LocalFileMentionCompleter(Completer):
|
|
|
300
304
|
return self._top_cached_paths
|
|
301
305
|
|
|
302
306
|
def _get_deep_paths(self) -> list[str]:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
307
|
+
with self._deep_cache_lock:
|
|
308
|
+
now = time.monotonic()
|
|
309
|
+
if now - self._deep_cache_time <= self._refresh_interval:
|
|
310
|
+
return self._deep_cached_paths
|
|
306
311
|
|
|
307
|
-
|
|
308
|
-
|
|
312
|
+
file_paths = self._load_indexed_file_paths()
|
|
313
|
+
paths = self._build_deep_paths(file_paths)
|
|
309
314
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
315
|
+
self._deep_cached_paths = paths
|
|
316
|
+
self._deep_cache_time = time.monotonic()
|
|
317
|
+
return self._deep_cached_paths
|
|
318
|
+
|
|
319
|
+
def start_deep_cache_warmup(
|
|
320
|
+
self,
|
|
321
|
+
*,
|
|
322
|
+
on_complete: Callable[[], None] | None = None,
|
|
323
|
+
) -> bool:
|
|
324
|
+
"""Build the deep path cache in a daemon thread if it is stale."""
|
|
325
|
+
with self._deep_warmup_lock:
|
|
326
|
+
if (
|
|
327
|
+
self._deep_warmup_thread is not None
|
|
328
|
+
and self._deep_warmup_thread.is_alive()
|
|
329
|
+
):
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
with self._deep_cache_lock:
|
|
333
|
+
now = time.monotonic()
|
|
334
|
+
if now - self._deep_cache_time <= self._refresh_interval:
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
def _warm() -> None:
|
|
338
|
+
try:
|
|
339
|
+
self._get_deep_paths()
|
|
340
|
+
except Exception:
|
|
341
|
+
logger.debug("deep mention cache warmup failed", exc_info=True)
|
|
342
|
+
finally:
|
|
343
|
+
with self._deep_warmup_lock:
|
|
344
|
+
if self._deep_warmup_thread is threading.current_thread():
|
|
345
|
+
self._deep_warmup_thread = None
|
|
346
|
+
if on_complete is not None:
|
|
347
|
+
try:
|
|
348
|
+
on_complete()
|
|
349
|
+
except Exception:
|
|
350
|
+
logger.debug(
|
|
351
|
+
"deep mention cache warmup callback failed",
|
|
352
|
+
exc_info=True,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
thread = threading.Thread(
|
|
356
|
+
target=_warm,
|
|
357
|
+
name="comate-mention-index-warmup",
|
|
358
|
+
daemon=True,
|
|
359
|
+
)
|
|
360
|
+
with self._deep_warmup_lock:
|
|
361
|
+
if (
|
|
362
|
+
self._deep_warmup_thread is not None
|
|
363
|
+
and self._deep_warmup_thread.is_alive()
|
|
364
|
+
):
|
|
365
|
+
return False
|
|
366
|
+
self._deep_warmup_thread = thread
|
|
367
|
+
thread.start()
|
|
368
|
+
return True
|
|
313
369
|
|
|
314
370
|
def _load_indexed_file_paths(self) -> list[str]:
|
|
315
371
|
git_paths = self._get_paths_from_git()
|
|
@@ -368,12 +424,17 @@ class LocalFileMentionCompleter(Completer):
|
|
|
368
424
|
"%s mention index command exited with %s: %s",
|
|
369
425
|
source_name,
|
|
370
426
|
completed.returncode,
|
|
371
|
-
completed.stderr.strip(),
|
|
427
|
+
(completed.stderr or "").strip(),
|
|
372
428
|
)
|
|
373
429
|
return None
|
|
374
430
|
|
|
431
|
+
stdout = completed.stdout
|
|
432
|
+
if stdout is None:
|
|
433
|
+
logger.debug("%s mention index command returned no stdout", source_name)
|
|
434
|
+
return None
|
|
435
|
+
|
|
375
436
|
indexed_paths: set[str] = set()
|
|
376
|
-
for line in
|
|
437
|
+
for line in stdout.splitlines():
|
|
377
438
|
normalized = self._normalize_indexed_path(line)
|
|
378
439
|
if normalized is None or not self._should_include_relative_path(normalized):
|
|
379
440
|
continue
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Resume picker
|
|
1
|
+
"""Resume picker entry point: feeds SDK SessionInfo into ResumePickerApp."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -52,7 +52,7 @@ def list_resume_sessions_via_sdk(*, cwd: Path | str | None) -> list[ResumeSessio
|
|
|
52
52
|
async def select_resume_session_id(console: Console, *, cwd: Path | None = None) -> str | None:
|
|
53
53
|
items = list_resume_sessions_via_sdk(cwd=cwd)
|
|
54
54
|
if not items:
|
|
55
|
-
console.print("[dim]
|
|
55
|
+
console.print("[dim]No resumable sessions.[/]")
|
|
56
56
|
return None
|
|
57
57
|
|
|
58
58
|
cwd_by_session: dict[str, str | None] = {item.session_id: item.raw.cwd for item in items}
|
|
@@ -82,6 +82,6 @@ async def select_resume_session_id(console: Console, *, cwd: Path | None = None)
|
|
|
82
82
|
with patch_stdout(raw=True):
|
|
83
83
|
result = await app.run_async()
|
|
84
84
|
if not isinstance(result, str) or not result.strip():
|
|
85
|
-
console.print("[dim]
|
|
85
|
+
console.print("[dim]Resume cancelled.[/]")
|
|
86
86
|
return None
|
|
87
87
|
return result
|
|
@@ -329,6 +329,11 @@ class TerminalAgentTUI(
|
|
|
329
329
|
if self._busy:
|
|
330
330
|
return
|
|
331
331
|
doc = self._input_area.buffer.document
|
|
332
|
+
mention_context = self._mention_completer.extract_context(
|
|
333
|
+
doc.text_before_cursor
|
|
334
|
+
)
|
|
335
|
+
if mention_context is not None:
|
|
336
|
+
self._start_mention_cache_warmup()
|
|
332
337
|
if self._completion_context_active(
|
|
333
338
|
doc.text_before_cursor,
|
|
334
339
|
doc.text_after_cursor,
|
|
@@ -1325,6 +1330,43 @@ class TerminalAgentTUI(
|
|
|
1325
1330
|
|
|
1326
1331
|
task.add_done_callback(_done)
|
|
1327
1332
|
|
|
1333
|
+
def _start_mention_cache_warmup(self) -> None:
|
|
1334
|
+
mention_completer = getattr(self, "_mention_completer", None)
|
|
1335
|
+
start_warmup = getattr(mention_completer, "start_deep_cache_warmup", None)
|
|
1336
|
+
if not callable(start_warmup):
|
|
1337
|
+
return
|
|
1338
|
+
start_warmup(on_complete=self._on_mention_cache_warmed)
|
|
1339
|
+
|
|
1340
|
+
def _on_mention_cache_warmed(self) -> None:
|
|
1341
|
+
app = self._app
|
|
1342
|
+
if app is None or self._closing:
|
|
1343
|
+
return
|
|
1344
|
+
loop = getattr(app, "loop", None)
|
|
1345
|
+
if loop is None or loop.is_closed():
|
|
1346
|
+
return
|
|
1347
|
+
loop.call_soon_threadsafe(self._restart_active_mention_completion)
|
|
1348
|
+
app.invalidate()
|
|
1349
|
+
|
|
1350
|
+
def _restart_active_mention_completion(self) -> None:
|
|
1351
|
+
if self._closing or self._busy or self._ui_mode != UIMode.NORMAL:
|
|
1352
|
+
return
|
|
1353
|
+
input_area = getattr(self, "_input_area", None)
|
|
1354
|
+
if input_area is None:
|
|
1355
|
+
return
|
|
1356
|
+
buffer = input_area.buffer
|
|
1357
|
+
doc = buffer.document
|
|
1358
|
+
if not self._completion_context_active(
|
|
1359
|
+
doc.text_before_cursor,
|
|
1360
|
+
doc.text_after_cursor,
|
|
1361
|
+
):
|
|
1362
|
+
return
|
|
1363
|
+
complete_state = buffer.complete_state
|
|
1364
|
+
if complete_state is not None:
|
|
1365
|
+
self._invalidate()
|
|
1366
|
+
return
|
|
1367
|
+
buffer.start_completion(select_first=False)
|
|
1368
|
+
self._invalidate()
|
|
1369
|
+
|
|
1328
1370
|
def _refresh_layers(self) -> None:
|
|
1329
1371
|
self._sync_focus_for_mode()
|
|
1330
1372
|
self._render_dirty = True
|
|
@@ -1546,6 +1588,7 @@ class TerminalAgentTUI(
|
|
|
1546
1588
|
if self._app is None:
|
|
1547
1589
|
return
|
|
1548
1590
|
|
|
1591
|
+
self._start_mention_cache_warmup()
|
|
1549
1592
|
self._refresh_layers()
|
|
1550
1593
|
|
|
1551
1594
|
try:
|
|
@@ -43,12 +43,12 @@ if TYPE_CHECKING:
|
|
|
43
43
|
|
|
44
44
|
class CommandsMixin:
|
|
45
45
|
def _begin_llm_long_task(self) -> None:
|
|
46
|
-
"""
|
|
46
|
+
"""Mark the start of a long-running LLM task: enable busy state and record the start time."""
|
|
47
47
|
self._set_busy(True)
|
|
48
48
|
self._run_start_time = time.monotonic()
|
|
49
49
|
|
|
50
50
|
def _end_llm_long_task(self) -> None:
|
|
51
|
-
"""
|
|
51
|
+
"""Mark the end of a long-running LLM task: clear the run timer and exit busy state."""
|
|
52
52
|
self._run_start_time = None
|
|
53
53
|
self._set_busy(False)
|
|
54
54
|
|
|
@@ -78,7 +78,7 @@ class CommandsMixin:
|
|
|
78
78
|
def _cycle_agent_mode(self) -> None:
|
|
79
79
|
if self._busy:
|
|
80
80
|
self._renderer.append_system_message(
|
|
81
|
-
"
|
|
81
|
+
"A task is currently running. Please switch modes after this turn completes.",
|
|
82
82
|
)
|
|
83
83
|
self._refresh_layers()
|
|
84
84
|
return
|
|
@@ -126,7 +126,7 @@ class CommandsMixin:
|
|
|
126
126
|
is_busy = self._busy
|
|
127
127
|
if is_busy and not entry.allow_when_busy:
|
|
128
128
|
self._append_slash_command_result(
|
|
129
|
-
f"
|
|
129
|
+
f"A task is currently running. /{entry.spec.name} is not available right now.",
|
|
130
130
|
severity="error",
|
|
131
131
|
)
|
|
132
132
|
self._refresh_layers()
|
|
@@ -265,7 +265,7 @@ class CommandsMixin:
|
|
|
265
265
|
self._open_mcp_server_list_menu(rows)
|
|
266
266
|
|
|
267
267
|
def _slash_btw(self, args: str) -> None:
|
|
268
|
-
"""/btw <question> —
|
|
268
|
+
"""/btw <question> — Enter the BTW panel for side-channel Q&A without polluting the main context."""
|
|
269
269
|
normalized = args.strip()
|
|
270
270
|
if not normalized:
|
|
271
271
|
self._append_slash_command_result(
|
|
@@ -274,7 +274,7 @@ class CommandsMixin:
|
|
|
274
274
|
)
|
|
275
275
|
return
|
|
276
276
|
if self._ui_mode == UIMode.BTW:
|
|
277
|
-
#
|
|
277
|
+
# Already in BTW mode (theoretically unreachable: input is hidden); defensive fallback.
|
|
278
278
|
self._append_slash_command_result(
|
|
279
279
|
"Already in /btw mode. Press Esc to exit first.",
|
|
280
280
|
severity="error",
|
|
@@ -282,7 +282,7 @@ class CommandsMixin:
|
|
|
282
282
|
return
|
|
283
283
|
|
|
284
284
|
def _on_exit() -> None:
|
|
285
|
-
# handle_escape
|
|
285
|
+
# Called from inside handle_escape; UI mode switch is finalized by the key binding.
|
|
286
286
|
self._ui_mode = UIMode.NORMAL
|
|
287
287
|
self._sync_focus_for_mode()
|
|
288
288
|
self._invalidate()
|
|
@@ -606,7 +606,7 @@ class CommandsMixin:
|
|
|
606
606
|
*,
|
|
607
607
|
enabled: bool,
|
|
608
608
|
) -> None:
|
|
609
|
-
"""
|
|
609
|
+
"""Start the MCP server enable/disable flow."""
|
|
610
610
|
agent_runtime = self._session._agent
|
|
611
611
|
row = rows_by_alias.get(alias, {})
|
|
612
612
|
config_scope = str(row.get("config_scope", "")).strip().lower()
|
|
@@ -692,7 +692,7 @@ class CommandsMixin:
|
|
|
692
692
|
self._invalidate()
|
|
693
693
|
|
|
694
694
|
def _start_mcp_reconnect(self, alias: str, rows_by_alias: dict[str, dict[str, Any]]) -> None:
|
|
695
|
-
"""
|
|
695
|
+
"""Start the MCP server reconnect flow."""
|
|
696
696
|
agent_runtime = self._session._agent
|
|
697
697
|
row = rows_by_alias.get(alias, {})
|
|
698
698
|
server_type = str(row.get("server_type", "")).strip().upper()
|
|
@@ -711,17 +711,17 @@ class CommandsMixin:
|
|
|
711
711
|
else:
|
|
712
712
|
message = f"Failed to reconnect to {alias}: {error_message}"
|
|
713
713
|
|
|
714
|
-
#
|
|
714
|
+
# Flush the /mcp command and result into scrollback.
|
|
715
715
|
self._renderer.seed_user_message("/mcp")
|
|
716
716
|
self._renderer.append_subtitle(message)
|
|
717
717
|
|
|
718
718
|
if success:
|
|
719
|
-
#
|
|
719
|
+
# Success: return to NORMAL mode without going back to the menu.
|
|
720
720
|
self._ui_mode = UIMode.NORMAL
|
|
721
721
|
self._sync_focus_for_mode()
|
|
722
722
|
self._refresh_layers()
|
|
723
723
|
else:
|
|
724
|
-
#
|
|
724
|
+
# Failure: return to the detail menu so the user can retry.
|
|
725
725
|
refreshed_rows = self._collect_mcp_cached_rows()
|
|
726
726
|
if refreshed_rows:
|
|
727
727
|
refreshed_by_alias = {str(r["alias"]): r for r in refreshed_rows}
|
|
@@ -1050,14 +1050,14 @@ class CommandsMixin:
|
|
|
1050
1050
|
)
|
|
1051
1051
|
except CustomSlashExpandError as exc:
|
|
1052
1052
|
self._append_slash_command_result(
|
|
1053
|
-
f"/{command_name}
|
|
1053
|
+
f"/{command_name} failed: {exc}",
|
|
1054
1054
|
severity="error",
|
|
1055
1055
|
)
|
|
1056
1056
|
return
|
|
1057
1057
|
except Exception as exc:
|
|
1058
1058
|
logger.exception("custom slash command rendering failed")
|
|
1059
1059
|
self._append_slash_command_result(
|
|
1060
|
-
f"/{command_name}
|
|
1060
|
+
f"/{command_name} failed: {exc}",
|
|
1061
1061
|
severity="error",
|
|
1062
1062
|
)
|
|
1063
1063
|
return
|
|
@@ -1090,10 +1090,11 @@ class CommandsMixin:
|
|
|
1090
1090
|
self._is_compacting = True
|
|
1091
1091
|
self._compact_cancel_requested = False
|
|
1092
1092
|
self._compact_task = asyncio.current_task()
|
|
1093
|
-
#
|
|
1094
|
-
#
|
|
1095
|
-
#
|
|
1096
|
-
#
|
|
1093
|
+
# Do not pre-seed the `> /compact` anchor here: compact must await a long task,
|
|
1094
|
+
# and seeding it early would leave the user entry's trailing blank line in scrollback
|
|
1095
|
+
# where the subsequent drain's `⎿` subtitle cannot pop it
|
|
1096
|
+
# (see the in-batch pop constraint in history_printer.py:223).
|
|
1097
|
+
# The anchor is written together with the subtitle by _append_slash_command_result in the result phase.
|
|
1097
1098
|
self._refresh_layers()
|
|
1098
1099
|
|
|
1099
1100
|
try:
|
|
@@ -1132,19 +1133,19 @@ class CommandsMixin:
|
|
|
1132
1133
|
|
|
1133
1134
|
if result.compacted:
|
|
1134
1135
|
self._append_slash_command_result(
|
|
1135
|
-
f"
|
|
1136
|
+
f"compact completed: {result.tokens_before:,} {INJECTED_ARROW} {result.tokens_after:,} tokens",
|
|
1136
1137
|
)
|
|
1137
1138
|
return
|
|
1138
1139
|
|
|
1139
1140
|
if not result.attempted:
|
|
1140
1141
|
self._append_slash_command_result(
|
|
1141
|
-
f"
|
|
1142
|
+
f"compact skipped: {result.reason}",
|
|
1142
1143
|
severity="warning",
|
|
1143
1144
|
)
|
|
1144
1145
|
return
|
|
1145
1146
|
|
|
1146
1147
|
self._append_slash_command_result(
|
|
1147
|
-
f"
|
|
1148
|
+
f"compact made no changes: {result.reason}",
|
|
1148
1149
|
severity="warning",
|
|
1149
1150
|
)
|
|
1150
1151
|
|
|
@@ -1157,7 +1158,7 @@ class CommandsMixin:
|
|
|
1157
1158
|
return
|
|
1158
1159
|
if self._busy:
|
|
1159
1160
|
self._append_slash_command_result(
|
|
1160
|
-
"
|
|
1161
|
+
"A task is currently running. Please try /rewind again later.",
|
|
1161
1162
|
severity="error",
|
|
1162
1163
|
)
|
|
1163
1164
|
return
|
|
@@ -1165,8 +1166,9 @@ class CommandsMixin:
|
|
|
1165
1166
|
targets = self._session.list_rewind_targets()
|
|
1166
1167
|
if not targets:
|
|
1167
1168
|
self._append_slash_command_result(
|
|
1168
|
-
"
|
|
1169
|
-
"
|
|
1169
|
+
"No rewind-able text messages are available. /rewind currently supports plain text "
|
|
1170
|
+
"messages from this session that can still be restored; images, team messages, "
|
|
1171
|
+
"queue-injected messages, and content already compacted into the summary are not supported."
|
|
1170
1172
|
)
|
|
1171
1173
|
return
|
|
1172
1174
|
self._show_rewind_target_menu(targets)
|
|
@@ -1175,7 +1177,7 @@ class CommandsMixin:
|
|
|
1175
1177
|
self._request_exit()
|
|
1176
1178
|
|
|
1177
1179
|
def _slash_plugin(self, args: str) -> None:
|
|
1178
|
-
"""Handle /plugin command —
|
|
1180
|
+
"""Handle /plugin command — enter the embedded plugin picker mode."""
|
|
1179
1181
|
from comate_agent_sdk.plugins import (
|
|
1180
1182
|
MarketplaceManager,
|
|
1181
1183
|
PluginLoader,
|
|
@@ -1188,7 +1190,7 @@ class CommandsMixin:
|
|
|
1188
1190
|
registry = create_plugin_registry()
|
|
1189
1191
|
mkt_mgr = MarketplaceManager(registry)
|
|
1190
1192
|
|
|
1191
|
-
#
|
|
1193
|
+
# Shortcut: /plugin marketplace add <repo> → go straight to the lightweight install panel.
|
|
1192
1194
|
if context.action == "add" and context.target_marketplace:
|
|
1193
1195
|
self._install_view.enter(
|
|
1194
1196
|
repo=context.target_marketplace,
|
|
@@ -1238,7 +1240,7 @@ class CommandsMixin:
|
|
|
1238
1240
|
self._append_slash_command_result(msg)
|
|
1239
1241
|
|
|
1240
1242
|
def _handle_plugin_action(self, action: PluginPickerAction | None) -> None:
|
|
1241
|
-
"""
|
|
1243
|
+
"""Handle the plugin picker handler return value and switch back to NORMAL mode."""
|
|
1242
1244
|
from comate_cli.terminal_agent.plugins.plugin_picker import PluginPickerAction
|
|
1243
1245
|
if action is None or not isinstance(action, PluginPickerAction):
|
|
1244
1246
|
return
|
|
@@ -1254,13 +1256,13 @@ class CommandsMixin:
|
|
|
1254
1256
|
self._refresh_layers()
|
|
1255
1257
|
|
|
1256
1258
|
def _exit_install_view(self) -> None:
|
|
1257
|
-
"""
|
|
1259
|
+
"""Close the marketplace install panel and switch back to NORMAL mode."""
|
|
1258
1260
|
repo = self._install_view._repo
|
|
1259
1261
|
result = self._install_view.take_result()
|
|
1260
1262
|
self._ui_mode = UIMode.NORMAL
|
|
1261
1263
|
self._sync_focus_for_mode()
|
|
1262
|
-
# seed_user_message + append_subtitle
|
|
1263
|
-
#
|
|
1264
|
+
# seed_user_message + append_subtitle must occur in the same drain batch;
|
|
1265
|
+
# otherwise the user entry's trailing blank line will remain in scrollback.
|
|
1264
1266
|
self._renderer.seed_user_message(f"/plugin marketplace add {repo}")
|
|
1265
1267
|
if result.installed:
|
|
1266
1268
|
self._renderer.append_subtitle(
|
|
@@ -1272,7 +1274,7 @@ class CommandsMixin:
|
|
|
1272
1274
|
self._refresh_layers()
|
|
1273
1275
|
|
|
1274
1276
|
def _on_marketplace_install_done(self) -> None:
|
|
1275
|
-
"""install_view
|
|
1277
|
+
"""Callback invoked after install_view succeeds — automatically closes the panel."""
|
|
1276
1278
|
self._exit_install_view()
|
|
1277
1279
|
|
|
1278
1280
|
def _slash_model(self, args: str) -> None:
|
|
@@ -1308,7 +1310,7 @@ class CommandsMixin:
|
|
|
1308
1310
|
)
|
|
1309
1311
|
logger.info(f"Model level switched: {event}")
|
|
1310
1312
|
|
|
1311
|
-
# Update status bar model name
|
|
1313
|
+
# Update status bar model name using the new model name from event.
|
|
1312
1314
|
self._status_bar.set_model_name(new_model)
|
|
1313
1315
|
self._invalidate()
|
|
1314
1316
|
except Exception as e:
|
|
@@ -1524,7 +1526,7 @@ class CommandsMixin:
|
|
|
1524
1526
|
return
|
|
1525
1527
|
if self._busy:
|
|
1526
1528
|
self._append_slash_command_result(
|
|
1527
|
-
"
|
|
1529
|
+
"A task is currently running. Please try /rewind again later.",
|
|
1528
1530
|
severity="error",
|
|
1529
1531
|
)
|
|
1530
1532
|
self._refresh_layers()
|
|
@@ -1623,7 +1625,7 @@ class CommandsMixin:
|
|
|
1623
1625
|
self._exit_selection_mode()
|
|
1624
1626
|
return
|
|
1625
1627
|
|
|
1626
|
-
#
|
|
1628
|
+
# The callback may have switched ui_mode (e.g., MCP_CONNECTING); respect its intent.
|
|
1627
1629
|
if self._ui_mode == UIMode.SELECTION:
|
|
1628
1630
|
self._exit_selection_mode()
|
|
1629
1631
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
import unittest
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
|
|
8
|
+
from prompt_toolkit.document import Document
|
|
9
|
+
|
|
10
|
+
from comate_cli.terminal_agent.mention_completer import LocalFileMentionCompleter
|
|
11
|
+
from comate_cli.terminal_agent.tui import TerminalAgentTUI
|
|
12
|
+
from comate_cli.terminal_agent.tui_parts import UIMode
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestCompletionContextActivation(unittest.TestCase):
|
|
16
|
+
def setUp(self) -> None:
|
|
17
|
+
self._tmpdir = tempfile.TemporaryDirectory()
|
|
18
|
+
self.tui = TerminalAgentTUI.__new__(TerminalAgentTUI)
|
|
19
|
+
self.tui._mention_completer = LocalFileMentionCompleter(
|
|
20
|
+
Path(self._tmpdir.name),
|
|
21
|
+
refresh_interval=0.0,
|
|
22
|
+
limit=200,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def tearDown(self) -> None:
|
|
26
|
+
self._tmpdir.cleanup()
|
|
27
|
+
|
|
28
|
+
def test_slash_completion_requires_cursor_at_end(self) -> None:
|
|
29
|
+
self.assertTrue(self.tui._completion_context_active("/model", ""))
|
|
30
|
+
self.assertFalse(self.tui._completion_context_active("/model", " trailing"))
|
|
31
|
+
|
|
32
|
+
def test_mention_completion_triggers_at_line_end(self) -> None:
|
|
33
|
+
self.assertTrue(self.tui._completion_context_active("message @file", ""))
|
|
34
|
+
|
|
35
|
+
def test_mention_completion_triggers_at_line_start(self) -> None:
|
|
36
|
+
self.assertTrue(self.tui._completion_context_active("@file", " message"))
|
|
37
|
+
|
|
38
|
+
def test_mention_completion_triggers_in_middle_with_space_boundaries(self) -> None:
|
|
39
|
+
self.assertTrue(
|
|
40
|
+
self.tui._completion_context_active("message @file", " message")
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def test_mention_completion_rejects_middle_without_right_space(self) -> None:
|
|
44
|
+
self.assertFalse(
|
|
45
|
+
self.tui._completion_context_active("message @file", "message")
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def test_mention_completion_rejects_middle_without_left_space(self) -> None:
|
|
49
|
+
self.assertFalse(
|
|
50
|
+
self.tui._completion_context_active("message@file", " message")
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def test_mention_completion_rejects_inside_token_editing(self) -> None:
|
|
54
|
+
self.assertFalse(
|
|
55
|
+
self.tui._completion_context_active("message @fi", "le message")
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def test_mention_cache_warmup_registers_completion_callback(self) -> None:
|
|
59
|
+
callbacks = []
|
|
60
|
+
|
|
61
|
+
class _FakeMentionCompleter:
|
|
62
|
+
def start_deep_cache_warmup(self, *, on_complete):
|
|
63
|
+
callbacks.append(on_complete)
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
self.tui._mention_completer = _FakeMentionCompleter()
|
|
67
|
+
|
|
68
|
+
self.tui._start_mention_cache_warmup()
|
|
69
|
+
|
|
70
|
+
self.assertEqual(len(callbacks), 1)
|
|
71
|
+
self.assertTrue(callable(callbacks[0]))
|
|
72
|
+
|
|
73
|
+
def test_warmup_callback_does_not_cancel_in_flight_completion(self) -> None:
|
|
74
|
+
class _FakeBuffer:
|
|
75
|
+
def __init__(self) -> None:
|
|
76
|
+
self.document = Document("@穆", cursor_position=2)
|
|
77
|
+
self.complete_state = SimpleNamespace(completions=[])
|
|
78
|
+
self.start_calls = 0
|
|
79
|
+
|
|
80
|
+
def start_completion(self, *, select_first: bool) -> None:
|
|
81
|
+
del select_first
|
|
82
|
+
self.start_calls += 1
|
|
83
|
+
|
|
84
|
+
buffer = _FakeBuffer()
|
|
85
|
+
self.tui._closing = False
|
|
86
|
+
self.tui._busy = False
|
|
87
|
+
self.tui._ui_mode = UIMode.NORMAL
|
|
88
|
+
self.tui._app = SimpleNamespace(invalidate=lambda: None)
|
|
89
|
+
self.tui._input_area = SimpleNamespace(buffer=buffer)
|
|
90
|
+
|
|
91
|
+
self.tui._restart_active_mention_completion()
|
|
92
|
+
|
|
93
|
+
self.assertEqual(buffer.start_calls, 0)
|
|
94
|
+
|
|
95
|
+
def test_warmup_callback_restarts_when_no_completion_is_active(self) -> None:
|
|
96
|
+
class _FakeBuffer:
|
|
97
|
+
def __init__(self) -> None:
|
|
98
|
+
self.document = Document("@穆", cursor_position=2)
|
|
99
|
+
self.complete_state = None
|
|
100
|
+
self.start_calls = 0
|
|
101
|
+
|
|
102
|
+
def start_completion(self, *, select_first: bool) -> None:
|
|
103
|
+
del select_first
|
|
104
|
+
self.start_calls += 1
|
|
105
|
+
|
|
106
|
+
buffer = _FakeBuffer()
|
|
107
|
+
self.tui._closing = False
|
|
108
|
+
self.tui._busy = False
|
|
109
|
+
self.tui._ui_mode = UIMode.NORMAL
|
|
110
|
+
self.tui._app = SimpleNamespace(invalidate=lambda: None)
|
|
111
|
+
self.tui._input_area = SimpleNamespace(buffer=buffer)
|
|
112
|
+
|
|
113
|
+
self.tui._restart_active_mention_completion()
|
|
114
|
+
|
|
115
|
+
self.assertEqual(buffer.start_calls, 1)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
unittest.main(verbosity=2)
|