comate-cli 0.7.3__tar.gz → 0.7.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.7.3 → comate_cli-0.7.4}/PKG-INFO +1 -1
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/status_bar.py +16 -3
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui.py +55 -10
- {comate_cli-0.7.3 → comate_cli-0.7.4}/pyproject.toml +1 -1
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_status_bar.py +79 -8
- comate_cli-0.7.4/tests/test_tui_startup_latency.py +274 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/uv.lock +2 -2
- comate_cli-0.7.3/tests/test_tui_startup_latency.py +0 -104
- {comate_cli-0.7.3 → comate_cli-0.7.4}/.gitignore +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/CHANGELOG.md +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/README.md +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/__init__.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/__main__.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/main.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/app.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/config/__init__.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/config/model.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/config/picker.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/config/picker_state.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/config/store.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/event_renderer.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/figures.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/goal_resume_view.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/history_printer.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/resume_picker.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/resume_preview.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/startup_profile.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/statusline/__init__.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/statusline/model.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/statusline/picker.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/statusline/picker_state.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/statusline/store.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tool_fold.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tool_result_formatters.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tool_result_store.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tool_view.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/comate_cli/terminal_agent/update_check.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/config/__init__.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/config/test_model.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/config/test_picker_state.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/config/test_picker_ui.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/config/test_roundtrip.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/config/test_store_load.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/config/test_store_save.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/conftest.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/fixtures/fake_mcp_misbehaving_stdout.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/statusline/__init__.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/statusline/test_model.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/statusline/test_picker_state.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/statusline/test_store.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_app_startup_latency.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_app_token_cost_config.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_btw_slash_command.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_completion_status_panel.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_context_command.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_event_renderer.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_event_renderer_boundary.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_event_renderer_e2e.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_event_renderer_log_boundary.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_event_renderer_log_queue.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_event_renderer_streaming.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_event_renderer_tool_fold.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_format_error.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_goal_resume_tui.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_goal_resume_view.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_goal_slash_command.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_handle_error.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_history_printer.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_history_printer_log.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_history_printer_subtitle_position.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_history_printer_tool_fold.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_history_sync.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_history_sync_tool_fold.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_input_history.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_logo.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_main_args.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_markdown_render.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_preflight.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_question_view.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_resume_picker.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_resume_preview.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_session_query_token_summary.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_shutdown_noise_guard.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_shutdown_noise_integration.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_slash_clear.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_startup_profile.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_task_poll.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tool_fold.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tool_fold_panel.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tool_result_formatters.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tool_result_store.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tool_result_viewer.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tool_result_viewer_key_bindings.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tool_view.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_transcript_viewer.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_transcript_viewer_tool_fold.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_esc_queue.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_paste_newline_guard.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_queue_preview.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_queue_sdk_source.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_team_messages.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_thinking_display.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_update_check.py +0 -0
- {comate_cli-0.7.3 → comate_cli-0.7.4}/tests/test_usage_command.py +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import logging
|
|
4
5
|
import subprocess
|
|
5
6
|
import time
|
|
@@ -40,7 +41,7 @@ class StatusBar:
|
|
|
40
41
|
self._session = session
|
|
41
42
|
self._model_name: str = self._resolve_model_name(session)
|
|
42
43
|
self._mode: str = "act"
|
|
43
|
-
self._git_branch: str =
|
|
44
|
+
self._git_branch: str = "N/A"
|
|
44
45
|
self._context_used_pct: float = 0.0
|
|
45
46
|
self._context_left_pct: float = 100.0
|
|
46
47
|
self._git_diff_stats: GitDiffStats | None = None
|
|
@@ -274,8 +275,20 @@ class StatusBar:
|
|
|
274
275
|
self._context_used_pct = normalized
|
|
275
276
|
self._context_left_pct = max(0.0, 100.0 - normalized)
|
|
276
277
|
|
|
277
|
-
|
|
278
|
-
|
|
278
|
+
branch_task = asyncio.to_thread(self._resolve_git_branch)
|
|
279
|
+
now = time.monotonic()
|
|
280
|
+
diff_is_stale = (
|
|
281
|
+
self._git_diff_stats is None
|
|
282
|
+
or now - self._git_diff_cache_time >= self._GIT_DIFF_CACHE_SECONDS
|
|
283
|
+
)
|
|
284
|
+
if diff_is_stale:
|
|
285
|
+
diff_task = asyncio.to_thread(self._resolve_git_diff_stats)
|
|
286
|
+
branch, diff_stats = await asyncio.gather(branch_task, diff_task)
|
|
287
|
+
self._git_diff_stats = diff_stats
|
|
288
|
+
self._git_diff_cache_time = time.monotonic()
|
|
289
|
+
else:
|
|
290
|
+
branch = await branch_task
|
|
291
|
+
self._git_branch = branch
|
|
279
292
|
|
|
280
293
|
# Refresh token usage cache if any token-related items are enabled.
|
|
281
294
|
cfg = self._config
|
|
@@ -139,6 +139,16 @@ def _truncate_file_history(path: Path, max_entries: int = 200) -> None:
|
|
|
139
139
|
path.write_bytes(rewritten_bytes)
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
def _load_plugins_blocking(project_path: Path) -> tuple[list[Any], list[Any]]:
|
|
143
|
+
from comate_agent_sdk.plugins import PluginLoader, create_plugin_registry
|
|
144
|
+
|
|
145
|
+
loader = PluginLoader()
|
|
146
|
+
registry = create_plugin_registry()
|
|
147
|
+
data_dir = registry.base_dir / "data"
|
|
148
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
return loader.load_all(registry=registry, project_path=project_path)
|
|
150
|
+
|
|
151
|
+
|
|
142
152
|
class TerminalAgentTUI(
|
|
143
153
|
KeyBindingsMixin,
|
|
144
154
|
InputBehaviorMixin,
|
|
@@ -266,6 +276,7 @@ class TerminalAgentTUI(
|
|
|
266
276
|
self._ui_tick_task: asyncio.Task[None] | None = None
|
|
267
277
|
self._mcp_init_task: asyncio.Task[None] | None = None
|
|
268
278
|
self._plugin_init_task: asyncio.Task[None] | None = None
|
|
279
|
+
self._startup_status_refresh_task: asyncio.Task[None] | None = None
|
|
269
280
|
self._plugin_init_cancel_timeout_s = 1.0
|
|
270
281
|
self._interrupt_requested_at: float | None = None
|
|
271
282
|
self._interrupt_force_window_seconds = 1.5
|
|
@@ -1801,6 +1812,24 @@ class TerminalAgentTUI(
|
|
|
1801
1812
|
self._ui_tick(),
|
|
1802
1813
|
name="terminal-ui-tick",
|
|
1803
1814
|
)
|
|
1815
|
+
status_bar = getattr(self, "_status_bar", None)
|
|
1816
|
+
if status_bar is not None:
|
|
1817
|
+
async def _do_startup_status_refresh() -> None:
|
|
1818
|
+
try:
|
|
1819
|
+
await status_bar.refresh()
|
|
1820
|
+
except asyncio.CancelledError:
|
|
1821
|
+
raise
|
|
1822
|
+
except Exception:
|
|
1823
|
+
logger.debug("startup status refresh failed", exc_info=True)
|
|
1824
|
+
finally:
|
|
1825
|
+
if not self._closing:
|
|
1826
|
+
self._refresh_layers()
|
|
1827
|
+
self._invalidate()
|
|
1828
|
+
|
|
1829
|
+
self._startup_status_refresh_task = asyncio.create_task(
|
|
1830
|
+
_do_startup_status_refresh(),
|
|
1831
|
+
name="terminal-startup-status-refresh",
|
|
1832
|
+
)
|
|
1804
1833
|
if hasattr(self, "_session") and self._session is not None:
|
|
1805
1834
|
self._event_pump_task = asyncio.create_task(
|
|
1806
1835
|
self._consume_event_stream(),
|
|
@@ -1841,6 +1870,16 @@ class TerminalAgentTUI(
|
|
|
1841
1870
|
self._ui_tick_task.cancel()
|
|
1842
1871
|
with suppress(asyncio.CancelledError):
|
|
1843
1872
|
await self._ui_tick_task
|
|
1873
|
+
startup_status_refresh_task = getattr(
|
|
1874
|
+
self,
|
|
1875
|
+
"_startup_status_refresh_task",
|
|
1876
|
+
None,
|
|
1877
|
+
)
|
|
1878
|
+
if startup_status_refresh_task is not None:
|
|
1879
|
+
startup_status_refresh_task.cancel()
|
|
1880
|
+
with suppress(asyncio.CancelledError):
|
|
1881
|
+
await startup_status_refresh_task
|
|
1882
|
+
self._startup_status_refresh_task = None
|
|
1844
1883
|
event_pump_task = getattr(self, "_event_pump_task", None)
|
|
1845
1884
|
if event_pump_task is not None:
|
|
1846
1885
|
event_pump_task.cancel()
|
|
@@ -1848,19 +1887,25 @@ class TerminalAgentTUI(
|
|
|
1848
1887
|
await event_pump_task
|
|
1849
1888
|
self._renderer.close()
|
|
1850
1889
|
|
|
1890
|
+
def _plugin_project_path(self) -> Path:
|
|
1891
|
+
session = getattr(self, "_session", None)
|
|
1892
|
+
session_cwd = getattr(session, "_cwd", None)
|
|
1893
|
+
if session_cwd:
|
|
1894
|
+
return Path(session_cwd).expanduser().resolve()
|
|
1895
|
+
return Path.cwd().expanduser().resolve()
|
|
1896
|
+
|
|
1851
1897
|
async def _init_plugins(self) -> None:
|
|
1852
1898
|
"""Load and inject plugin resources at startup."""
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
registry = create_plugin_registry()
|
|
1857
|
-
# Ensure plugin data directory exists
|
|
1858
|
-
data_dir = registry.base_dir / "data"
|
|
1859
|
-
data_dir.mkdir(parents=True, exist_ok=True)
|
|
1860
|
-
self._loaded_plugins, self._plugin_errors = loader.load_all(
|
|
1861
|
-
registry=registry,
|
|
1862
|
-
project_path=Path.cwd(),
|
|
1899
|
+
loaded_plugins, plugin_errors = await asyncio.to_thread(
|
|
1900
|
+
_load_plugins_blocking,
|
|
1901
|
+
self._plugin_project_path(),
|
|
1863
1902
|
)
|
|
1903
|
+
if self._closing:
|
|
1904
|
+
logger.debug("Plugin load completed after TUI close; discarding results")
|
|
1905
|
+
return
|
|
1906
|
+
|
|
1907
|
+
self._loaded_plugins = loaded_plugins
|
|
1908
|
+
self._plugin_errors = plugin_errors
|
|
1864
1909
|
self._plugin_command_names: set[str] = set()
|
|
1865
1910
|
self._inject_plugin_resources(self._loaded_plugins)
|
|
1866
1911
|
logger.info(
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import subprocess
|
|
5
|
+
import time
|
|
4
6
|
import unittest
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
from types import SimpleNamespace
|
|
7
9
|
from unittest.mock import patch
|
|
8
10
|
|
|
9
11
|
from comate_agent_sdk.agent.goal_state import GoalSnapshot, GoalStatus
|
|
10
|
-
from comate_cli.terminal_agent.status_bar import StatusBar
|
|
12
|
+
from comate_cli.terminal_agent.status_bar import GitDiffStats, StatusBar
|
|
11
13
|
from comate_cli.terminal_agent.statusline.model import StatuslineConfig
|
|
12
14
|
|
|
13
15
|
|
|
@@ -74,15 +76,14 @@ def _goal_snapshot(
|
|
|
74
76
|
|
|
75
77
|
class TestStatusBar(unittest.IsolatedAsyncioTestCase):
|
|
76
78
|
@patch("comate_cli.terminal_agent.status_bar.subprocess.run")
|
|
77
|
-
async def
|
|
78
|
-
mock_run.return_value = SimpleNamespace(returncode=0, stdout="my-branch\n")
|
|
79
|
+
async def test_constructor_does_not_resolve_git_branch_immediately(self, mock_run) -> None:
|
|
79
80
|
session = _FakeSession(utilization_percent=0.0)
|
|
80
81
|
|
|
81
82
|
status_bar = StatusBar(session)
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
self.assertEqual(status_bar._git_branch, "
|
|
85
|
-
self.assertIn("~
|
|
84
|
+
mock_run.assert_not_called()
|
|
85
|
+
self.assertEqual(status_bar._git_branch, "N/A")
|
|
86
|
+
self.assertIn("~N/A", status_bar.footer_status_text())
|
|
86
87
|
|
|
87
88
|
@patch("comate_cli.terminal_agent.status_bar.subprocess.run")
|
|
88
89
|
async def test_refresh_updates_cached_context_percent(self, mock_run) -> None:
|
|
@@ -157,7 +158,6 @@ class TestStatusBar(unittest.IsolatedAsyncioTestCase):
|
|
|
157
158
|
@patch("comate_cli.terminal_agent.status_bar.subprocess.run")
|
|
158
159
|
async def test_render_uses_cached_git_diff_stats_without_new_subprocess(self, mock_run) -> None:
|
|
159
160
|
mock_run.side_effect = [
|
|
160
|
-
SimpleNamespace(returncode=0, stdout="main\n"),
|
|
161
161
|
SimpleNamespace(returncode=0, stdout="main\n"),
|
|
162
162
|
SimpleNamespace(
|
|
163
163
|
returncode=0,
|
|
@@ -169,7 +169,7 @@ class TestStatusBar(unittest.IsolatedAsyncioTestCase):
|
|
|
169
169
|
status_bar.apply_config(StatuslineConfig(show_git_diff=True))
|
|
170
170
|
|
|
171
171
|
await status_bar.refresh()
|
|
172
|
-
self.assertEqual(mock_run.call_count,
|
|
172
|
+
self.assertEqual(mock_run.call_count, 2)
|
|
173
173
|
|
|
174
174
|
mock_run.reset_mock()
|
|
175
175
|
footer = _join_fragments(status_bar.footer_toolbar())
|
|
@@ -178,6 +178,77 @@ class TestStatusBar(unittest.IsolatedAsyncioTestCase):
|
|
|
178
178
|
self.assertIn("-1", footer)
|
|
179
179
|
mock_run.assert_not_called()
|
|
180
180
|
|
|
181
|
+
async def test_refresh_git_commands_do_not_block_event_loop(self) -> None:
|
|
182
|
+
session = _FakeSession(utilization_percent=10.0)
|
|
183
|
+
status_bar = StatusBar(session)
|
|
184
|
+
git_done = False
|
|
185
|
+
ticked_before_git_done = False
|
|
186
|
+
|
|
187
|
+
def slow_branch() -> str:
|
|
188
|
+
time.sleep(0.05)
|
|
189
|
+
return "main"
|
|
190
|
+
|
|
191
|
+
def slow_diff() -> GitDiffStats | None:
|
|
192
|
+
nonlocal git_done
|
|
193
|
+
time.sleep(0.05)
|
|
194
|
+
git_done = True
|
|
195
|
+
return GitDiffStats(added=3, removed=1)
|
|
196
|
+
|
|
197
|
+
ticked = asyncio.Event()
|
|
198
|
+
|
|
199
|
+
async def ticker() -> None:
|
|
200
|
+
nonlocal ticked_before_git_done
|
|
201
|
+
await asyncio.sleep(0)
|
|
202
|
+
ticked_before_git_done = not git_done
|
|
203
|
+
ticked.set()
|
|
204
|
+
|
|
205
|
+
with (
|
|
206
|
+
patch.object(StatusBar, "_resolve_git_branch", side_effect=slow_branch),
|
|
207
|
+
patch.object(StatusBar, "_resolve_git_diff_stats", side_effect=slow_diff),
|
|
208
|
+
):
|
|
209
|
+
await asyncio.gather(status_bar.refresh(), ticker())
|
|
210
|
+
|
|
211
|
+
self.assertTrue(ticked.is_set())
|
|
212
|
+
self.assertTrue(ticked_before_git_done)
|
|
213
|
+
self.assertEqual(status_bar._git_branch, "main")
|
|
214
|
+
self.assertEqual(status_bar._git_diff_stats, GitDiffStats(added=3, removed=1))
|
|
215
|
+
|
|
216
|
+
async def test_refresh_does_not_recompute_git_diff_when_cache_is_fresh(self) -> None:
|
|
217
|
+
session = _FakeSession(utilization_percent=10.0)
|
|
218
|
+
status_bar = StatusBar(session)
|
|
219
|
+
status_bar._git_diff_stats = GitDiffStats(added=1, removed=0)
|
|
220
|
+
status_bar._git_diff_cache_time = time.monotonic()
|
|
221
|
+
|
|
222
|
+
with (
|
|
223
|
+
patch.object(StatusBar, "_resolve_git_branch", return_value="main"),
|
|
224
|
+
patch.object(StatusBar, "_resolve_git_diff_stats") as diff_mock,
|
|
225
|
+
):
|
|
226
|
+
await status_bar.refresh()
|
|
227
|
+
|
|
228
|
+
diff_mock.assert_not_called()
|
|
229
|
+
self.assertEqual(status_bar._git_diff_stats, GitDiffStats(added=1, removed=0))
|
|
230
|
+
|
|
231
|
+
async def test_refresh_recomputes_git_diff_when_cache_is_stale(self) -> None:
|
|
232
|
+
session = _FakeSession(utilization_percent=10.0)
|
|
233
|
+
status_bar = StatusBar(session)
|
|
234
|
+
status_bar._git_diff_stats = GitDiffStats(added=1, removed=0)
|
|
235
|
+
status_bar._git_diff_cache_time = (
|
|
236
|
+
time.monotonic() - StatusBar._GIT_DIFF_CACHE_SECONDS - 1
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
with (
|
|
240
|
+
patch.object(StatusBar, "_resolve_git_branch", return_value="main"),
|
|
241
|
+
patch.object(
|
|
242
|
+
StatusBar,
|
|
243
|
+
"_resolve_git_diff_stats",
|
|
244
|
+
return_value=GitDiffStats(added=2, removed=1),
|
|
245
|
+
) as diff_mock,
|
|
246
|
+
):
|
|
247
|
+
await status_bar.refresh()
|
|
248
|
+
|
|
249
|
+
diff_mock.assert_called_once()
|
|
250
|
+
self.assertEqual(status_bar._git_diff_stats, GitDiffStats(added=2, removed=1))
|
|
251
|
+
|
|
181
252
|
@patch("comate_cli.terminal_agent.status_bar.subprocess.run")
|
|
182
253
|
async def test_goal_budget_item_uses_cached_snapshot(self, mock_run) -> None:
|
|
183
254
|
mock_run.return_value = SimpleNamespace(returncode=0, stdout="main\n")
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import threading
|
|
5
|
+
import unittest
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
from typing import Any
|
|
10
|
+
from unittest.mock import patch
|
|
11
|
+
|
|
12
|
+
from comate_cli.terminal_agent.tui import TerminalAgentTUI
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _FakeRenderer:
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
self.closed = False
|
|
18
|
+
|
|
19
|
+
def close(self) -> None:
|
|
20
|
+
self.closed = True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _FakeApp:
|
|
24
|
+
def __init__(self, *, wait_for: asyncio.Event | None = None) -> None:
|
|
25
|
+
self.entered = asyncio.Event()
|
|
26
|
+
self._wait_for = wait_for
|
|
27
|
+
self.invalidated = False
|
|
28
|
+
|
|
29
|
+
async def run_async(self) -> None:
|
|
30
|
+
self.entered.set()
|
|
31
|
+
if self._wait_for is not None:
|
|
32
|
+
await asyncio.wait_for(self._wait_for.wait(), timeout=0.1)
|
|
33
|
+
else:
|
|
34
|
+
await asyncio.sleep(0)
|
|
35
|
+
|
|
36
|
+
def invalidate(self) -> None:
|
|
37
|
+
self.invalidated = True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@contextmanager
|
|
41
|
+
def _noop_patch_stdout(*args: Any, **kwargs: Any):
|
|
42
|
+
del args, kwargs
|
|
43
|
+
yield
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _make_tui(app: _FakeApp) -> TerminalAgentTUI:
|
|
47
|
+
tui = TerminalAgentTUI.__new__(TerminalAgentTUI)
|
|
48
|
+
tui._app = app
|
|
49
|
+
tui._renderer = _FakeRenderer()
|
|
50
|
+
tui._refresh_layers = lambda: None
|
|
51
|
+
tui._busy = False
|
|
52
|
+
tui._closing = False
|
|
53
|
+
tui._mcp_init_cancel_timeout_s = 0.05
|
|
54
|
+
tui._mcp_init_task = None
|
|
55
|
+
tui._plugin_init_task = None
|
|
56
|
+
tui._plugin_init_cancel_timeout_s = 0.05
|
|
57
|
+
tui._ui_tick_task = None
|
|
58
|
+
tui._event_pump_task = None
|
|
59
|
+
|
|
60
|
+
async def _fake_ui_tick() -> None:
|
|
61
|
+
await asyncio.Event().wait()
|
|
62
|
+
|
|
63
|
+
async def _fake_event_pump() -> None:
|
|
64
|
+
await asyncio.Event().wait()
|
|
65
|
+
|
|
66
|
+
tui._ui_tick = _fake_ui_tick
|
|
67
|
+
tui._consume_event_stream = _fake_event_pump
|
|
68
|
+
return tui
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TestTUIStartupLatency(unittest.IsolatedAsyncioTestCase):
|
|
72
|
+
async def test_plugin_init_pending_does_not_block_run_async(self) -> None:
|
|
73
|
+
plugin_started = asyncio.Event()
|
|
74
|
+
plugin_cancelled = asyncio.Event()
|
|
75
|
+
app = _FakeApp(wait_for=plugin_started)
|
|
76
|
+
tui = _make_tui(app)
|
|
77
|
+
|
|
78
|
+
async def _stuck_plugin_init() -> None:
|
|
79
|
+
plugin_started.set()
|
|
80
|
+
try:
|
|
81
|
+
await asyncio.Event().wait()
|
|
82
|
+
finally:
|
|
83
|
+
plugin_cancelled.set()
|
|
84
|
+
|
|
85
|
+
tui._init_plugins = _stuck_plugin_init
|
|
86
|
+
|
|
87
|
+
with patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout):
|
|
88
|
+
await asyncio.wait_for(tui.run(), timeout=0.2)
|
|
89
|
+
|
|
90
|
+
self.assertTrue(plugin_started.is_set())
|
|
91
|
+
self.assertTrue(app.entered.is_set())
|
|
92
|
+
self.assertTrue(plugin_cancelled.is_set())
|
|
93
|
+
self.assertIsNone(tui._plugin_init_task)
|
|
94
|
+
self.assertTrue(tui._renderer.closed)
|
|
95
|
+
|
|
96
|
+
async def test_plugin_init_exception_is_logged_and_run_async_still_runs(self) -> None:
|
|
97
|
+
app = _FakeApp()
|
|
98
|
+
tui = _make_tui(app)
|
|
99
|
+
|
|
100
|
+
async def _failing_plugin_init() -> None:
|
|
101
|
+
raise RuntimeError("plugin boom")
|
|
102
|
+
|
|
103
|
+
tui._init_plugins = _failing_plugin_init
|
|
104
|
+
|
|
105
|
+
with (
|
|
106
|
+
patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout),
|
|
107
|
+
self.assertLogs("comate_cli.terminal_agent.tui", level="ERROR") as logs,
|
|
108
|
+
):
|
|
109
|
+
await asyncio.wait_for(tui.run(), timeout=0.2)
|
|
110
|
+
|
|
111
|
+
self.assertTrue(app.entered.is_set())
|
|
112
|
+
self.assertIn("Plugin init failed at startup", "\n".join(logs.output))
|
|
113
|
+
|
|
114
|
+
async def test_sync_plugin_loader_runs_off_event_loop(self) -> None:
|
|
115
|
+
load_started = threading.Event()
|
|
116
|
+
release_load = threading.Event()
|
|
117
|
+
app_release = asyncio.Event()
|
|
118
|
+
app = _FakeApp(wait_for=app_release)
|
|
119
|
+
tui = _make_tui(app)
|
|
120
|
+
tui._inject_plugin_resources = lambda plugins: None
|
|
121
|
+
|
|
122
|
+
def slow_load_all(*args, **kwargs): # noqa: ANN001, ANN202
|
|
123
|
+
del args, kwargs
|
|
124
|
+
load_started.set()
|
|
125
|
+
release_load.wait(timeout=1.0)
|
|
126
|
+
return [], []
|
|
127
|
+
|
|
128
|
+
fake_registry = SimpleNamespace(base_dir=Path("/tmp/plugin-registry"))
|
|
129
|
+
|
|
130
|
+
with (
|
|
131
|
+
patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout),
|
|
132
|
+
patch("comate_agent_sdk.plugins.create_plugin_registry", return_value=fake_registry),
|
|
133
|
+
patch("comate_agent_sdk.plugins.PluginLoader.load_all", side_effect=slow_load_all),
|
|
134
|
+
):
|
|
135
|
+
run_task = asyncio.create_task(tui.run())
|
|
136
|
+
self.assertTrue(await asyncio.to_thread(load_started.wait, 0.2))
|
|
137
|
+
await asyncio.wait_for(app.entered.wait(), timeout=0.1)
|
|
138
|
+
self.assertFalse(run_task.done())
|
|
139
|
+
release_load.set()
|
|
140
|
+
app_release.set()
|
|
141
|
+
await asyncio.wait_for(run_task, timeout=0.5)
|
|
142
|
+
|
|
143
|
+
async def test_plugin_loader_uses_session_cwd_as_project_path(self) -> None:
|
|
144
|
+
app_release = asyncio.Event()
|
|
145
|
+
app = _FakeApp(wait_for=app_release)
|
|
146
|
+
tui = _make_tui(app)
|
|
147
|
+
tui._session = SimpleNamespace(_cwd="/tmp/session-project")
|
|
148
|
+
tui._inject_plugin_resources = lambda plugins: None
|
|
149
|
+
seen_project_paths: list[Path] = []
|
|
150
|
+
load_called = threading.Event()
|
|
151
|
+
|
|
152
|
+
def load_all(*, registry, project_path, **kwargs): # noqa: ANN001, ANN202
|
|
153
|
+
del registry, kwargs
|
|
154
|
+
seen_project_paths.append(Path(project_path))
|
|
155
|
+
load_called.set()
|
|
156
|
+
return [], []
|
|
157
|
+
|
|
158
|
+
fake_registry = SimpleNamespace(base_dir=Path("/tmp/plugin-registry"))
|
|
159
|
+
|
|
160
|
+
with (
|
|
161
|
+
patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout),
|
|
162
|
+
patch("comate_agent_sdk.plugins.create_plugin_registry", return_value=fake_registry),
|
|
163
|
+
patch("comate_agent_sdk.plugins.PluginLoader.load_all", side_effect=load_all),
|
|
164
|
+
):
|
|
165
|
+
run_task = asyncio.create_task(tui.run())
|
|
166
|
+
self.assertTrue(await asyncio.to_thread(load_called.wait, 0.2))
|
|
167
|
+
app_release.set()
|
|
168
|
+
await asyncio.wait_for(run_task, timeout=0.5)
|
|
169
|
+
|
|
170
|
+
self.assertEqual(seen_project_paths, [Path("/tmp/session-project").resolve()])
|
|
171
|
+
|
|
172
|
+
async def test_cancelled_plugin_loader_does_not_inject_after_close(self) -> None:
|
|
173
|
+
load_started = threading.Event()
|
|
174
|
+
release_load = threading.Event()
|
|
175
|
+
app_release = asyncio.Event()
|
|
176
|
+
app = _FakeApp(wait_for=app_release)
|
|
177
|
+
tui = _make_tui(app)
|
|
178
|
+
injected: list[object] = []
|
|
179
|
+
tui._inject_plugin_resources = lambda plugins: injected.extend(plugins)
|
|
180
|
+
|
|
181
|
+
loaded_plugin = SimpleNamespace(
|
|
182
|
+
namespace="slow",
|
|
183
|
+
skills=[],
|
|
184
|
+
agents=[],
|
|
185
|
+
commands=[],
|
|
186
|
+
mcp_servers={},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def slow_load_all(*args, **kwargs): # noqa: ANN001, ANN202
|
|
190
|
+
del args, kwargs
|
|
191
|
+
load_started.set()
|
|
192
|
+
release_load.wait(timeout=1.0)
|
|
193
|
+
return [loaded_plugin], []
|
|
194
|
+
|
|
195
|
+
fake_registry = SimpleNamespace(base_dir=Path("/tmp/plugin-registry"))
|
|
196
|
+
|
|
197
|
+
with (
|
|
198
|
+
patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout),
|
|
199
|
+
patch("comate_agent_sdk.plugins.create_plugin_registry", return_value=fake_registry),
|
|
200
|
+
patch("comate_agent_sdk.plugins.PluginLoader.load_all", side_effect=slow_load_all),
|
|
201
|
+
):
|
|
202
|
+
run_task = asyncio.create_task(tui.run())
|
|
203
|
+
self.assertTrue(await asyncio.to_thread(load_started.wait, 0.2))
|
|
204
|
+
await asyncio.wait_for(app.entered.wait(), timeout=0.1)
|
|
205
|
+
app_release.set()
|
|
206
|
+
await asyncio.sleep(0)
|
|
207
|
+
release_load.set()
|
|
208
|
+
await asyncio.wait_for(run_task, timeout=0.5)
|
|
209
|
+
|
|
210
|
+
self.assertEqual(injected, [])
|
|
211
|
+
|
|
212
|
+
async def test_startup_status_refresh_does_not_block_run_async(self) -> None:
|
|
213
|
+
refresh_started = asyncio.Event()
|
|
214
|
+
release_refresh = asyncio.Event()
|
|
215
|
+
app_release = asyncio.Event()
|
|
216
|
+
app = _FakeApp(wait_for=app_release)
|
|
217
|
+
tui = _make_tui(app)
|
|
218
|
+
tui._init_plugins = lambda: asyncio.sleep(0)
|
|
219
|
+
|
|
220
|
+
class _StatusBar:
|
|
221
|
+
def __init__(self) -> None:
|
|
222
|
+
self.refreshed = False
|
|
223
|
+
|
|
224
|
+
async def refresh(self) -> None:
|
|
225
|
+
refresh_started.set()
|
|
226
|
+
await release_refresh.wait()
|
|
227
|
+
self.refreshed = True
|
|
228
|
+
|
|
229
|
+
status_bar = _StatusBar()
|
|
230
|
+
tui._status_bar = status_bar
|
|
231
|
+
|
|
232
|
+
with patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout):
|
|
233
|
+
run_task = asyncio.create_task(tui.run())
|
|
234
|
+
await asyncio.wait_for(app.entered.wait(), timeout=0.1)
|
|
235
|
+
await asyncio.wait_for(refresh_started.wait(), timeout=0.1)
|
|
236
|
+
self.assertFalse(run_task.done())
|
|
237
|
+
release_refresh.set()
|
|
238
|
+
app_release.set()
|
|
239
|
+
await asyncio.wait_for(run_task, timeout=0.5)
|
|
240
|
+
|
|
241
|
+
self.assertTrue(status_bar.refreshed)
|
|
242
|
+
self.assertTrue(app.invalidated)
|
|
243
|
+
|
|
244
|
+
async def test_pending_startup_status_refresh_is_cancelled_on_shutdown(self) -> None:
|
|
245
|
+
refresh_started = asyncio.Event()
|
|
246
|
+
refresh_cancelled = asyncio.Event()
|
|
247
|
+
app_release = asyncio.Event()
|
|
248
|
+
app = _FakeApp(wait_for=app_release)
|
|
249
|
+
tui = _make_tui(app)
|
|
250
|
+
tui._init_plugins = lambda: asyncio.sleep(0)
|
|
251
|
+
|
|
252
|
+
class _StatusBar:
|
|
253
|
+
async def refresh(self) -> None:
|
|
254
|
+
refresh_started.set()
|
|
255
|
+
try:
|
|
256
|
+
await asyncio.Event().wait()
|
|
257
|
+
finally:
|
|
258
|
+
refresh_cancelled.set()
|
|
259
|
+
|
|
260
|
+
tui._status_bar = _StatusBar()
|
|
261
|
+
|
|
262
|
+
with patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout):
|
|
263
|
+
run_task = asyncio.create_task(tui.run())
|
|
264
|
+
await asyncio.wait_for(app.entered.wait(), timeout=0.1)
|
|
265
|
+
await asyncio.wait_for(refresh_started.wait(), timeout=0.1)
|
|
266
|
+
app_release.set()
|
|
267
|
+
await asyncio.wait_for(run_task, timeout=0.5)
|
|
268
|
+
|
|
269
|
+
self.assertTrue(refresh_cancelled.is_set())
|
|
270
|
+
self.assertIsNone(tui._startup_status_refresh_task)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__":
|
|
274
|
+
unittest.main(verbosity=2)
|
|
@@ -389,7 +389,7 @@ wheels = [
|
|
|
389
389
|
|
|
390
390
|
[[package]]
|
|
391
391
|
name = "comate-agent-sdk"
|
|
392
|
-
version = "0.8.
|
|
392
|
+
version = "0.8.4"
|
|
393
393
|
source = { editable = "../../" }
|
|
394
394
|
dependencies = [
|
|
395
395
|
{ name = "aiohttp" },
|
|
@@ -463,7 +463,7 @@ dev = [
|
|
|
463
463
|
|
|
464
464
|
[[package]]
|
|
465
465
|
name = "comate-cli"
|
|
466
|
-
version = "0.7.
|
|
466
|
+
version = "0.7.3"
|
|
467
467
|
source = { editable = "." }
|
|
468
468
|
dependencies = [
|
|
469
469
|
{ name = "charset-normalizer" },
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import unittest
|
|
5
|
-
from contextlib import contextmanager
|
|
6
|
-
from typing import Any
|
|
7
|
-
from unittest.mock import patch
|
|
8
|
-
|
|
9
|
-
from comate_cli.terminal_agent.tui import TerminalAgentTUI
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class _FakeRenderer:
|
|
13
|
-
def __init__(self) -> None:
|
|
14
|
-
self.closed = False
|
|
15
|
-
|
|
16
|
-
def close(self) -> None:
|
|
17
|
-
self.closed = True
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class _FakeApp:
|
|
21
|
-
def __init__(self, *, wait_for: asyncio.Event | None = None) -> None:
|
|
22
|
-
self.entered = asyncio.Event()
|
|
23
|
-
self._wait_for = wait_for
|
|
24
|
-
|
|
25
|
-
async def run_async(self) -> None:
|
|
26
|
-
self.entered.set()
|
|
27
|
-
if self._wait_for is not None:
|
|
28
|
-
await asyncio.wait_for(self._wait_for.wait(), timeout=0.1)
|
|
29
|
-
else:
|
|
30
|
-
await asyncio.sleep(0)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@contextmanager
|
|
34
|
-
def _noop_patch_stdout(*args: Any, **kwargs: Any):
|
|
35
|
-
del args, kwargs
|
|
36
|
-
yield
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _make_tui(app: _FakeApp) -> TerminalAgentTUI:
|
|
40
|
-
tui = TerminalAgentTUI.__new__(TerminalAgentTUI)
|
|
41
|
-
tui._app = app
|
|
42
|
-
tui._renderer = _FakeRenderer()
|
|
43
|
-
tui._refresh_layers = lambda: None
|
|
44
|
-
tui._busy = False
|
|
45
|
-
tui._closing = False
|
|
46
|
-
tui._mcp_init_cancel_timeout_s = 0.05
|
|
47
|
-
tui._mcp_init_task = None
|
|
48
|
-
tui._plugin_init_task = None
|
|
49
|
-
tui._plugin_init_cancel_timeout_s = 0.05
|
|
50
|
-
tui._ui_tick_task = None
|
|
51
|
-
|
|
52
|
-
async def _fake_ui_tick() -> None:
|
|
53
|
-
await asyncio.Event().wait()
|
|
54
|
-
|
|
55
|
-
tui._ui_tick = _fake_ui_tick
|
|
56
|
-
return tui
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class TestTUIStartupLatency(unittest.IsolatedAsyncioTestCase):
|
|
60
|
-
async def test_plugin_init_pending_does_not_block_run_async(self) -> None:
|
|
61
|
-
plugin_started = asyncio.Event()
|
|
62
|
-
plugin_cancelled = asyncio.Event()
|
|
63
|
-
app = _FakeApp(wait_for=plugin_started)
|
|
64
|
-
tui = _make_tui(app)
|
|
65
|
-
|
|
66
|
-
async def _stuck_plugin_init() -> None:
|
|
67
|
-
plugin_started.set()
|
|
68
|
-
try:
|
|
69
|
-
await asyncio.Event().wait()
|
|
70
|
-
finally:
|
|
71
|
-
plugin_cancelled.set()
|
|
72
|
-
|
|
73
|
-
tui._init_plugins = _stuck_plugin_init
|
|
74
|
-
|
|
75
|
-
with patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout):
|
|
76
|
-
await asyncio.wait_for(tui.run(), timeout=0.2)
|
|
77
|
-
|
|
78
|
-
self.assertTrue(plugin_started.is_set())
|
|
79
|
-
self.assertTrue(app.entered.is_set())
|
|
80
|
-
self.assertTrue(plugin_cancelled.is_set())
|
|
81
|
-
self.assertIsNone(tui._plugin_init_task)
|
|
82
|
-
self.assertTrue(tui._renderer.closed)
|
|
83
|
-
|
|
84
|
-
async def test_plugin_init_exception_is_logged_and_run_async_still_runs(self) -> None:
|
|
85
|
-
app = _FakeApp()
|
|
86
|
-
tui = _make_tui(app)
|
|
87
|
-
|
|
88
|
-
async def _failing_plugin_init() -> None:
|
|
89
|
-
raise RuntimeError("plugin boom")
|
|
90
|
-
|
|
91
|
-
tui._init_plugins = _failing_plugin_init
|
|
92
|
-
|
|
93
|
-
with (
|
|
94
|
-
patch("comate_cli.terminal_agent.tui.patch_stdout", _noop_patch_stdout),
|
|
95
|
-
self.assertLogs("comate_cli.terminal_agent.tui", level="ERROR") as logs,
|
|
96
|
-
):
|
|
97
|
-
await asyncio.wait_for(tui.run(), timeout=0.2)
|
|
98
|
-
|
|
99
|
-
self.assertTrue(app.entered.is_set())
|
|
100
|
-
self.assertIn("Plugin init failed at startup", "\n".join(logs.output))
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if __name__ == "__main__":
|
|
104
|
-
unittest.main(verbosity=2)
|
|
File without changes
|
|
File without changes
|