comate-cli 0.8.2__tar.gz → 0.8.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.8.2 → comate_cli-0.8.4}/PKG-INFO +2 -2
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/event_renderer.py +103 -1
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/render_panels.py +49 -14
- {comate_cli-0.8.2 → comate_cli-0.8.4}/docs/cli-rpc-stdio-guide.md +6 -1
- {comate_cli-0.8.2 → comate_cli-0.8.4}/pyproject.toml +2 -2
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/recap/test_idle_recap_controller.py +55 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_completion_status_panel.py +62 -0
- comate_cli-0.8.4/tests/test_event_renderer_tool_args_progress.py +193 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_rpc_protocol.py +41 -1
- comate_cli-0.8.4/tests/test_tui_provider_error_shield.py +427 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/.gitignore +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/CHANGELOG.md +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/CONTEXT.md +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/README.md +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/__init__.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/__main__.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/main.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/app.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/builtin_commands/.keep +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/config/__init__.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/config/model.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/config/picker.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/config/picker_state.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/config/store.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/figures.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/file_ref_hint.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/goal_resume_view.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/history_printer.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/operation_state.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/preflight_wizard.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/print_mode.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/recap/__init__.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/recap/controller.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/resume_picker.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/resume_preview.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/startup_profile.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/statusline/__init__.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/statusline/model.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/statusline/picker.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/statusline/picker_state.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/statusline/store.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tool_fold.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tool_result_formatters.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tool_result_store.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tool_view.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/comate_cli/terminal_agent/update_check.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/dist-comate/.gitignore +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/dist-comate/comate_cli-0.7.10-py3-none-any.whl +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/dist-comate/comate_cli-0.7.10.tar.gz +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/dist-hico/.gitignore +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/dist-hico/hico_cli-0.7.17-py3-none-any.whl +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/dist-hico/hico_cli-0.7.17.tar.gz +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/config/__init__.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/config/test_model.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/config/test_picker_state.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/config/test_picker_ui.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/config/test_roundtrip.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/config/test_store_load.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/config/test_store_save.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/conftest.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/fixtures/fake_mcp_misbehaving_stdout.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/recap/test_recap_history_rendering.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/recap/test_tui_idle_recap_integration.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/statusline/__init__.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/statusline/test_model.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/statusline/test_picker_state.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/statusline/test_store.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_app_startup_latency.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_app_token_cost_config.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_btw_slash_command.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_context_command.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_event_renderer.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_event_renderer_boundary.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_event_renderer_e2e.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_event_renderer_log_boundary.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_event_renderer_log_queue.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_event_renderer_streaming.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_event_renderer_tool_fold.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_file_ref_hint.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_format_error.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_goal_resume_tui.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_goal_resume_view.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_goal_slash_command.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_handle_error.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_history_printer.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_history_printer_log.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_history_printer_subtitle_position.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_history_printer_tool_fold.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_history_sync.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_history_sync_tool_fold.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_input_history.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_logo.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_main_args.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_markdown_render.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_model_switch_command.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_plugin_operation_state.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_preflight.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_question_view.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_resume_picker.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_resume_preview.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_session_query_token_summary.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_shutdown_noise_guard.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_shutdown_noise_integration.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_slash_clear.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_startup_import_budget.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_startup_profile.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_status_bar.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_task_poll.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tips.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tool_fold.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tool_fold_panel.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tool_result_formatters.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tool_result_store.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tool_result_viewer.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tool_result_viewer_key_bindings.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tool_view.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_transcript_viewer.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_transcript_viewer_tool_fold.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_builtin_slash_chain.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_esc_queue.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_llm_retry_panel.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_local_interactive_barrier.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_paste_newline_guard.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_queue_preview.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_queue_sdk_source.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_scheduled_fire_log.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_send_undo_window.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_startup_latency.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_team_messages.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_thinking_display.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_update_check.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_usage_command.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tests/test_wrap_user_text.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tools/logo_lab.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/tools/nico_preview.py +0 -0
- {comate_cli-0.8.2 → comate_cli-0.8.4}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: comate-cli
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Requires-Python: <3.14,>=3.11
|
|
17
17
|
Requires-Dist: charset-normalizer==3.4.7
|
|
18
|
-
Requires-Dist: comate-agent-sdk<0.9.
|
|
18
|
+
Requires-Dist: comate-agent-sdk<0.9.20,>=0.8.0a1
|
|
19
19
|
Requires-Dist: concurrent-log-handler>=0.9.25
|
|
20
20
|
Requires-Dist: curl-cffi==0.13.0
|
|
21
21
|
Requires-Dist: packaging>=21.0
|
|
@@ -35,8 +35,10 @@ from comate_agent_sdk.agent.events import (
|
|
|
35
35
|
ThinkingDeltaEvent,
|
|
36
36
|
TaskUpdatedEvent,
|
|
37
37
|
ToolCallEvent,
|
|
38
|
+
ToolCallArgsProgressEvent,
|
|
38
39
|
ToolCallStartEvent,
|
|
39
40
|
ToolResultEvent,
|
|
41
|
+
StreamDisplayResetEvent,
|
|
40
42
|
UsageDeltaEvent,
|
|
41
43
|
UserQuestionEvent,
|
|
42
44
|
)
|
|
@@ -81,6 +83,9 @@ _DEFAULT_TOOL_PANEL_MAX_LINES = 4
|
|
|
81
83
|
_DEFAULT_TASK_PANEL_MAX_LINES = 6
|
|
82
84
|
_RECENT_TEAM_EVENT_CACHE_SIZE = 128
|
|
83
85
|
_SYSTEM_MESSAGE_DEDUPE_WINDOW_SECONDS = 0.5
|
|
86
|
+
TOOL_ARGS_PROGRESS_VISIBLE_TOKENS = 1000
|
|
87
|
+
TOOL_ARGS_PROGRESS_VISIBLE_AFTER_SECONDS = 1.5
|
|
88
|
+
TOOL_ARGS_PROGRESS_VISIBLE_MIN_TOKENS_AFTER_DELAY = 256
|
|
84
89
|
|
|
85
90
|
|
|
86
91
|
def _truncate(content: str, max_len: int = 120) -> str:
|
|
@@ -129,6 +134,13 @@ def _format_tokens(token_count: int) -> str:
|
|
|
129
134
|
return f"{compact}k tok"
|
|
130
135
|
|
|
131
136
|
|
|
137
|
+
def _format_tool_args_tokens(token_count: int) -> str:
|
|
138
|
+
tokens = max(int(token_count), 0)
|
|
139
|
+
if tokens < 1_000:
|
|
140
|
+
return f"{tokens} tokens"
|
|
141
|
+
return f"{tokens / 1_000:.1f}k tokens"
|
|
142
|
+
|
|
143
|
+
|
|
132
144
|
def _split_fragments_by_newline(
|
|
133
145
|
fragments: list[tuple[str, str]],
|
|
134
146
|
) -> list[list[tuple[str, str]]]:
|
|
@@ -245,6 +257,16 @@ class _RunningTool:
|
|
|
245
257
|
subagent_model_name: str = ""
|
|
246
258
|
|
|
247
259
|
|
|
260
|
+
@dataclass
|
|
261
|
+
class _ToolArgsProgressState:
|
|
262
|
+
tool_call_id: str
|
|
263
|
+
tool_name: str
|
|
264
|
+
started_at_monotonic: float
|
|
265
|
+
last_updated_at_monotonic: float
|
|
266
|
+
estimated_tokens: int = 0
|
|
267
|
+
visible: bool = False
|
|
268
|
+
|
|
269
|
+
|
|
248
270
|
@dataclass(frozen=True)
|
|
249
271
|
class ActiveLLMRetrySnapshot:
|
|
250
272
|
retry_id: str
|
|
@@ -309,6 +331,7 @@ class EventRenderer:
|
|
|
309
331
|
self._fallback_tool_result_sequence = 0
|
|
310
332
|
self._assistant_buffer = ""
|
|
311
333
|
self._pending_tool_starts: set[str] = set()
|
|
334
|
+
self._tool_args_progress: dict[str, _ToolArgsProgressState] = {}
|
|
312
335
|
self._loading_state: LoadingState = LoadingState.idle()
|
|
313
336
|
self._current_tasks: list[dict[str, Any]] = []
|
|
314
337
|
self._current_task_title: str | None = None
|
|
@@ -668,6 +691,55 @@ class EventRenderer:
|
|
|
668
691
|
self._text_delta_started = False
|
|
669
692
|
self._active_text_message_id = None
|
|
670
693
|
|
|
694
|
+
def _clear_tool_args_progress(self, tool_call_id: str | None = None) -> None:
|
|
695
|
+
if tool_call_id is None:
|
|
696
|
+
self._tool_args_progress.clear()
|
|
697
|
+
return
|
|
698
|
+
normalized_id = str(tool_call_id or "").strip()
|
|
699
|
+
if normalized_id:
|
|
700
|
+
self._tool_args_progress.pop(normalized_id, None)
|
|
701
|
+
|
|
702
|
+
def _start_tool_args_progress(self, tool_call_id: str, tool_name: str) -> None:
|
|
703
|
+
normalized_id = str(tool_call_id or "").strip()
|
|
704
|
+
if not normalized_id:
|
|
705
|
+
return
|
|
706
|
+
now = time.monotonic()
|
|
707
|
+
state = self._tool_args_progress.get(normalized_id)
|
|
708
|
+
if state is None:
|
|
709
|
+
self._tool_args_progress[normalized_id] = _ToolArgsProgressState(
|
|
710
|
+
tool_call_id=normalized_id,
|
|
711
|
+
tool_name=str(tool_name or "").strip(),
|
|
712
|
+
started_at_monotonic=now,
|
|
713
|
+
last_updated_at_monotonic=now,
|
|
714
|
+
)
|
|
715
|
+
return
|
|
716
|
+
if tool_name:
|
|
717
|
+
state.tool_name = str(tool_name).strip()
|
|
718
|
+
|
|
719
|
+
def _handle_tool_args_progress(self, event: ToolCallArgsProgressEvent) -> None:
|
|
720
|
+
normalized_id = str(event.tool_call_id or "").strip()
|
|
721
|
+
if not normalized_id:
|
|
722
|
+
return
|
|
723
|
+
state = self._tool_args_progress.get(normalized_id)
|
|
724
|
+
if state is None:
|
|
725
|
+
return
|
|
726
|
+
now = time.monotonic()
|
|
727
|
+
state.last_updated_at_monotonic = now
|
|
728
|
+
if event.tool:
|
|
729
|
+
state.tool_name = str(event.tool).strip()
|
|
730
|
+
state.estimated_tokens = max(int(event.estimated_tokens), 0)
|
|
731
|
+
if state.visible:
|
|
732
|
+
return
|
|
733
|
+
elapsed = now - state.started_at_monotonic
|
|
734
|
+
state.visible = (
|
|
735
|
+
state.estimated_tokens >= TOOL_ARGS_PROGRESS_VISIBLE_TOKENS
|
|
736
|
+
or (
|
|
737
|
+
elapsed >= TOOL_ARGS_PROGRESS_VISIBLE_AFTER_SECONDS
|
|
738
|
+
and state.estimated_tokens
|
|
739
|
+
>= TOOL_ARGS_PROGRESS_VISIBLE_MIN_TOKENS_AFTER_DELAY
|
|
740
|
+
)
|
|
741
|
+
)
|
|
742
|
+
|
|
671
743
|
def _should_drop_duplicate_system_message(
|
|
672
744
|
self,
|
|
673
745
|
*,
|
|
@@ -699,6 +771,7 @@ class EventRenderer:
|
|
|
699
771
|
self._active_llm_retry = None
|
|
700
772
|
self._flush_assistant_segment()
|
|
701
773
|
self._pending_tool_starts.clear()
|
|
774
|
+
self._clear_tool_args_progress()
|
|
702
775
|
self._turn_received_text_delta = False
|
|
703
776
|
self._turn_received_thinking_delta = False
|
|
704
777
|
self._text_delta_started = False
|
|
@@ -760,6 +833,7 @@ class EventRenderer:
|
|
|
760
833
|
self._flush_assistant_segment()
|
|
761
834
|
self.flush_pending_logs()
|
|
762
835
|
self._pending_tool_starts.clear()
|
|
836
|
+
self._clear_tool_args_progress()
|
|
763
837
|
self._rebuild_loading_line()
|
|
764
838
|
|
|
765
839
|
def tick_progress(self) -> None:
|
|
@@ -782,6 +856,7 @@ class EventRenderer:
|
|
|
782
856
|
self._flush_assistant_segment()
|
|
783
857
|
self._commit_active_tool_fold()
|
|
784
858
|
self._pending_tool_starts.clear()
|
|
859
|
+
self._clear_tool_args_progress()
|
|
785
860
|
# spec §6.4:用户中断 → 5 个新字段全部丢弃不入队
|
|
786
861
|
self._drop_all_pending()
|
|
787
862
|
self._rebuild_loading_line()
|
|
@@ -802,6 +877,7 @@ class EventRenderer:
|
|
|
802
877
|
self._active_tool_fold.clear()
|
|
803
878
|
self._assistant_buffer = ""
|
|
804
879
|
self._pending_tool_starts.clear()
|
|
880
|
+
self._clear_tool_args_progress()
|
|
805
881
|
self._turn_received_text_delta = False
|
|
806
882
|
self._turn_received_thinking_delta = False
|
|
807
883
|
self._text_delta_started = False
|
|
@@ -869,6 +945,15 @@ class EventRenderer:
|
|
|
869
945
|
拼接到 spinner phrase。无容器时返回 ""。"""
|
|
870
946
|
return self._loading_aux_text
|
|
871
947
|
|
|
948
|
+
def tool_args_progress_text(self) -> str:
|
|
949
|
+
visible_states = [
|
|
950
|
+
state for state in self._tool_args_progress.values() if state.visible
|
|
951
|
+
]
|
|
952
|
+
if not visible_states:
|
|
953
|
+
return ""
|
|
954
|
+
total_tokens = sum(state.estimated_tokens for state in visible_states)
|
|
955
|
+
return f"↓ {_format_tool_args_tokens(total_tokens)}"
|
|
956
|
+
|
|
872
957
|
def active_llm_retry_snapshot(self) -> ActiveLLMRetrySnapshot | None:
|
|
873
958
|
return self._active_llm_retry
|
|
874
959
|
|
|
@@ -1467,7 +1552,14 @@ class EventRenderer:
|
|
|
1467
1552
|
def handle_event(self, event: Any) -> tuple[bool, list[dict[str, Any]] | None]:
|
|
1468
1553
|
if not isinstance(
|
|
1469
1554
|
event,
|
|
1470
|
-
(
|
|
1555
|
+
(
|
|
1556
|
+
TextEvent,
|
|
1557
|
+
TextDeltaEvent,
|
|
1558
|
+
ThinkingDeltaEvent,
|
|
1559
|
+
ToolCallStartEvent,
|
|
1560
|
+
ToolCallArgsProgressEvent,
|
|
1561
|
+
StreamDisplayResetEvent,
|
|
1562
|
+
),
|
|
1471
1563
|
):
|
|
1472
1564
|
self._flush_assistant_segment()
|
|
1473
1565
|
|
|
@@ -1498,6 +1590,12 @@ class EventRenderer:
|
|
|
1498
1590
|
normalized_id = str(tool_call_id or "").strip()
|
|
1499
1591
|
if normalized_id and normalized_id not in self._pending_tool_starts:
|
|
1500
1592
|
self._pending_tool_starts.add(normalized_id)
|
|
1593
|
+
self._start_tool_args_progress(normalized_id, tool_name)
|
|
1594
|
+
case ToolCallArgsProgressEvent():
|
|
1595
|
+
self._handle_tool_args_progress(event)
|
|
1596
|
+
case StreamDisplayResetEvent(reason="stream_fallback"):
|
|
1597
|
+
self._pending_tool_starts.clear()
|
|
1598
|
+
self._clear_tool_args_progress()
|
|
1501
1599
|
case ThinkingEvent(content=thinking):
|
|
1502
1600
|
if self._turn_received_thinking_delta:
|
|
1503
1601
|
pass
|
|
@@ -1566,6 +1664,7 @@ class EventRenderer:
|
|
|
1566
1664
|
severity="warning",
|
|
1567
1665
|
)
|
|
1568
1666
|
case ToolCallEvent(tool=tool_name, args=arguments, tool_call_id=tool_call_id):
|
|
1667
|
+
self._clear_tool_args_progress(tool_call_id)
|
|
1569
1668
|
args_dict = arguments if isinstance(arguments, dict) else {"_raw": str(arguments)}
|
|
1570
1669
|
# Store args for ToolResult phase lookup.
|
|
1571
1670
|
self._tool_call_args[tool_call_id] = args_dict
|
|
@@ -1584,6 +1683,7 @@ class EventRenderer:
|
|
|
1584
1683
|
return (False, None)
|
|
1585
1684
|
self._append_tool_call(tool_name, args_dict, tool_call_id)
|
|
1586
1685
|
case ToolResultEvent(tool=tool_name, result=result, tool_call_id=tool_call_id, is_error=is_error, metadata=metadata, output=output):
|
|
1686
|
+
self._clear_tool_args_progress(tool_call_id)
|
|
1587
1687
|
stored_args = self._tool_call_args.pop(tool_call_id, {})
|
|
1588
1688
|
if self._active_tool_fold.has_call(tool_call_id):
|
|
1589
1689
|
display_name = resolve_display_tool_name(tool_name, stored_args)
|
|
@@ -1764,11 +1864,13 @@ class EventRenderer:
|
|
|
1764
1864
|
# Cancellation/error paths may emit StepCompleteEvent without ToolResultEvent.
|
|
1765
1865
|
# Ensure tool panel state is cleaned up by step id.
|
|
1766
1866
|
self._running_tools.pop(step_id, None)
|
|
1867
|
+
self._clear_tool_args_progress(step_id)
|
|
1767
1868
|
case StopEvent(reason=reason):
|
|
1768
1869
|
self._active_llm_retry = None
|
|
1769
1870
|
self._flush_assistant_segment()
|
|
1770
1871
|
self._commit_active_tool_fold()
|
|
1771
1872
|
self._pending_tool_starts.clear()
|
|
1873
|
+
self._clear_tool_args_progress()
|
|
1772
1874
|
# Safety net: if any running tool rows remain, stop event means this turn is ending.
|
|
1773
1875
|
self._running_tools.clear()
|
|
1774
1876
|
self._hidden_thinking_badge_text = ""
|
|
@@ -823,35 +823,55 @@ class RenderPanelsMixin:
|
|
|
823
823
|
return ""
|
|
824
824
|
return str(badge_fn() or "").strip()
|
|
825
825
|
|
|
826
|
+
def _tool_args_progress_text(self) -> str:
|
|
827
|
+
progress_fn = getattr(self._renderer, "tool_args_progress_text", None)
|
|
828
|
+
if not callable(progress_fn):
|
|
829
|
+
return ""
|
|
830
|
+
return str(progress_fn() or "").strip()
|
|
831
|
+
|
|
826
832
|
@staticmethod
|
|
827
833
|
def _fragments_width(frags: list[tuple[str, str]]) -> int:
|
|
828
834
|
return sum(get_cwidth(char) for _, text in frags for char in text)
|
|
829
835
|
|
|
830
|
-
def _running_status_suffix_fragments(
|
|
836
|
+
def _running_status_suffix_fragments(
|
|
837
|
+
self,
|
|
838
|
+
*,
|
|
839
|
+
max_width: int | None = None,
|
|
840
|
+
) -> list[tuple[str, str]]:
|
|
831
841
|
"""Build the dim parenthetical status suffix for active work."""
|
|
832
842
|
status_style = "fg:#6B7280"
|
|
833
843
|
badge_style = "fg:#FBBF24 bold"
|
|
834
844
|
|
|
835
845
|
run_start: float | None = getattr(self, "_run_start_time", None)
|
|
846
|
+
args_progress = self._tool_args_progress_text()
|
|
836
847
|
badge = self._hidden_thinking_badge_text()
|
|
837
|
-
if run_start is None and not badge:
|
|
848
|
+
if run_start is None and not args_progress and not badge:
|
|
838
849
|
return []
|
|
839
850
|
|
|
840
851
|
parts: list[tuple[str, str]] = [(status_style, " (")]
|
|
841
|
-
|
|
842
|
-
if
|
|
852
|
+
has_parts = False
|
|
853
|
+
if run_start is not None:
|
|
843
854
|
elapsed = time.monotonic() - run_start
|
|
844
|
-
parts.append(
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
)
|
|
849
|
-
)
|
|
855
|
+
parts.append((status_style, self._format_run_elapsed(elapsed)))
|
|
856
|
+
has_parts = True
|
|
857
|
+
if args_progress:
|
|
858
|
+
if has_parts:
|
|
859
|
+
parts.append((status_style, f" {BULLET} "))
|
|
860
|
+
parts.append((status_style, args_progress))
|
|
861
|
+
has_parts = True
|
|
862
|
+
if run_start is not None:
|
|
863
|
+
if has_parts:
|
|
864
|
+
parts.append((status_style, f" {BULLET} "))
|
|
865
|
+
parts.append((status_style, "ctrl+c to interrupt"))
|
|
866
|
+
has_parts = True
|
|
850
867
|
if badge:
|
|
851
|
-
if
|
|
868
|
+
if has_parts:
|
|
852
869
|
parts.append((status_style, f" {BULLET} "))
|
|
853
870
|
parts.append((badge_style, badge))
|
|
854
871
|
parts.append((status_style, ")"))
|
|
872
|
+
if max_width is not None and self._fragments_width(parts) > max_width:
|
|
873
|
+
clipped = fit_single_line("".join(text for _, text in parts), max_width)
|
|
874
|
+
return [(status_style, clipped)]
|
|
855
875
|
return parts
|
|
856
876
|
|
|
857
877
|
def _visible_loading_tip(self) -> str:
|
|
@@ -897,7 +917,9 @@ class RenderPanelsMixin:
|
|
|
897
917
|
) -> list[tuple[str, str]]:
|
|
898
918
|
"""用低噪音 spinner + 流光走字渲染 loading 文案."""
|
|
899
919
|
width = self._terminal_width()
|
|
900
|
-
status_suffix = self._running_status_suffix_fragments(
|
|
920
|
+
status_suffix = self._running_status_suffix_fragments(
|
|
921
|
+
max_width=max(width - 10, 8)
|
|
922
|
+
)
|
|
901
923
|
|
|
902
924
|
# glyph(1) + space(1) = 2,再减去运行状态后缀宽度
|
|
903
925
|
phrase_budget = width - 2 - self._fragments_width(status_suffix)
|
|
@@ -921,7 +943,10 @@ class RenderPanelsMixin:
|
|
|
921
943
|
frags: list[tuple[str, str]],
|
|
922
944
|
) -> list[tuple[str, str]]:
|
|
923
945
|
"""Append the running status suffix before any Rich trailing newline."""
|
|
924
|
-
|
|
946
|
+
width = self._terminal_width()
|
|
947
|
+
status_suffix = self._running_status_suffix_fragments(
|
|
948
|
+
max_width=max(width - 8, 8)
|
|
949
|
+
)
|
|
925
950
|
if not status_suffix:
|
|
926
951
|
return frags
|
|
927
952
|
# Rich __rich_console__ 末尾会输出一个 "\n" segment,需要先剥掉再追加,
|
|
@@ -930,7 +955,17 @@ class RenderPanelsMixin:
|
|
|
930
955
|
trimmed = list(frags)
|
|
931
956
|
while trimmed and trimmed[-1][1] == "\n":
|
|
932
957
|
tail.insert(0, trimmed.pop())
|
|
933
|
-
|
|
958
|
+
base_width = self._fragments_width(trimmed)
|
|
959
|
+
suffix_width = self._fragments_width(status_suffix)
|
|
960
|
+
if base_width + suffix_width <= width:
|
|
961
|
+
return trimmed + status_suffix + tail
|
|
962
|
+
|
|
963
|
+
base_budget = max(width - suffix_width, 8)
|
|
964
|
+
clipped_base = fit_single_line(
|
|
965
|
+
"".join(text for _, text in trimmed),
|
|
966
|
+
base_budget,
|
|
967
|
+
)
|
|
968
|
+
return [("class:loading", clipped_base)] + status_suffix + tail
|
|
934
969
|
|
|
935
970
|
def _loading_height(self) -> int:
|
|
936
971
|
if self._should_hide_loading_panel():
|
|
@@ -217,8 +217,10 @@ stdio bridge 明确不承担历史重放职责。需要历史请走 `comate resu
|
|
|
217
217
|
| `TextEvent` | 文本块最终落地(入历史) | `content`, `structured_output`, `force_display` |
|
|
218
218
|
| `ThinkingDeltaEvent` / `ThinkingEvent` | 思维链增量 / 落地 | `content`/`delta` |
|
|
219
219
|
| `ToolCallStartEvent` | 工具调用开始(transient) | `tool_call_id`, `tool` |
|
|
220
|
+
| `ToolCallArgsProgressEvent` | 工具参数仍在流式生成中的 UI-only 进度(transient) | `tool_call_id`, `tool`, `estimated_tokens` |
|
|
220
221
|
| `ToolCallEvent` | 工具调用入参确定 | `tool`, `args`, `tool_call_id`, `input` |
|
|
221
222
|
| `ToolResultEvent` | 工具返回 | `tool`, `result`, `is_error`, `output` |
|
|
223
|
+
| `StreamDisplayResetEvent` | 内部 transient display reset;stream fallback 前要求客户端清理 partial display state;facade 不公开 | `reason` |
|
|
222
224
|
| `StepStartEvent` / `StepCompleteEvent` | 单步边界 | — |
|
|
223
225
|
| `StopEvent` | 流终止信号 | `reason` |
|
|
224
226
|
| `UsageDeltaEvent` | token 用量 | — |
|
|
@@ -228,7 +230,10 @@ stdio bridge 明确不承担历史重放职责。需要历史请走 `comate resu
|
|
|
228
230
|
**消费建议**:
|
|
229
231
|
- 只关心最终答案:忽略所有 `*DeltaEvent`,只收集 `TextEvent`,以 `StopEvent` 为终止。
|
|
230
232
|
- 想做打字机效果:按 `message_id` 累加 `TextDeltaEvent`,`TextEvent` 到达后用 `force_display` 决定是否覆盖。
|
|
231
|
-
- 工具调用可视化:用 `ToolCallStartEvent` → `ToolCallEvent` → `ToolResultEvent
|
|
233
|
+
- 工具调用可视化:用 `ToolCallStartEvent` → 可选多个 `ToolCallArgsProgressEvent` → `ToolCallEvent` → `ToolResultEvent`。
|
|
234
|
+
- stream fallback:收到 `StreamDisplayResetEvent(reason="stream_fallback")` 时清理任何 pending tool args progress / pending tool start 展示;不要把它显示为 assistant 内容。
|
|
235
|
+
|
|
236
|
+
`ToolCallArgsProgressEvent` 与 `StreamDisplayResetEvent` 都不是 assistant message content,也不应作为 transcript 内容持久化。`ToolCallArgsProgressEvent.estimated_tokens` 只用于 UI estimate,不进入 provider usage、context token budget 或 compaction 阈值。`ToolCallArgsProgressEvent` 在 facade 中属于 `include_partial_messages=True` 时才公开的 partial event;`StreamDisplayResetEvent` 是 CLI/RPC internal transient display control,facade adapter 会忽略。
|
|
232
237
|
|
|
233
238
|
事件被序列化时,dataclass 字段直接展开到 `params`,并补一个 `event_type` 字段。无法被 JSON 序列化的对象会 fallback 为 `str(value)`。
|
|
234
239
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "comate-cli"
|
|
7
|
-
version = "0.8.
|
|
7
|
+
version = "0.8.4"
|
|
8
8
|
description = "Comate terminal CLI built on comate-agent-sdk"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11,<3.14"
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Environment :: Console",
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
|
-
"comate-agent-sdk>=0.8.0a1,<0.9.
|
|
25
|
+
"comate-agent-sdk>=0.8.0a1,<0.9.20",
|
|
26
26
|
"rich>=14.0",
|
|
27
27
|
"prompt-toolkit>=3.0",
|
|
28
28
|
"packaging>=21.0",
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import gc
|
|
4
5
|
import unittest
|
|
5
6
|
|
|
6
7
|
from comate_agent_sdk.agent.recap import RecapResult
|
|
8
|
+
from comate_agent_sdk.llm.exceptions import ModelRateLimitError
|
|
7
9
|
from comate_cli.terminal_agent.recap.controller import IdleRecapController, IdleRecapOptions
|
|
8
10
|
|
|
9
11
|
|
|
@@ -298,6 +300,59 @@ class TestIdleRecapController(unittest.IsolatedAsyncioTestCase):
|
|
|
298
300
|
|
|
299
301
|
self.assertEqual(appended, [])
|
|
300
302
|
|
|
303
|
+
async def test_cancelled_generation_provider_error_is_observed(self) -> None:
|
|
304
|
+
appended: list[str] = []
|
|
305
|
+
started = asyncio.Event()
|
|
306
|
+
provider_error_raised = asyncio.Event()
|
|
307
|
+
loop = asyncio.get_running_loop()
|
|
308
|
+
previous_handler = loop.get_exception_handler()
|
|
309
|
+
unretrieved_contexts: list[dict[str, object]] = []
|
|
310
|
+
|
|
311
|
+
async def generate(parent_runtime):
|
|
312
|
+
del parent_runtime
|
|
313
|
+
started.set()
|
|
314
|
+
try:
|
|
315
|
+
await asyncio.sleep(60)
|
|
316
|
+
except asyncio.CancelledError:
|
|
317
|
+
provider_error_raised.set()
|
|
318
|
+
raise ModelRateLimitError("rate limited", model="fake:model")
|
|
319
|
+
|
|
320
|
+
def _collect_unretrieved(loop, context): # type: ignore[no-untyped-def]
|
|
321
|
+
del loop
|
|
322
|
+
if context.get("message") == "Task exception was never retrieved":
|
|
323
|
+
unretrieved_contexts.append(dict(context))
|
|
324
|
+
return
|
|
325
|
+
if previous_handler is not None:
|
|
326
|
+
previous_handler(asyncio.get_running_loop(), context)
|
|
327
|
+
|
|
328
|
+
controller = IdleRecapController(
|
|
329
|
+
options=IdleRecapOptions(enabled=True, delay_seconds=0),
|
|
330
|
+
generate=generate,
|
|
331
|
+
append=appended.append,
|
|
332
|
+
is_idle=lambda: True,
|
|
333
|
+
get_input_text=lambda: "",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
loop.set_exception_handler(_collect_unretrieved)
|
|
337
|
+
try:
|
|
338
|
+
controller.arm_after_stop(
|
|
339
|
+
stop_seq=1,
|
|
340
|
+
parent_runtime=object(),
|
|
341
|
+
stop_reason="completed",
|
|
342
|
+
)
|
|
343
|
+
await started.wait()
|
|
344
|
+
controller.cancel_for_busy()
|
|
345
|
+
await controller.wait_for_current()
|
|
346
|
+
await asyncio.wait_for(provider_error_raised.wait(), timeout=3.0)
|
|
347
|
+
await asyncio.sleep(0)
|
|
348
|
+
gc.collect()
|
|
349
|
+
await asyncio.sleep(0)
|
|
350
|
+
finally:
|
|
351
|
+
loop.set_exception_handler(previous_handler)
|
|
352
|
+
|
|
353
|
+
self.assertEqual(appended, [])
|
|
354
|
+
self.assertEqual(unretrieved_contexts, [])
|
|
355
|
+
|
|
301
356
|
async def test_generate_exception_does_not_poison_next_stop(self) -> None:
|
|
302
357
|
appended: list[str] = []
|
|
303
358
|
call_count = 0
|
|
@@ -7,6 +7,7 @@ from unittest.mock import patch
|
|
|
7
7
|
|
|
8
8
|
from prompt_toolkit.completion import Completion
|
|
9
9
|
from prompt_toolkit.document import Document
|
|
10
|
+
from prompt_toolkit.utils import get_cwidth
|
|
10
11
|
from rich.text import Text
|
|
11
12
|
|
|
12
13
|
from comate_cli.terminal_agent.event_renderer import ActiveLLMRetrySnapshot
|
|
@@ -68,6 +69,7 @@ class _FakeRenderer:
|
|
|
68
69
|
self.running_tools = False
|
|
69
70
|
self.auto_compacting = False
|
|
70
71
|
self.hidden_thinking_badge = ""
|
|
72
|
+
self.tool_args_progress = ""
|
|
71
73
|
self.todo_lines: list[str] = []
|
|
72
74
|
self.tool_entries: list[tuple[int, str]] = [(0, "Agent(task) · 1s · +1 tool uses")]
|
|
73
75
|
self.retry_snapshot: ActiveLLMRetrySnapshot | None = None
|
|
@@ -93,6 +95,9 @@ class _FakeRenderer:
|
|
|
93
95
|
def hidden_thinking_badge_text(self) -> str:
|
|
94
96
|
return self.hidden_thinking_badge
|
|
95
97
|
|
|
98
|
+
def tool_args_progress_text(self) -> str:
|
|
99
|
+
return self.tool_args_progress
|
|
100
|
+
|
|
96
101
|
def active_llm_retry_snapshot(self) -> ActiveLLMRetrySnapshot | None:
|
|
97
102
|
return self.retry_snapshot
|
|
98
103
|
|
|
@@ -436,6 +441,63 @@ class TestCompletionStatusPanel(unittest.TestCase):
|
|
|
436
441
|
)
|
|
437
442
|
)
|
|
438
443
|
|
|
444
|
+
def test_loading_text_appends_tool_args_progress_before_interrupt_hint(self) -> None:
|
|
445
|
+
tui = _FakeTUI(None, width=120)
|
|
446
|
+
tui._busy = True
|
|
447
|
+
tui._run_start_time = 780.0
|
|
448
|
+
tui._renderer.tool_args_progress = "↓ 1.2k tokens"
|
|
449
|
+
tui._renderer.hidden_thinking_badge = "ultra thinking"
|
|
450
|
+
|
|
451
|
+
with patch(
|
|
452
|
+
"comate_cli.terminal_agent.tui_parts.render_panels.time.monotonic",
|
|
453
|
+
return_value=1000.0,
|
|
454
|
+
):
|
|
455
|
+
fragments = tui._loading_text()
|
|
456
|
+
text = _join_fragments(fragments)
|
|
457
|
+
|
|
458
|
+
self.assertIn(
|
|
459
|
+
"Thinking (3m 40s • ↓ 1.2k tokens • ctrl+c to interrupt • ultra thinking)",
|
|
460
|
+
text,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def test_loading_text_keeps_status_suffix_on_one_line_when_width_is_tight(self) -> None:
|
|
464
|
+
tui = _FakeTUI(None, width=40)
|
|
465
|
+
tui._busy = True
|
|
466
|
+
tui._run_start_time = 780.0
|
|
467
|
+
tui._renderer.tool_args_progress = "↓ 12.5k tokens"
|
|
468
|
+
|
|
469
|
+
with patch(
|
|
470
|
+
"comate_cli.terminal_agent.tui_parts.render_panels.time.monotonic",
|
|
471
|
+
return_value=1000.0,
|
|
472
|
+
):
|
|
473
|
+
fragments = tui._loading_text()
|
|
474
|
+
text = _join_fragments(fragments)
|
|
475
|
+
|
|
476
|
+
self.assertNotIn("\n", text)
|
|
477
|
+
self.assertLessEqual(get_cwidth(text), 40)
|
|
478
|
+
self.assertIn("↓ 12.5k tokens", text)
|
|
479
|
+
|
|
480
|
+
def test_animator_loading_text_keeps_status_suffix_inside_tight_width(self) -> None:
|
|
481
|
+
tui = _FakeTUI(None, width=40)
|
|
482
|
+
tui._busy = True
|
|
483
|
+
tui._run_start_time = 780.0
|
|
484
|
+
tui._renderer.tool_args_progress = "↓ 12.5k tokens"
|
|
485
|
+
tui._animator = SimpleNamespace(
|
|
486
|
+
is_active=True,
|
|
487
|
+
renderable=lambda: Text("Generating a very long phrase"),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
with patch(
|
|
491
|
+
"comate_cli.terminal_agent.tui_parts.render_panels.time.monotonic",
|
|
492
|
+
return_value=1000.0,
|
|
493
|
+
):
|
|
494
|
+
fragments = tui._loading_text()
|
|
495
|
+
text = _join_fragments(fragments)
|
|
496
|
+
|
|
497
|
+
self.assertNotIn("\n", text)
|
|
498
|
+
self.assertLessEqual(get_cwidth(text), 40)
|
|
499
|
+
self.assertIn("↓ 12.5k tokens", text)
|
|
500
|
+
|
|
439
501
|
def test_loading_text_appends_hidden_thinking_badge_to_active_animator(self) -> None:
|
|
440
502
|
tui = _FakeTUI(None, width=80)
|
|
441
503
|
tui._busy = True
|